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
/Surface
/ AudioTrack
一起使用。请参考 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_code的nalu数据,即| 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