ijkplayer播放器剖析系列文章:

ijkplayer播放器剖析(一)从应用层分析至Jni层的流程分析

ijkplayer播放器剖析(二)消息机制分析

ijkplayer播放器剖析(三)音频解码与音频输出机制分析

一、引言:

在上一篇博客中,将音频的解码和输出放在了一起分析,文章显得又长又冗杂,考虑到视频渲染及同步也是一个重点分析点,所以这篇博客仅分析视频解码相关的内容。因为ijkplayer和FFmpeg在音频和视频的处理上有很多共用代码,并且在上一篇博客中讲解的足够详细,所以对于视频解码的分析就直接以重点代码来分析了。

二、MediaCodec解码通路分析:

先来看下视频解码相关的通路,ijkplayer有一个option叫“async-init-decoder”,可以通过上层apk设置到底层中。这个option的含义我不是特别清楚,一般情况下,是没有设置的。所以,在ijkplayer的地方读取该值时为0,即ffp->async_init_decoder。

下面看一下ijkplayer创建vdec的地方:

stream_component_open@ijkmedia\ijkplayer\ff_ffplay.c:case AVMEDIA_TYPE_VIDEO:is->video_stream = stream_index;is->video_st = ic->streams[stream_index];/* 通常默认为0,走下面的else */if (ffp->async_init_decoder) {...} else {/* 初始化 */decoder_init(&is->viddec, avctx, &is->videoq, is->continue_read_thread);/* 打开vdec */ffp->node_vdec = ffpipeline_open_video_decoder(ffp->pipeline, ffp);if (!ffp->node_vdec)goto fail;}/* 开启解码 */if ((ret = decoder_start(&is->viddec, video_thread, ffp, "ff_video_dec")) < 0)goto out;is->queue_attachments_req = 1;/* 这里是关于帧率设置的判断,略过 */...break;

