IJKPLAYER源码分析-mediacodec硬解

1 前言

    近期腾出了点时间,拟对IJKPLAYER做更完整的源码分析,并对关键实现细节,作为技术笔记,记录下来。包括Android端硬解码/AudioTrack/OpenSL播放,以及iOS端硬解码/AudioUnit播放,以及OpenGL渲染和Android/iOS端的图像显示技术,不一而足。

    本文着重分析Android端mediacodec硬解实现,由于audio用的是ffmpeg软解方案,因此本文只介绍用mediacodec硬解视频。

2 mediacodec简介

2.1 初识mediacodec

    mediacodec技术是Android系统音视频硬编/硬解的一套标准,各硬件厂商加以实现,常和MediaExtractor/MediaSync/MediaMuxer/MediaCrypto/MediaDrm/Image/SurfaceAudioTrack一起使用。请参考 MEDIACODEC开发文档

    由上图了解到,一言以蔽之,一端输入、一端输出,input输入的数据提交给codec异步处理后,由output输出再归还给codec。输入的是raw video/audio数据,则输出的是编码后的video/audio数据,输入的是编码后的video/audio数据,则输出的是raw的video/audio数据。

2.2 mediacodec状态机

    同步模式状态机:

     上图显示,Stopped状态包含三个子状态:Uninitialize/Configured/Error;Executing同样包含三个子状态:Flushed,/Running/End-of-Stream.

  • 对mediacodec的使用需遵守上图所示流程,否则会发生错误。
  • 以decode为例,当创建了mediacodec并且指定为解码后,进入Uninitialized状态,调用configure方法后,进入Configured状态,再调用start方法进入Executing状态。
  • 进入Executing状态后,首先到达Flush状态,此时mediacodec会持有所有的数据,当第一个inputbufffer从队列中取出时,立即进入Running状态,这个时间很短。然后就可以调用dequeInputBuffer和getInputBuffer来获取用户可用的缓冲区,用户填满数据后调用queueInputbuffer方法提交给解码器,解码器大部分时间都会工作在Running状态。当向inputBuffer中输入一帧标记EndOfStream的时候,进入End-of-Stream状态,此时,解码器不再接受任何新的数据输入,缓冲区中的数据和标记EndOfStream最终会执行完毕。在任何时候都可以调用flush方法回到Flush状态。
  • 调用stop方法会使mediacodec进入Uninitialized状态,这时候可以执行configure方法来进入下一循环。当mediacodec使用完毕后必须调用release方法来释放所有的资源。
  • 在某些情况下,如取出缓冲区索引时,mediacodec会发生错误进入Error状态,此时调用reset方法使得mediacodec重新处于Uninitialized状态,或者调用release来结束解码。

    异步模式状态机:

2.3 input

    此处以解码为例,一般的解码操作如下:

  • 通过queueInputBuffer提交给mediacodec解码器的数据,应当是1个完整帧;
  • 若不是完整帧,则解码后会马赛克、画面显示异常,或在API 26以后可通过queueInputBuffer()方法设置标志位BUFFER_FLAG_PARTIAL_FRAME告诉mediacodec是部分帧,由mediacodec组装成1个完整的帧再解码;

2.4 output 

  • 通过releaseOutputBuffer(codec, output_buffer_index, render)来控制此帧显示与否,并归还output_buffer_index的缓冲区;

3 硬解码

3.1 何为硬解

    所谓软解,是指使用CPU进行解码运算,GPU用以视频渲染加速,而硬解则是指利用移动端设备DSP芯片的解码能力进行解码。

    此外,必须说明的是,IJKPLAYER所支持的mediacodec硬解,是在native层以反射调用java层mediacodec的硬解码能力。

    为何没用NDK的mediacodec解码能力?原因或在于NDK的硬解能力是在API 21(Android 5.0)后才得以支持。

3.2 Android SDK硬解

    由上一节可知,IJKPLAYER是用反射在native层回调Android SDK的mediacodec解码能力。因此,下文主要以此为基础做介绍。

3.2.1 感性认识

     为了让各位对mediacodec有个感性认识,以下是我写的伪代码,大致描述了用jmediacodec解码播放的流程:

// use mediacodec api 伪代码
void decodeWithMediacodec() {
    // create h264 decoder
    MediaCodec codec = MediaCodec.createByCodecName("video/avc");

    // configure
    codec.configure(android_media_format, android_surface, crypto, flags);

    // start
    codec.start();

    // 持续给mediacodec解码器喂数据,解码与render
    while (1) {
        // dequeInputBuffer
        int input_buffer_index = codec.dequeInputBuffer(timeout);

        // write copy_size pixel data to input_buffer_index buffer
        ......

        // queueInputBuffer
        codec.queueInputBuffer(input_buffer_index, 0, copy_size, time_stamp, queue_flags)

        // dequeOutputBuffer
        int output_buffer_index = codec.dequeOutputBuffer(bufferInfo, timeout);

        // releaseOutputBuffer
        codec.releaseOutputBuffer(output_buffer_index, true);
    }

    // flush
    codec.flush()

    // stop / release
    codec.stop() or codec.release()
}

