版本信息

AndroidStudio 3.5.2

FFmpeg 4.0.2

背景
AndroidStudio3.5.1下搭建FFmpeg环境

Android使用FFmpeg动态库播放视频

Android基于OpenSL ES的音频播放

播放器架构

关键代码梳理

视频解封装,将音视频流逐帧分成Packet放到相应的队列

// 解码操作
void NativePlayer::codec_internal() {int ret = 0;while (isPlaying) {// 判断队列是否已满if (audioChannel && audioChannel->packetQueue.size() > QUEUE_MAX_SIZE) {// 音频的包已经满了,线程睡10msav_usleep(THREAD_SLEEP_TIME);continue;}if (videoChannel && videoChannel->packetQueue.size() > QUEUE_MAX_SIZE) {// 视频的包已经满了,线程睡10msav_usleep(THREAD_SLEEP_TIME);continue;}// 读取数据包AVPacket *avPacket = av_packet_alloc();ret = av_read_frame(avFormatContext, avPacket);if (ret == 0) {// 将数据包放到队列中if (audioChannel && avPacket->stream_index == audioChannel->channelId) {audioChannel->packetQueue.push(avPacket);} else if (videoChannel && avPacket->stream_index == videoChannel->channelId) {videoChannel->packetQueue.push(avPacket);}} else if (ret == AVERROR_EOF) {// 读取完毕, 但不一定是播放完毕,只是将数据包全部放到队列中了if (videoChannel->packetQueue.empty() && videoChannel->frameQueue.empty()&& audioChannel->packetQueue.empty() && audioChannel->frameQueue.empty()) {LOGE("播放完毕...");break;}// 因为seek的存在,就算读取完毕,依然要循环下去, 执行av_read_frame(否则seek无效)} else {break;}}isPlaying = false;if (audioChannel) {audioChannel->stop();}if (videoChannel) {videoChannel->stop();}
}

视频Packet->视频Frame

// 将packet转成frame, 是packetQueue的消费者, 是frameQueue的生产者
void VideoChannel::decode_packet_internal() {// 子线程运行AVPacket *avPacket = 0;while (isPlaying) {int ret = packetQueue.pop(avPacket);if (!isPlaying) {break;}if (!ret) {continue;}ret = avcodec_send_packet(codecContext, avPacket);if (ret == AVERROR(EAGAIN)) {// 需要更多的数据continue;} else if (ret < 0) {// 失败break;}releaseAvPacket(avPacket);AVFrame *avFrame = av_frame_alloc();ret = avcodec_receive_frame(codecContext, avFrame);if (ret == AVERROR(EAGAIN)) {// 需要更多的数据continue;} else if (ret < 0) {// 失败break;}frameQueue.push(avFrame);while (frameQueue.size() > QUEUE_MAX_SIZE && isPlaying) {av_usleep(THREAD_SLEEP_TIME);}}releaseAvPacket(avPacket);
}

解码视频,Frame转成RGBA数组

// 在子线程循环, 从frameQueue中取数据,是frameQueue的消费者
void VideoChannel::play_internal() {// 初始化转换上下文, 将I420 的帧转成 rgba的帧SwsContext *sws_cxt = sws_getContext(codecContext->width, codecContext->height, codecContext->pix_fmt,codecContext->width, codecContext->height, AV_PIX_FMT_RGBA,SWS_BILINEAR, 0, 0, 0);// 4个通道,分别是RGBA// !!!! 这个数组不能声明成[1]  否则报错都看不懂!!!!uint8_t *dst_data[4];int dst_linesize[4];// 创建一个画布av_image_alloc(dst_data, dst_linesize,codecContext->width, codecContext->height, AV_PIX_FMT_RGBA, 1);AVFrame *avFrame = 0;while (isPlaying) {int ret = frameQueue.pop(avFrame);if (!isPlaying) {break;}if (!ret) {continue;}while (isPause) {av_usleep(THREAD_SLEEP_TIME);}// 执行转换sws_scale(sws_cxt,avFrame->data, avFrame->linesize, 0, avFrame->height,dst_data, dst_linesize);// 回调到native-lib中绘制到ANativeWindow中renderFrame(dst_data[0], dst_linesize[0], codecContext->width, codecContext->height);// 音视频同步处理// pts 是编码的时候赋值的, 需要加上解码的时间clock = avFrame->pts * av_q2d(time_base);// 解码时间, 因为配置差的手机 解码耗时多double extra_delay = avFrame->repeat_pict / (2 * fps);double frame_delay = 1.0 / fps;double audio_clock = audioChannel->clock;double diff = clock - audio_clock;double delay = extra_delay + frame_delay;
//        LOGE("----相差----%f", diff);if (clock > audio_clock) {// 视频超前if (diff > 1) {// 差太久 慢慢赶av_usleep((delay * 2) * 1000 * 1000);} else {av_usleep((delay + diff) * 1000 * 1000);}} else {// 音频超前if (diff > 1) {// 超太多,放弃同步} else if (diff >= 0.05) {// 视频需要追赶,丢帧// 删除frame队列的非关键帧// 减少睡眠时间的话会震荡frameQueue.sync();} else {// 视为同步了}}//        av_usleep(16 * 1000);releaseAvFrame(avFrame);}if (dst_data[0]) {av_freep(&dst_data[0]);}isPlaying = false;releaseAvFrame(avFrame);sws_freeContext(sws_cxt);}

将RGBA数据渲染到ANativeWindow

void renderFrame(uint8_t *data, int linesize, int width, int height) {pthread_mutex_lock(&mutex);// 将rgb的数据渲染到窗口// 设置窗口属性if (!window) {pthread_mutex_unlock(&mutex);ANativeWindow_release(window);return;}ANativeWindow_setBuffersGeometry(window, width, height, WINDOW_FORMAT_RGBA_8888);// 锁缓冲区if (ANativeWindow_lock(window, &buffer, 0)) {ANativeWindow_release(window);window = 0;pthread_mutex_unlock(&mutex);return;}// 输出到屏幕的输入源uint8_t *srcFirstLine = data;// 输出到屏幕的输入源的行数int srcStride = linesize;// 输出到屏幕时的跨度 是 缓冲区的跨度 * 4 , 因为一个pixel是4个字节int windowStride = buffer.stride * 4;// 屏幕的首行地址uint8_t *windowFirstLine = (uint8_t *) buffer.bits;for (int i = 0; i < buffer.height; ++i) {memcpy(windowFirstLine + i * windowStride, srcFirstLine + i * srcStride,windowStride);}// 解锁缓冲区ANativeWindow_unlockAndPost(window);pthread_mutex_unlock(&mutex);
}

音频OpenSL ES初始化

// 在子线程初始化OpenSLES
void AudioChannel::initOpenSLES_internal() {// 流程/*** 创建音频引擎* 设置混响器* 创建播放器* 设置缓存队列和回调函数* 设置播放状态* 启动回调函数*/// 音频引擎SLEngineItf engineInterface = NULL;// 音频对象SLObjectItf engineObject = NULL;// 混响器对象SLObjectItf outputMixObject = NULL;// 播放器对象SLObjectItf bqPlayerObject = NULL;// 回调接口SLPlayItf bqPlayerInterface = NULL;// 缓冲队列SLAndroidSimpleBufferQueueItf bqPlayerBufferQueue = NULL;// 初始化播放引擎SLresult result;result = slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL);if (SL_RESULT_SUCCESS != result) {return;}result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);if (SL_RESULT_SUCCESS != result) {return;}// 初始化音频接口result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineInterface);if (SL_RESULT_SUCCESS != result) {return;}//初始化播放引擎// 创建混响器result = (*engineInterface)->CreateOutputMix(engineInterface, &outputMixObject, 0, 0, 0);if (SL_RESULT_SUCCESS != result) {return;}// 初始化混响器/result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);if (SL_RESULT_SUCCESS != result) {return;}// 设置数据源的信息SLDataLocator_AndroidSimpleBufferQueue slDataLocatorAndroidSimpleBufferQueue= {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2};SLDataFormat_PCM pcm = {SL_DATAFORMAT_PCM, // 播放pcm格式的数据2, // 2个声道,立体声SL_SAMPLINGRATE_44_1, // 44100hz的频率SL_PCMSAMPLEFORMAT_FIXED_16, // 采样位数16位SL_PCMSAMPLEFORMAT_FIXED_16,SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT, // 立体声(前左前右)SL_BYTEORDER_LITTLEENDIAN // 小端模式};SLDataSource slDataSource = {&slDataLocatorAndroidSimpleBufferQueue, &pcm};SLDataLocator_OutputMix loc_outmix = {SL_DATALOCATOR_OUTPUTMIX, outputMixObject};SLDataSink audioSnk = {&loc_outmix, NULL};const SLInterfaceID ids[1] = {SL_IID_BUFFERQUEUE};const SLboolean req[1] = {SL_BOOLEAN_TRUE};// 创建播放器(*engineInterface)->CreateAudioPlayer(engineInterface,// 播放器接口&bqPlayerObject,// 播放器&slDataSource,// 播放器参数 播放缓冲队列 播放格式&audioSnk,// 播放缓冲区1,// 接口回调个数ids,// 设置播放队列idreq// 是否采用内置的缓冲区);// 初始化播放器(*bqPlayerObject)->Realize(bqPlayerObject, SL_BOOLEAN_FALSE);// 得到接口后调用 获取Player接口(*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_PLAY, &bqPlayerInterface);// 获得播放器接口(*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_BUFFERQUEUE, &bqPlayerBufferQueue);// 注册回调(*bqPlayerBufferQueue)->RegisterCallback(bqPlayerBufferQueue, bqPlayerCallback, this);// 设置播放状态--(*bqPlayerInterface)->SetPlayState(bqPlayerInterface, SL_PLAYSTATE_PLAYING);// 启动回调函数bqPlayerCallback(bqPlayerBufferQueue, this);
}

音频Packet->Frame

// 在子线程编码,将Packet编码成frame
void AudioChannel::codec_internal() {// 子线程运行AVPacket *avPacket = 0;while (isPlaying) {int ret = packetQueue.pop(avPacket);if (!isPlaying) {break;}if (!ret) {continue;}ret = avcodec_send_packet(codecContext, avPacket);if (ret == AVERROR(EAGAIN)) {// 需要更多的数据continue;} else if (ret < 0) {// 失败break;}releaseAvPacket(avPacket);AVFrame *avFrame = av_frame_alloc();ret = avcodec_receive_frame(codecContext, avFrame);if (ret == AVERROR(EAGAIN)) {// 需要更多的数据continue;} else if (ret < 0) {// 失败break;}frameQueue.push(avFrame);while (frameQueue.size() > QUEUE_MAX_SIZE && isPlaying) {av_usleep(THREAD_SLEEP_TIME);}}releaseAvPacket(avPacket);
}

音频frame数据转pcm源数据

// 将frame的数据放到buffer中
int AudioChannel::getBufferSize() {AVFrame *avFrame = 0;int out_buffer_size = 0;while (isPlaying) {int ret = frameQueue.pop(avFrame);if (!isPlaying) {break;}if (!ret) {continue;}while (isPause) {av_usleep(THREAD_SLEEP_TIME);}int64_t dst_nb_samples = av_rescale_rnd(swr_get_delay(swrContext, avFrame->sample_rate) + avFrame->nb_samples,out_sample_rate,avFrame->sample_rate,AV_ROUND_UP);// frame --> 统一格式int nb = swr_convert(swrContext, &out_buffer, dst_nb_samples,(const uint8_t **) avFrame->data,avFrame->nb_samples);// 获取当前帧的实际大小out_buffer_size = nb * out_channel_nb * out_samplesize;//av_samples_get_buffer_size(NULL, out_channel_nb, avFrame->nb_samples,out_sample,1);clock = avFrame->pts * av_q2d(time_base);break;}if (javaCallHelper) {javaCallHelper->onProgress(THREAD_CHILD, clock);}releaseAvFrame(avFrame);return out_buffer_size;
}

OpenSL ES 渲染音频

// 注册给bqPlayerBufferQueue的回调函数
void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void *context) {AudioChannel *audioChannel = static_cast<AudioChannel *>(context);int lenth = audioChannel->getBufferSize();if (lenth > 0) {(*bq)->Enqueue(bq, audioChannel->out_buffer, lenth);}
}

基于FFmpeg的音视频播放器相关推荐

  1. 基于Qt、FFMpeg的音视频播放器设计一

    前言:整个项目的源代码 https://download.csdn.net/download/hfuu1504011020/10672140 最近刚完成基于Qt.FFMpeg的音视频播放器相关C++程 ...

  2. 最简单的基于FFMPEG+SDL的视频播放器 ver2 (采用SDL2.0)

    ===================================================== 最简单的基于FFmpeg的视频播放器系列文章列表: 100行代码实现最简单的基于FFMPEG ...

  3. 最简单的基于FFMPEG+SDL的视频播放器:拆分-解码器和播放器

    ===================================================== 最简单的基于FFmpeg的视频播放器系列文章列表: 100行代码实现最简单的基于FFMPEG ...

  4. 100行代码实现最简单的基于FFMPEG+SDL的视频播放器(SDL1.x)

    ===================================================== 最简单的基于FFmpeg的视频播放器系列文章列表: 100行代码实现最简单的基于FFMPEG ...

  5. 基于electron的音视频播放器

    基于electron的音视频播放器 前言 选择做一个音视频播放器桌面应用程序原因 技术的选型 已经实现了的功能 音视频播放实现 右键菜单实现 总结 效果图 安装包下载: 最后如果大家觉得我这个音视频播 ...

  6. 《基于 FFmpeg + SDL 的视频播放器的制作》课程的视频

    这两天开始带广播电视工程大二的暑假小学期的课程设计了.本次小学期课程内容为<基于 FFmpeg + SDL 的视频播放器的制作>,其中主要讲述了视音频开发的入门知识.由于感觉本课程的内容不 ...

  7. 基于 FFmpeg SDL 的视频播放器的制作 课程的视频

    分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow 也欢迎大家转载本篇文章.分享知识,造福人民,实现我们中华民族伟大复兴! 这两天开 ...

  8. 基于 FFmpeg 的跨平台视频播放器简明教程(四):像素格式与格式转换

    系列文章目录 基于 FFmpeg 的跨平台视频播放器简明教程(一):FFMPEG + Conan 环境集成 基于 FFmpeg 的跨平台视频播放器简明教程(二):基础知识和解封装(demux) 基于 ...

  9. 100行代码实现最简单的基于FFMPEG+SDL的视频播放器

    简介 FFMPEG工程浩大,可以参考的书籍又不是很多,因此很多刚学习FFMPEG的人常常感觉到无从下手.我刚接触FFMPEG的时候也感觉不知从何学起. 因此我把自己做项目过程中实现的一个非常简单的视频 ...

最新文章

  1. PHP移动文件指针ftell()、fseek()、rewind()总结
  2. 肾炎治疗有效方(湿热壅滞三焦,气机不利)
  3. URL概念及与URL的区别
  4. leader选举的源码分析-FastLeaderElection.starter
  5. .net面试题大全,绝大部分面试题
  6. 研究生导师一般希望招什么样的研究生?
  7. python pip的配置
  8. 【Oracle AWR详解分析-02】
  9. java 发送邮件带附件
  10. tnsnames.ora配置未生效_一文了解网络交换机的6种命令配置模式
  11. python中rjust_Python字符串rjust()和ljust()
  12. 犀牛Rhinoceros 7 for Mac(三维建模软件)
  13. 如何使用Bartender标签打印软件批量打印构件二维码标签?
  14. COGS 2507 零食店
  15. 解决依赖包引入后重复问题Duplicate zip entry
  16. 【.7z 格式文件的压缩、解压】
  17. 【Windows Server 2019】路由服务的配置和管理
  18. 概念模型(conceptualDataModel)
  19. 【实战】python-docx---每页表格固定显示行数
  20. Linux内核在中国大发展的黄金十年-写于中国Linux存储、内存管理和文件系统峰会十周年之际...

热门文章

  1. Ngnix 的代码分析
  2. vue 如何下载文件(包含txt jpg pdf word)
  3. OATS-正交表测试策略
  4. IFS应用系统-面向服务的架构(SOA)
  5. fffffffffffffffffff
  6. Element-ui Popconfirm气泡确认框的确认及取消事件不生效
  7. Vant Icon 图标
  8. 走进音视频的世界——视频封装格式
  9. Bugku:杂项 猫片(安恒)
  10. 输出任意边长的菱形————C语言实践应用(1)(完整源码)