本文福利, 免费领取C++音视频学习资料包、技术视频,内容包括(音视频开发,面试题,FFmpeg webRTC rtmp hls rtsp ffplay srs↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓

如果上面没有主动设置option下来的话,代码中就会走到else中去,首先是decoder_init,最重要的操作是将packet的queue绑定到解码器中。接下来看下
ffpipeline_open_video_decoder

IJKFF_Pipenode* ffpipeline_open_video_decoder(IJKFF_Pipeline *pipeline, FFPlayer *ffp)
{return pipeline->func_open_video_decoder(pipeline, ffp);
}

pipeline的创建如下:

ijkmp_android_create@ijkmedia\ijkplayer\android\ijkplayer_android.c:IjkMediaPlayer *ijkmp_android_create(int(*msg_loop)(void*))
{...mp->ffplayer->pipeline = ffpipeline_create_from_android(mp->ffplayer);if (!mp->ffplayer->pipeline)goto fail;...
}

找到函数指针的指向:

pipeline->func_open_video_decoder   = func_open_video_decoder;
func_open_video_decoder@ijkmedia\ijkplayer\android\ijkplayer_android.c:static IJKFF_Pipenode *func_open_video_decoder(IJKFF_Pipeline *pipeline, FFPlayer *ffp)
{IJKFF_Pipeline_Opaque *opaque = pipeline->opaque;IJKFF_Pipenode        *node = NULL;/* 走Mediacodec的硬解码 */if (ffp->mediacodec_all_videos || ffp->mediacodec_avc || ffp->mediacodec_hevc || ffp->mediacodec_mpeg2)node = ffpipenode_create_video_decoder_from_android_mediacodec(ffp, pipeline, opaque->weak_vout);/* 走FFmpeg的软解 */if (!node) {node = ffpipenode_create_video_decoder_from_ffplay(ffp);}return node;
}

可以看到,代码中可以使用mediacodec或者FFmpeg进行视频解码,当然,选择mediacodec解码是需要if中的判断条件来让上层进行设置的:

@ijkmedia\ijkplayer\ff_ffplay_options.h:// Android only options{ "mediacodec",                             "MediaCodec: enable H264 (deprecated by 'mediacodec-avc')",OPTION_OFFSET(mediacodec_avc),          OPTION_INT(0, 0, 1) },{ "mediacodec-all-videos",                  "MediaCodec: enable all videos",OPTION_OFFSET(mediacodec_all_videos),   OPTION_INT(0, 0, 1) },{ "mediacodec-avc",                         "MediaCodec: enable H264",OPTION_OFFSET(mediacodec_avc),          OPTION_INT(0, 0, 1) },{ "mediacodec-hevc",                        "MediaCodec: enable HEVC", OPTION_OFFSET(mediacodec_hevc),         OPTION_INT(0, 0, 1) },

我们只研究硬解码ffpipenode_create_video_decoder_from_android_mediacodec

IJKFF_Pipenode *ffpipenode_create_video_decoder_from_android_mediacodec(FFPlayer *ffp, IJKFF_Pipeline *pipeline, SDL_Vout *vout)
{...IJKFF_Pipenode *node = ffpipenode_alloc(sizeof(IJKFF_Pipenode_Opaque));if (!node)return node;  ...node->func_destroy  = func_destroy;/* 上层没有设置这个option将走else分支 */if (ffp->mediacodec_sync) {node->func_run_sync = func_run_sync_loop;} else {node->func_run_sync = func_run_sync;}node->func_flush    = func_flush;opaque->pipeline    = pipeline;opaque->ffp         = ffp;opaque->decoder     = &is->viddec;opaque->weak_vout   = vout;    ...
}

把重要的函数指针指向都确认好了之后,接下来我们就要去看vdec的解码线程了。看下decoder_start的入参,即解码线程video_thread

static int video_thread(void *arg)
{FFPlayer *ffp = (FFPlayer *)arg;int       ret = 0;if (ffp->node_vdec) {ret = ffpipenode_run_sync(ffp->node_vdec);}return ret;
}

之所以前面花了大篇幅去分析node_vdec,就是为了确认ffpipenode_run_sync

int ffpipenode_run_sync(IJKFF_Pipenode *node)
{return node->func_run_sync(node);
}

node->func_run_sync指向的是func_run_sync

static int func_run_sync(IJKFF_Pipenode *node)
{.../* 找到mediacodec的解码器之后不会进入这个if */if (!opaque->acodec) {return ffp_video_thread(ffp);}...frame = av_frame_alloc();if (!frame)goto fail;/* 1.创建填充数据的线程 */opaque->enqueue_thread = SDL_CreateThreadEx(&opaque->_enqueue_thread, enqueue_thread_func, node, "amediacodec_input_thread");if (!opaque->enqueue_thread) {ALOGE("%s: SDL_CreateThreadEx failed\n", __func__);ret = -1;goto fail;}while (!q->abort_request) {...got_frame = 0;/* 2.获取outputbuffer */ret = drain_output_buffer(env, node, timeUs, &dequeue_count, frame, &got_frame);...if (got_frame) {/* 3.将output picture入队列 */ret = ffp_queue_picture(ffp, frame, pts, duration, av_frame_get_pkt_pos(frame), is->viddec.pkt_serial);if (ret) {if (frame->opaque)SDL_VoutAndroid_releaseBufferProxyP(opaque->weak_vout, (SDL_AMediaCodecBufferProxy **)&frame->opaque, false);}av_frame_unref(frame);         }}
}

三、往MediaCodec中填充数据:

如果熟悉Android MediaCodec的操作流程,就能够看出来上面这个函数浓缩了整个操作。首先,ijkplayer专门创建了一个线程enqueue_thread_func往mediacodec中填充数据:

static int enqueue_thread_func(void *arg)
{....while (!q->abort_request && !opaque->abort) {ret = feed_input_buffer(env, node, AMC_INPUT_TIMEOUT_US, &dequeue_count);if (ret != 0) {goto fail;}}...
}

如果buffer队列没有停止接收数据的话,那么就会一直调用feed_input_buffer函数:

static int feed_input_buffer(JNIEnv *env, IJKFF_Pipenode *node, int64_t timeUs, int *enqueue_count)
{.../* 从mediacodec出列一个inputbuff的index */input_buffer_index = SDL_AMediaCodec_dequeueInputBuffer(opaque->acodec, timeUs);.../* 将packet中的待解码数据写入到mediacodec中 */copy_size = SDL_AMediaCodec_writeInputData(opaque->acodec, input_buffer_index, d->pkt_temp.data, d->pkt_temp.size);.../* 数据写完之后将inbuffer入列等待解码 */amc_ret = SDL_AMediaCodec_queueInputBuffer(opaque->acodec, input_buffer_index, 0, copy_size, time_stamp, queue_flags);
}

有兴趣的同学可以去看JNI如何反射到java层的,这个函数的主要作用就是从mediacodec中出列可用的inputbuffer,然后将packet中的源数据写入到mediacodec,再入列即可。

本文福利, 免费领取C++音视频学习资料包、技术视频,内容包括(音视频开发,面试题,FFmpeg webRTC rtmp hls rtsp ffplay srs↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓

四、从MediaCodec中取出数据:

接下来看一下ijkplayer是如何处理mediacodec解码完后的数据的。

回到func_run_sync,先看while循环中的drain_output_buffer:

static int drain_output_buffer(JNIEnv *env, IJKFF_Pipenode *node, int64_t timeUs, int *dequeue_count, AVFrame *frame, int *got_frame)
{...int ret = drain_output_buffer_l(env, node, timeUs, dequeue_count, frame, got_frame);...
}
static int drain_output_buffer_l(JNIEnv *env, IJKFF_Pipenode *node, int64_t timeUs, int *dequeue_count, AVFrame *frame, int *got_frame)
{.../* 从mediacodec的buffer队列中出列可用的buffer index */output_buffer_index = SDL_AMediaCodecFake_dequeueOutputBuffer(opaque->acodec, &bufferInfo, timeUs);if (output_buffer_index == AMEDIACODEC__INFO_OUTPUT_BUFFERS_CHANGED) {ALOGI("AMEDIACODEC__INFO_OUTPUT_BUFFERS_CHANGED\n");// continue;}...else if (output_buffer_index >= 0){...if (opaque->n_buf_out){...}/* 进入else分支进行数据的copy */else{ret = amc_fill_frame(node, frame, got_frame, output_buffer_index, SDL_AMediaCodec_getSerial(opaque->acodec), &bufferInfo);}}
}

这个函数很长,从表面看也仅仅是通过调用mediacodec拿到可以使用的outputbuffer的可用index,还需要找到buffer才能去进行copy操作。看一下amc_fill_frame函数中:

static int amc_fill_frame(IJKFF_Pipenode            *node,AVFrame                   *frame,int                       *got_frame,int                        output_buffer_index,int                        acodec_serial,SDL_AMediaCodecBufferInfo *buffer_info)
{IJKFF_Pipenode_Opaque *opaque     = node->opaque;FFPlayer              *ffp        = opaque->ffp;VideoState            *is         = ffp->is;/* 搞了一个代理 */frame->opaque = SDL_VoutAndroid_obtainBufferProxy(opaque->weak_vout, acodec_serial, output_buffer_index, buffer_info);if (!frame->opaque)goto fail;frame->width  = opaque->frame_width;frame->height = opaque->frame_height;frame->format = IJK_AV_PIX_FMT__ANDROID_MEDIACODEC;frame->sample_aspect_ratio = opaque->codecpar->sample_aspect_ratio;frame->pts    = av_rescale_q(buffer_info->presentationTimeUs, AV_TIME_BASE_Q, is->video_st->time_base);if (frame->pts < 0)frame->pts = AV_NOPTS_VALUE;// ALOGE("%s: %f", __func__, (float)frame->pts);*got_frame = 1;return 0;
fail:*got_frame = 0;return -1;
}

这个地方需要注意,ijkplayer搞了一个代理来进行数据操作,但是往下一直追踪代码并没有发现有拷贝的地方,仅仅是将buffer_index进行了一个赋值:

proxy->buffer_index  = buffer_index;

那么说明,拷贝操作将在后面进行。
继续回到外层的func_run_sync函数,看一下ffp_queue_picture

int ffp_queue_picture(FFPlayer *ffp, AVFrame *src_frame, double pts, double duration, int64_t pos, int serial)
{return queue_picture(ffp, src_frame, pts, duration, pos, serial);
}

queue_picture这个函数就在ff_ffplay.c,这个函数ijkplayer也写的很复杂,只抓重点如下:

static int queue_picture(FFPlayer *ffp, AVFrame *src_frame, double pts, double duration, int64_t pos, int serial)
{.../* 出队列一帧可写的frame等待填充 */if (!(vp = frame_queue_peek_writable(&is->pictq)))return -1;.../* 进行数据拷贝:src_frame->vp->bmp */if (SDL_VoutFillFrameYUVOverlay(vp->bmp, src_frame) < 0) {av_log(NULL, AV_LOG_FATAL, "Cannot initialize the conversion context\n");exit(1);}.../* 将picture推入队列 */frame_queue_push(&is->pictq);...
}

将mediacodec解码出来的视频帧入列之后,接下来就是进行同步和渲染的事情了。

如果你对音视频开发感兴趣,觉得文章对您有帮助,别忘了点赞、收藏哦!或者对本文的一些阐述有自己的看法,有任何问题,欢迎在下方评论区讨论!

本文福利, 免费领取C++音视频学习资料包、技术视频,内容包括(音视频开发,面试题,FFmpeg webRTC rtmp hls rtsp ffplay srs↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓

ijkplayer播放器剖析(四)音频解码与音频输出机制分析相关推荐

  1. ijkplayer播放器剖析(一)让ijkplayer播起来

    一.引言: ijkplayer是一款对FFmpeg封装非常好的第三方开源播放器,遗憾的是,ijkplayer2.0似乎不开源,并且1.0版本更新也基本停止了,很多公司都会采用ijkplayer作为其播 ...

  2. ijkplayer播放器剖析(三)音频解码与音频输出机制分析

    一.引言: 在前面的博客中,我们对ijkplayer整个jni的流程及消息机制都详细的分析了一遍,分析流程机制有助于我们对整个架构有一个大致的了解,便于后续对音视频解码与输出渲染的分析,消息机制的分析 ...

  3. ijkplayer播放器剖析(六)视频同步与渲染机制分析

    一.引言: 在前面的博客中,将音频解码播放及视频解码都分析了,这篇博客将单独针对视频同步及渲染来分析,看下ijkplayer是如何做的.本博客分析的同步方式为以音频为主,视频去同步音频. 二.同步前提 ...

  4. ijkplayer播放器剖析(五)视频解码线程分析

    一.引言: 在上一篇博客中,将音频的解码和输出放在了一起分析,文章显得又长又冗杂,考虑到视频渲染及同步也是一个重点分析点,所以这篇博客仅分析视频解码相关的内容.因为ijkplayer和FFmpeg在音 ...

  5. 一步步实现windows版ijkplayer系列文章之三——Ijkplayer播放器源码分析之音视频输出——音频篇

    https://www.cnblogs.com/harlanc/p/9693983.html 目录 OpenSL ES & AudioTrack 源码分析 创建播放器音频输出对象 配置并创建音 ...

  6. ExoPlayer播放器剖析(六)ExoPlayer同步机制分析

    关联博客 ExoPlayer播放器剖析(一)进入ExoPlayer的世界 ExoPlayer播放器剖析(二)编写exoplayer的demo ExoPlayer播放器剖析(三)流程分析-从build到 ...

  7. ExoPlayer播放器剖析(五)ExoPlayer对AudioTrack的操作

    关联博客 ExoPlayer播放器剖析(一)进入ExoPlayer的世界 ExoPlayer播放器剖析(二)编写exoplayer的demo ExoPlayer播放器剖析(三)流程分析-从build到 ...

  8. 一步步实现windows版ijkplayer系列文章之二——Ijkplayer播放器源码分析之音视频输出——视频篇...

    一步步实现windows版ijkplayer系列文章之一--Windows10平台编译ffmpeg 4.0.2,生成ffplay 一步步实现windows版ijkplayer系列文章之二--Ijkpl ...

  9. IjkPlayer播放器秒开优化以及常用Option设置

    IjkPlayer播放器秒开优化以及常用Option设置 96 GexYY 关注 1.5 2018.04.19 13:28* 字数 592 阅读 10797评论 9喜欢 32 ijkplayer和ff ...

最新文章

  1. 图解Redis事务机制
  2. arcgis和matlab,ArcGIS和MATLAB应用并不困难
  3. 29. Leetcode 19. 删除链表的倒数第 N 个结点 (链表-双指针)
  4. GNU make manual 翻译( 一百零九)
  5. 《大数据原理:复杂信息的准备、共享和分析》一一2.5 在标识符中嵌入信息:不推荐...
  6. 厉害了,我的清华大学,各系横幅让网友看花眼,尤其是第3条
  7. undolog 是binlog_mysql日志redo log、undo log、binlog以及作用看这篇就可以啦
  8. java:Map借口及其子类HashMap五,identityHashMap子类
  9. JavaScript 01
  10. 自然语言处理(NLP)与自然语言理解(NLU)的区别
  11. 数学建模5 代码论文降重 Excel表处理数据
  12. 将子节点中含子节点的json数据转换成ztree适合的json数据格式
  13. vue 关于飞行地图展示的功能
  14. 2021年苹果ASO商店优化技巧
  15. android+widget日历开发,安卓日历小部件源码(AppWidgetProvider)
  16. 《京东话费充值系统架构演进实践》读后感
  17. 《计算机网络参考模型》
  18. 解锁网易云音乐小工具_什么?网易云音乐又变灰了
  19. WebStorm破解补丁激活
  20. 正则表达式 - Python 正则表达式 学习笔记 最全整理

热门文章

  1. java开发实习面试
  2. 【Axure视频教程】中继器表格——自定义显示列表
  3. vivoX50Pro和vivoX50Pro+区别
  4. 嵌入式入门-交叉编译、bootloader、kernel、根文件系统关系
  5. Android 滚动的公告栏
  6. 树莓派zero w安装linux,树莓派 Zero W 的USB/以太网应用一例
  7. 逻辑面试题:叫你戴帽子
  8. “扫黄打非”办:集中整治自媒体炒作敏感问题等行为
  9. Linux升级ssh服务
  10. 难得和你相遇,用来生气多可惜