一、引言:
在上一篇博客中,将音频的解码和输出放在了一起分析,文章显得又长又冗杂,考虑到视频渲染及同步也是一个重点分析点,所以这篇博客仅分析视频解码相关的内容。因为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;
如果上面没有主动设置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,再入列即可。
四、从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);
...
}
看了一大圈,终于找到了最终copy的地方,将mediacodec解码出来的视频帧入列之后,接下来就是进行同步和渲染的事情了。