https://www.jianshu.com/p/41d3147a5e07

从API 21(Android 5.0)开始Android提供C层的NDK MediaCodec的接口。

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 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 新建pipenode

NDK 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; char acodec_name[128];
int frame_width;
int frame_height;
int frame_rotate_degrees; AVCodecContext *avctx; // not own
AVBitStreamFilterContext *bsfc; // own
size_t nal_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_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;
}
  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%左右

 
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 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
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

最新文章

  1. WebUtils-网络请求工具类
  2. innodb log file size 配置估算以及修改
  3. iPad应用开发者的建议
  4. JAVA NIO的理解
  5. 008sudo用户管理
  6. CSS3最简洁的轮播图
  7. luoguP2266 爱的距离
  8. QT 内存泄露 检测
  9. 超链接访问过后hover样式就不出现的问题是什么?如何解决?
  10. Java面向对象——类,对象和方法
  11. Overture小课堂之如何演绎钢琴滑音
  12. react 组件列表
  13. CentOS 7 安装配置 Vsftpd
  14. C++ code:函数指针数组
  15. 对linux安装中文字体库
  16. 每日linux命令学习-引用符号(反斜杠\,单引号&#39;&#39;,双引号&quot;&quot;)
  17. [0406]学习一个——Unit 1 Html、CSS与版本控制
  18. c# 用户页面
  19. VisualSVN server 搭建SVN服务器
  20. 006.MySQL双主-Master02可用配置

热门文章

  1. 在function module 中向数据库插入数据
  2. 266A
  3. HTML5服务器消息推送(java版)
  4. bat脚本简单命令
  5. 利用Tensorflow实现卷积神经网络模型
  6. gcc dynamic load library
  7. WebAPI的跨域访问CORS三种方法
  8. iOS UI基础-9.2 UITableView 简单微博列表
  9. python对字典及列表递归排序
  10. vue中使用ckeditor