Android NDK MediaCodec在ijkplayer中的实践

临文乍了了,彻卷兀若无。这篇文章主要讲述Android NDK MediaCodec在ijkplayer中的实践相关的知识,希望能为你提供帮助。
https://www.jianshu.com/p/41d3147a5e07
从API 21(android 5.0)开始Android提供C层的NDK MediaCodec的接口。
【Android NDK MediaCodec在ijkplayer中的实践】java MediaCodec是对NDK MediaCodec的封装,ijkplayer硬解通路一直使用的是Java MediaCodecSurface的方式。
本文的主要内容是:在ijkplayer框架内适配NDK MediaCodec,不再使用Surface输出,改用YUV输出达到软硬解通路一致的渲染流程。

下文提到的Java MediaCodec,如果不做特别说明,都指的Surface 输出。
下文提到的NDK MediaCodec,如果不做特别说明,都指的YUV 输出。
1. ijkplayer硬解码的过程
在增加NDK MediaCodec硬解流程之前,先简要说明Java MediaCodec的流程:

 
Android NDK MediaCodec在ijkplayer中的实践

文章图片
Android Java MediaCodec 
图中主要有三个步骤:AVPacket-> Decode-> AVFrame;
  1. read线程读到packet,放入packet queue
  2. 解码得到一帧AVFrame,放入picture queue
  3. picture queue取出一帧,渲染AVFrame(overlay)
数据来源AVPacket不变,目标AVFrame不变,现在我们将步骤2 Decode中的Java Mediacodec替换成 Ndk Mediacodec ,其他地方都不需要改动。
但是有一点需要注意:我们从NDK MediaCodec得到的YUV数据,并不是像Java Mediacodec得到的是一个index,所以NDK MediaCodec解码后渲染部分和软解流程一样,都是基于OpenGL
1.1 打开视频流在stream_component_open()函数打开解码器,以及创建解码线程:
//ff_ffplayer.c static int stream_component_open(FFPlayer *ffp, int stream_index) { ...... codec = avcodec_find_decoder(avctx-> codec_id); ...... if ((ret = avcodec_open2(avctx, codec, & opts)) < 0) { goto fail; } ...... case AVMEDIA_TYPE_VIDEO: ...... decoder_init(& is-> viddec, avctx, & is-> videoq, is-> continue_read_thread); 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; ...... }

FFmpeg软解码器默认打开,接着由IJKFF_Pipeline(ios/Android),创建ffpipeline_open_video_decoder硬解解码器结构体IJKFF_Pipenode
1.2 创建解码器ffpipeline_open_video_decoder()会根据设置创建硬解码器或软解码器IJKFF_Pipenode
//ffpipeline_android.c static IJKFF_Pipenode *func_open_video_decoder(IJKFF_Pipeline *pipeline, FFPlayer *ffp) { IJKFF_Pipeline_Opaque *opaque = pipeline-> opaque; IJKFF_Pipenode*node = NULL; 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; }

硬解码器创建失败会切到软解码器。
1.3 启动解码线程启动解码线程decoder_start()
//ff_ffplayer.c int ffpipenode_run_sync(IJKFF_Pipenode *node) { return node-> func_run_sync(node); }

IJKFF_Pipenode会根据func_run_sync函数指针,具体启动软解还是硬解线程。
1.4 解码线程工作
//ffpipenode_android_mediacodec_vdec.c static int func_run_sync(IJKFF_Pipenode *node) { ... opaque-> enqueue_thread = SDL_CreateThreadEx(& opaque-> _enqueue_thread, enqueue_thread_func, node, "amediacodec_input_thread"); ... while (!q-> abort_request) { ... ret = drain_output_buffer(env, node, timeUs, & dequeue_count, frame, & got_frame); ... ret = ffp_queue_picture(ffp, frame, pts, duration, av_frame_get_pkt_pos(frame), is-> viddec.pkt_serial); ... } }

  1. 可以看到解码线程又创建了子线程,enqueue_thread_func()主要是用来将压缩数据(H.264/H.265)放入解码器,这样往解码器放数据在enqueue_thread_func()里面,从解码器取数据在func_run_sync()里面;
  2. drain_output_buffer()从解码器取出一个AVFrame,但是这个AVFrame-> dataNULL并没有数据,其中AVFrame-> opaque指针指向一个SDL_AMediaCodecBufferProxy结构体:
