基于FFmpeg的音视频播放器
版本信息
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的音视频播放器相关推荐
- 基于Qt、FFMpeg的音视频播放器设计一
前言:整个项目的源代码 https://download.csdn.net/download/hfuu1504011020/10672140 最近刚完成基于Qt.FFMpeg的音视频播放器相关C++程 ...
- 最简单的基于FFMPEG+SDL的视频播放器 ver2 (采用SDL2.0)
===================================================== 最简单的基于FFmpeg的视频播放器系列文章列表: 100行代码实现最简单的基于FFMPEG ...
- 最简单的基于FFMPEG+SDL的视频播放器:拆分-解码器和播放器
===================================================== 最简单的基于FFmpeg的视频播放器系列文章列表: 100行代码实现最简单的基于FFMPEG ...
- 100行代码实现最简单的基于FFMPEG+SDL的视频播放器(SDL1.x)
===================================================== 最简单的基于FFmpeg的视频播放器系列文章列表: 100行代码实现最简单的基于FFMPEG ...
- 基于electron的音视频播放器
基于electron的音视频播放器 前言 选择做一个音视频播放器桌面应用程序原因 技术的选型 已经实现了的功能 音视频播放实现 右键菜单实现 总结 效果图 安装包下载: 最后如果大家觉得我这个音视频播 ...
- 《基于 FFmpeg + SDL 的视频播放器的制作》课程的视频
这两天开始带广播电视工程大二的暑假小学期的课程设计了.本次小学期课程内容为<基于 FFmpeg + SDL 的视频播放器的制作>,其中主要讲述了视音频开发的入门知识.由于感觉本课程的内容不 ...
- 基于 FFmpeg SDL 的视频播放器的制作 课程的视频
分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow 也欢迎大家转载本篇文章.分享知识,造福人民,实现我们中华民族伟大复兴! 这两天开 ...
- 基于 FFmpeg 的跨平台视频播放器简明教程(四):像素格式与格式转换
系列文章目录 基于 FFmpeg 的跨平台视频播放器简明教程(一):FFMPEG + Conan 环境集成 基于 FFmpeg 的跨平台视频播放器简明教程(二):基础知识和解封装(demux) 基于 ...
- 100行代码实现最简单的基于FFMPEG+SDL的视频播放器
简介 FFMPEG工程浩大,可以参考的书籍又不是很多,因此很多刚学习FFMPEG的人常常感觉到无从下手.我刚接触FFMPEG的时候也感觉不知从何学起. 因此我把自己做项目过程中实现的一个非常简单的视频 ...
最新文章
- PHP移动文件指针ftell()、fseek()、rewind()总结
- 肾炎治疗有效方(湿热壅滞三焦,气机不利)
- URL概念及与URL的区别
- leader选举的源码分析-FastLeaderElection.starter
- .net面试题大全,绝大部分面试题
- 研究生导师一般希望招什么样的研究生?
- python pip的配置
- 【Oracle AWR详解分析-02】
- java 发送邮件带附件
- tnsnames.ora配置未生效_一文了解网络交换机的6种命令配置模式
- python中rjust_Python字符串rjust()和ljust()
- 犀牛Rhinoceros 7 for Mac(三维建模软件)
- 如何使用Bartender标签打印软件批量打印构件二维码标签?
- COGS 2507 零食店
- 解决依赖包引入后重复问题Duplicate zip entry
- 【.7z 格式文件的压缩、解压】
- 【Windows Server 2019】路由服务的配置和管理
- 概念模型(conceptualDataModel)
- 【实战】python-docx---每页表格固定显示行数
- Linux内核在中国大发展的黄金十年-写于中国Linux存储、内存管理和文件系统峰会十周年之际...