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;

如果上面没有主动设置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解码出来的视频帧入列之后,接下来就是进行同步和渲染的事情了。

猜你喜欢

转载自blog.csdn.net/achina2011jy/article/details/115895624