struct SDL_AMediaCodecBufferProxy { int buffer_id; int buffer_index; int acodec_serial; SDL_AMediaCodecBufferInfo buffer_info; };

这些成员由硬解器SDL_AMediaCodecFake_dequeueOutputBuffer得来,它们在视频渲染的时候会用到;
  1. 将AVFrame放入待渲染队列。
2. 增加NDK MediaCodec解码
根据上面的解码流程,增加NDK MediaCodec就只需2个关键步骤:
  1. 创建IJKFF_Pipenode;
  2. 创建相应的解码线程。
2.1 新建pipenodeNDK MediaCodec创建一个IJKFF_Pipenode。在func_open_video_decoder()打开解码器时,软件解码器和Java Mediacodec都需要创建一个IJKFF_Pipenode,其中IJKFF_Pipenode-> opaque为自定义的解码结构体指针,所以定义一个IJKFF_Pipenode_Ndk_MediaCodec_Opaque结构体。
//ffpipenode_android_ndk_mediacodec_vdec.c typedef struct IJKFF_Pipenode_Ndk_MediaCodec_Opaque { FFPlayer*ffp; IJKFF_Pipeline*pipeline; Decoder*decoder; SDL_Vout*weak_vout; SDL_Thread_enqueue_thread; SDL_Thread*enqueue_thread; ijkmp_mediacodecinfo_context mcc; characodec_name[128]; intframe_width; intframe_height; intframe_rotate_degrees; AVCodecContext*avctx; // not own AVBitStreamFilterContext *bsfc; // own size_tnal_size; AMediaFormat *ndk_format; AMediaCodec*ndk_codec; } IJKFF_Pipenode_Ndk_MediaCodec_Opaque;

里面有两个比较重要的成员AMediaFormatAMediaCodec,他们就是native层的编解码器和媒体格式。定义函数ffpipenode_create_video_decoder_from_android_ndk_mediacodec()创建IJKFF_Pipenode
//ffpipenode_android_ndk_mediacodec_vdec.c IJKFF_Pipenode *ffpipenode_create_video_decoder_from_android_ndk_mediacodec(FFPlayer *ffp, IJKFF_Pipeline *pipeline, SDL_Vout *vout) { if (SDL_Android_GetApiLevel() < IJK_API_21_LOLLIPOP) return NULL; IJKFF_Pipenode *node = ffpipenode_alloc(sizeof(IJKFF_Pipenode_Ndk_MediaCodec_Opaque)); if (!node) return node; ... IJKFF_Pipenode_Ndk_MediaCodec_Opaque *opaque = node-> opaque; node-> func_destroy= func_destroy; node-> func_run_sync = func_run_sync; opaque-> ndk_format = AMediaFormat_new(); ... AMediaFormat_setString(opaque-> ndk_format , AMEDIAFORMAT_KEY_MIME, opaque-> mcc.mime_type); AMediaFormat_setBuffer(opaque-> ndk_format , "csd-0", convert_buffer, sps_pps_size); AMediaFormat_setInt32(opaque-> ndk_format , AMEDIAFORMAT_KEY_WIDTH, opaque-> avctx-> width); AMediaFormat_setInt32(opaque-> ndk_format , AMEDIAFORMAT_KEY_HEIGHT, opaque-> avctx-> height); AMediaFormat_setInt32(opaque-> ndk_format , AMEDIAFORMAT_KEY_COLOR_FORMAT, 19); opaque-> ndk_codec = AMediaCodec_createDecoderByType(opaque-> mcc.mime_type); if (AMediaCodec_configure(opaque-> ndk_codec, opaque-> ndk_format, NULL, NULL, 0) != AMEDIA_OK) goto fail; return node; fail: ffpipenode_free_p(& node); return NULL; }