3.2.2 Annex-b格式

    在进入硬解流程之前,先来认识下annex-b格式的h26x码流:

  • 所谓的annex-b格式码流,是指用0x00000001或0x000001起始码分割的nalu单元,是一种h26x的一种码流组织方式;
  • 具体如:| start_code | nalu header | nalu | start_code | nalu header | nalu | ......
  • mediacodec解码所需的数据必须是带start_codenalu数据,即| start_code | nalu header | nalu
  • 喂给mediacodec解码器的每1帧,必须是带start_code前缀的,包括sps/pps/I/B/P帧;

 3.2.3 硬解流程

    上一节用伪代码描述了对mediacodec的简易使用,此处便可以正式介绍IJKPLAYER对mediacodec的使用了。归纳起来,对mediacodec的使用有如下步骤:

  • 首先,创建对应h26x/mpeg4等解码器,并完成对surface的配置;
  • 调用start()启动mediacodec解码;
  • 调用dequeInputBuffer取得empty input buffer index,完成数据的写入;
  • 调用queueInputBuffer()将写入的数据提交给mediacodec解码;
  • 调用dequeOutputBuffer()取得output buffer index;
  • 调用releaseOutputBuffer(codec, output_buffer_index, true)完成视频的绘制,归还output buffer给mediacodec;
    { "mediacodec",                             "MediaCodec: enable H264 (deprecated by 'mediacodec-avc')",
        OPTION_OFFSET(mediacodec_avc),          OPTION_INT(0, 0, 1) },

     缺省情况下,mediacodec关闭,使用ffmpeg软解。在业务侧使能了mediacodec选项后,创建mediacodec video decoder时机,是在read_thread线程里通过avformat_find_stream_info()接口拿到视频的参数之后:

read_thread() => stream_component_open() => ffpipeline_open_video_decoder() => func_open_video_decoder() => ffpipenode_create_video_decoder_from_android_mediacodec()

    此处根据业务侧所设置的选项,走ffmpeg软解,还是mediacodec硬解: 

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);
    if (!node) {
        node = ffpipenode_create_video_decoder_from_ffplay(ffp);
    }

    return node;
}

     在此完成mediacodec的解码器的创建,surface的配置,并启动解码器:

IJKFF_Pipenode *ffpipenode_create_video_decoder_from_android_mediacodec(FFPlayer *ffp, IJKFF_Pipeline *pipeline, SDL_Vout *vout)
{
    ALOGD("ffpipenode_create_video_decoder_from_android_mediacodec()\n");
    if (SDL_Android_GetApiLevel() < IJK_API_16_JELLY_BEAN)
        return NULL;

    if (!ffp || !ffp->is)
        return NULL;

    IJKFF_Pipenode *node = ffpipenode_alloc(sizeof(IJKFF_Pipenode_Opaque));
    if (!node)
        return node;

    VideoState            *is     = ffp->is;
    IJKFF_Pipenode_Opaque *opaque = node->opaque;
    JNIEnv                *env    = NULL;
    int                    ret    = 0;
    jobject                jsurface = NULL;

    node->func_destroy  = func_destroy;
    // mediacodec的同步或异步模式,并注册相应callback
    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;

    opaque->codecpar = avcodec_parameters_alloc();
    if (!opaque->codecpar)
        goto fail;

    ret = avcodec_parameters_from_context(opaque->codecpar, opaque->decoder->avctx);
    if (ret)
        goto fail;

    // 根据opaque->codecpar->codec_id取得profile和level以及java层创建decoder时所需的mime_type,如“video/avc”,此处略去相关代码
    ......

    if (JNI_OK != SDL_JNI_SetupThreadEnv(&env)) {
        ALOGE("%s:create: SetupThreadEnv failed\n", __func__);
        goto fail;
    }

    opaque->acodec_mutex                      = SDL_CreateMutex();
    opaque->acodec_cond                       = SDL_CreateCond();
    opaque->acodec_first_dequeue_output_mutex = SDL_CreateMutex();
    opaque->acodec_first_dequeue_output_cond  = SDL_CreateCond();
    opaque->any_input_mutex                   = SDL_CreateMutex();
    opaque->any_input_cond                    = SDL_CreateCond();

    if (!opaque->acodec_cond || !opaque->acodec_cond || !opaque->acodec_first_dequeue_output_mutex || !opaque->acodec_first_dequeue_output_cond) {
        ALOGE("%s:open_video_decoder: SDL_CreateCond() failed\n", __func__);
        goto fail;
    }

    // 创建inputFormat,并通过setBuffer将sps和pps设置给mediacodec
    ret = recreate_format_l(env, node);
    if (ret) {
        ALOGE("amc: recreate_format_l failed\n");
        goto fail;
    }

    if (!ffpipeline_select_mediacodec_l(pipeline, &opaque->mcc) || !op
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

老中医的博客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值