临文乍了了,彻卷兀若无。这篇文章主要讲述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 MediaCodec接Surface的方式。
本文的主要内容是:在ijkplayer框架内适配NDK MediaCodec,不再使用Surface输出,改用YUV输出达到软硬解通路一致的渲染流程。
下文提到的Java MediaCodec,如果不做特别说明,都指的Surface 输出。1. ijkplayer硬解码的过程
下文提到的NDK MediaCodec,如果不做特别说明,都指的YUV 输出。
在增加NDK MediaCodec硬解流程之前,先简要说明Java MediaCodec的流程:
文章图片
Android Java MediaCodec
图中主要有三个步骤:AVPacket-> Decode-> AVFrame;
- read线程读到packet,放入packet queue;
- 解码得到一帧AVFrame,放入picture queue;
- 从picture queue取出一帧,渲染AVFrame(overlay)。
但是有一点需要注意:我们从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);
...
}
}
- 可以看到解码线程又创建了子线程,
enqueue_thread_func()
主要是用来将压缩数据(H.264/H.265)放入解码器,这样往解码器放数据在enqueue_thread_func()
里面,从解码器取数据在func_run_sync()
里面; drain_output_buffer()
从解码器取出一个AVFrame
,但是这个AVFrame-> data
为NULL
并没有数据,其中AVFrame-> opaque
指针指向一个SDL_AMediaCodecBufferProxy
结构体:
struct SDL_AMediaCodecBufferProxy
{
int buffer_id;
int buffer_index;
int acodec_serial;
SDL_AMediaCodecBufferInfo buffer_info;
};
这些成员由硬解器
SDL_AMediaCodecFake_dequeueOutputBuffer
得来,它们在视频渲染的时候会用到;- 将AVFrame放入待渲染队列。
根据上面的解码流程,增加NDK MediaCodec就只需2个关键步骤:
- 创建IJKFF_Pipenode;
- 创建相应的解码线程。
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;
里面有两个比较重要的成员
AMediaFormat
、AMediaCodec
,他们就是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_sync
func_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;
}
- 从解码器拿到解码后的数据buffer;
- 填充
AVFrame
结构体,申请相应大小的内存,由于我们设置解码器的输出格式是YUV420P,所以frame-> format = AV_PIX_FMT_YUV420P
,然后将buffer拷贝到frame-> data
; - 放入待渲染队列
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 MediaCodec和NDK MediaCodec解码时CPU及GPU的表现:
Java MediaCodec CPU 占用大约在5%左右
文章图片
Java MediaCodec解码CPU表现NDK MediaCodec CPU占用大约在12%左右
文章图片
NDK MediaCodec解码CPU表现Java MediaCodec GPU占用表现
文章图片
Java MediaCodec解码GPU表现NDK MediaCodec GPU占用表现
文章图片
NDK MediaCodec解码GPU表现3.1 测试数据分析NDK MediaCodec的CPU占比大约高出7%,但是GPU表现较好。
CPU为什么会比Java MediaCodec解码时高呢?
我们这里一直评估的Java MediaCodec,都指的Surface输出。这意味着接口内部完成了解码和渲染工作,高度封装的解码和渲染,内部做了一些数据传递优化的工作。同时ijkplayer进程的CPU占用并不能体现MediaCodec本身的耗用。
3.2 后续优化有一个原因是不可忽略的:在从解码器拿到buffer时,会先申请内存,然后拷贝得到
AVFrame
。但这一步也可以优化,直接将buffer指向AVFrame->
data
,然后在OpenGL渲染完成之后,调用AMediaCodec_releaseOutputBuffer
将buffer还给解码器,这样就需要修改渲染的代码,不能做到软硬解逻辑一致。4. 总结
当前的ijkplayer播放框架中,为了做到Android和iOS跨平台的设计,在Native层直接调用Java MediaCodec的接口。如果将API级别提高,在Native层调用NDK MediaCodec接口并输出YUV数据,可以拿到解码后的YUV数据,也能保证软硬解渲染通路的一致性。
当前测试数据不充分,两种方式哪种性能、系统占用更优,还需要做更多的评估工作。
作者:金山视频云
链接:https://www.jianshu.com/p/41d3147a5e07
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
推荐阅读
- spark中map与mapPartitions区别
- Android 9.0更新
- 朝花夕拾Android性能篇之Android进程管理机制
- android 开发设计模式---观察者模式
- webAPP 原生APP 对比
- android中Scrollview嵌套WebView问题
- Androidtouch事件分发
- 安卓开发 Activity入门
- 十大数据可视化工具详细图解