NDK MediaCodec的接口和Java MediaCodec的接口是一样的 。然后打开解码器就可以改为:
//ffpipeline_android.c static IJKFF_Pipenode *func_open_video_decoder(IJKFF_Pipeline *pipeline, FFPlayer *ffp) { IJKFF_Pipeline_Opaque *opaque = pipeline-> opaque; IJKFF_Pipenode*node = NULL; if (ffp-> mediacodec_all_videos || ffp-> mediacodec_avc || ffp-> mediacodec_hevc || ffp-> mediacodec_mpeg2) node = ffpipenode_create_video_decoder_from_android_ndk_mediacodec(ffp, pipeline, opaque-> weak_vout); if (!node) { node = ffpipenode_create_video_decoder_from_ffplay(ffp); }return node; }

2.2 创建解码线程func_run_syncfunc_run_sync()也会再创建一个子线程enqueue_thread_func(),用于往解码器放数据:
//ffpipenode_android_ndk_mediacodec_vdec.c static int func_run_sync(IJKFF_Pipenode *node) { ... AMediaCodec_start(c); opaque-> enqueue_thread = SDL_CreateThreadEx(& opaque-> _enqueue_thread, enqueue_thread_func, node, "amediacodec_input_thread"); AVFrame* frame = av_frame_alloc(); AMediaCodecBufferInfo info; ... while (!q-> abort_request) { outbufidx = AMediaCodec_dequeueOutputBuffer(c, & info, AMC_OUTPUT_TIMEOUT_US); if (outbufidx > = 0) { size_t size; uint8_t* buffer = AMediaCodec_getOutputBuffer(c, outbufidx, & size); if (size) { int num; AMediaFormat *format = AMediaCodec_getOutputFormat(c); AMediaFormat_getInt32(format, AMEDIAFORMAT_KEY_COLOR_FORMAT, & num) ; if (num == 19)//YUV420P { frame-> width = opaque-> avctx-> width; frame-> height = opaque-> avctx-> height; frame-> format = AV_PIX_FMT_YUV420P; frame-> sample_aspect_ratio = opaque-> avctx-> sample_aspect_ratio; frame-> pts = info.presentationTimeUs; double frame_pts = frame-> pts*av_q2d(AV_TIME_BASE_Q); double duration = (frame_rate.num & & frame_rate.den ? av_q2d((AVRational){frame_rate.den, frame_rate.num}) : 0); av_frame_get_buffer(frame, 1); memcpy(frame-> data[0], buffer, frame-> width*frame-> height); memcpy(frame-> data[1], buffer+frame-> width*frame-> height, frame-> width*frame-> height/4); memcpy(frame-> data[2], buffer+frame-> width*frame-> height*5/4, frame-> width*frame-> height/4); ffp_queue_picture(ffp, frame, frame_pts, duration, av_frame_get_pkt_pos(frame), is-> viddec.pkt_serial); av_frame_unref(frame); } else if (num == 21)// YUV420SP { } } AMediaCodec_releaseOutputBuffer(c,outbufidx, false); } else { switch (outbufidx) { case AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED: { AMediaFormat *format = AMediaCodec_getOutputFormat(c); int pix_format = -1; int width =0, height =0; AMediaFormat_getInt32(format, AMEDIAFORMAT_KEY_WIDTH, & width); AMediaFormat_getInt32(format, AMEDIAFORMAT_KEY_HEIGHT, & height); AMediaFormat_getInt32(format, AMEDIAFORMAT_KEY_COLOR_FORMAT, & pix_format); break; } case AMEDIACODEC_INFO_OUTPUT_BUFFERS_CHANGED: break; case AMEDIACODEC_INFO_TRY_AGAIN_LATER: break; default: break; } } }fail: av_frame_free(& frame); SDL_WaitThread(opaque-> enqueue_thread, NULL); ALOGI("MediaCodec: %s: exit: %d", __func__, ret); return ret; }

  1. 从解码器拿到解码后的数据buffer;
  2. 填充AVFrame结构体,申请相应大小的内存,由于我们设置解码器的输出格式是YUV420P,所以frame-> format = AV_PIX_FMT_YUV420P,然后将buffer拷贝到frame-> data;
  3. 放入待渲染队列ffp_queue_picture,至此渲染线程就能像软解一样取到AVFrame
