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

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↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部