//ffpipenode_android_ndk_mediacodec_vdec.c static int enqueue_thread_func(void *arg) { ... while (!q-> abort_request) { do { ... if (ffp_packet_queue_get_or_buffering(ffp, d-> queue, & pkt, & d-> pkt_serial, & d-> finished) < 0) { ret = -1; goto fail; } }while(ffp_is_flush_packet(& pkt) || d-> queue-> serial != d-> pkt_serial); if (opaque-> avctx-> codec_id == AV_CODEC_ID_H264 || opaque-> avctx-> codec_id == AV_CODEC_ID_HEVC) { convert_h264_to_annexb(pkt.data, pkt.size, opaque-> nal_size, & convert_state); ... }ssize_t id = AMediaCodec_dequeueInputBuffer(c, AMC_INPUT_TIMEOUT_US); if (id > = 0) { uint8_t *buf = AMediaCodec_getInputBuffer(c, (size_t) id, & size); if (buf != NULL & & size > = pkt.size) { memcpy(buf, pkt.data, (size_t)pkt.size); media_status = AMediaCodec_queueInputBuffer(c, (size_t) id, 0, (size_t) pkt.size, (uint64_t) time_stamp, keyframe_flag); if (media_status != AMEDIA_OK) { goto fail; } } } av_packet_unref(& pkt); } fail: return 0; }

往解码器放数据在enqueue_thread_func()线程里面,解码的整体流程和Java MediaCodec一样
2.3 其他需要修改的地方修改Android.mk
LOCAL_LDLIBS += -llog -landroid -lmediandk LOCAL_SRC_FILES += android/pipeline/ffpipenode_android_ndk_mediacodec_vdec.c

如果提示media/NdkMediaCodec.h找不到,可能是因为API级别< 21,修改Application.mk:
APP_PLATFORM := android-21

3. 性能分析
测试情况使用的设备为Oppo R11 Plus(Android 7.1.1),测试序列H. 264 (1920x1080 25fps)视频,Java MediaCodecNDK MediaCodec解码时CPU及GPU的表现:
Java MediaCodec CPU 占用大约在5%左右 
Android NDK MediaCodec在ijkplayer中的实践

文章图片
Java MediaCodec解码CPU表现NDK MediaCodec CPU占用大约在12%左右 
Android NDK MediaCodec在ijkplayer中的实践

文章图片
NDK MediaCodec解码CPU表现Java MediaCodec GPU占用表现
 
Android NDK MediaCodec在ijkplayer中的实践

文章图片
Java MediaCodec解码GPU表现NDK MediaCodec GPU占用表现 
Android NDK MediaCodec在ijkplayer中的实践

文章图片
NDK MediaCodec解码GPU表现3.1 测试数据分析NDK MediaCodecCPU占比大约高出7%,但是GPU表现较好。
CPU为什么会比Java MediaCodec解码时高呢?
我们这里一直评估的Java MediaCodec,都指的Surface输出。这意味着接口内部完成了解码和渲染工作,高度封装的解码和渲染,内部做了一些数据传递优化的工作。同时ijkplayer进程的CPU占用并不能体现MediaCodec本身的耗用。
3.2 后续优化有一个原因是不可忽略的:在从解码器拿到buffer时,会先申请内存,然后拷贝得到AVFrame。但这一步也可以优化,直接将buffer指向AVFrame-> data,然后在OpenGL渲染完成之后,调用AMediaCodec_releaseOutputBufferbuffer还给解码器,这样就需要修改渲染的代码,不能做到软硬解逻辑一致。
4. 总结
当前的ijkplayer播放框架中,为了做到AndroidiOS跨平台的设计,在Native层直接调用Java MediaCodec的接口。如果将API级别提高,在Native层调用NDK MediaCodec接口并输出YUV数据,可以拿到解码后的YUV数据,也能保证软硬解渲染通路的一致性。
当前测试数据不充分,两种方式哪种性能、系统占用更优,还需要做更多的评估工作。


作者:金山视频云
链接:https://www.jianshu.com/p/41d3147a5e07
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。 











    推荐阅读