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

转载于:https://www.cnblogs.com/jukan/p/9845673.html

Android NDK MediaCodec在ijkplayer中的实践相关推荐

  1. 【Android 逆向】Android 进程注入工具开发 ( Visual Studio 开发 Android NDK 应用 | Visual Studio 中 SDK 和 NDK 安装位置 )

    文章目录 一.Visual Studio 中安装 " 使用 C++ 的移动开发 " 开发库 二.Visual Studio 中安装的 Android SDK 和 NDK 位置 三. ...

  2. 使用LeakTracer检测android NDK C/C++代码中的memory leak

    Memory issue是C/C++开发中比较常遇到,经常带给人比较大困扰,debug起来又常常让人无从下手的一类问题,memory issue主要又分为memory leak,野指针,及其它非法访问 ...

  3. unity android ndk的作用,Unity中编写Android下使用的so插件

    四月 24.2019. 0 Comment 在android上编写插件有多种路子: 1. c# portable library,用c#写的可移植的assembly.使用起来最简单.最方便,比如那些j ...

  4. Android NDK学习:JNI中的数组、引用和异常的处理

    JNI的文档 https://docs.oracle.com/javase/6/docs/technotes/guides/jni/spec/jniTOC.html JNI数组操作 调用数组 Java ...

  5. android ndk 界面开发教程,AndroidStudio NDK开发最佳入门实践

    AndroidStudio NDK开发最佳入门实践 网上一些介绍AndroidStudio NDK入门的教程,感觉都不是很完整和全面,也没有告诉初学AndroidStudio NDK的同学们一些需要注 ...

  6. Android NDK 中堆栈日志 add2line 的分析实践

    文章目录 目的 常用的辅助工具 分析步骤 参考 目的 Android NDK 中出现的 crash 日志分析定位,使用 addr2line 对库中定位so 动态库崩溃位置,定位到某个函数的具体的代码行 ...

  7. android ndk之opencv+MediaCodec硬编解码来处理视频动态时间水印

    android ndk之opencv+MediaCodec硬编解码来处理视频水印学习笔记 android视频处理学习笔记.以前android增加时间水印的需求,希望多了解视频编解码,直播,特效这一块, ...

  8. Android NDK开发之旅(2):一篇文章搞定Android Studio中使用CMake进行NDK/JNI开发

    Android NDK开发之旅(2):一篇文章搞定android Studio中使用CMake进行NDK/JNI开发 (码字不易,转载请声明出处:http://blog.csdn.NET/andrex ...

  9. 【Android 逆向】Android 进程注入工具开发 ( 系统调用 | Android NDK 中的系统调用示例 )

    文章目录 一.系统调用 二.Android NDK 中的系统调用示例 一.系统调用 在 " 用户层 " , 运行的都是用户应用程序 ; 用户层 下面 是 驱动层 , 驱动层 下面是 ...

最新文章

  1. 智课雅思词汇---十、pend是什么意思
  2. 像“打游戏”一样用Numpy,可视化编程环境Math Inspector了解一下? | 代码开源
  3. Android4.0源码Launcher启动流程分析【android源码Launcher系列一】
  4. C#中使用StreamReader实现文本文件的读取与写入
  5. 图模型概述:三种分布(联合、条件、边缘分布)
  6. hdu 4267 多维树状数组
  7. 黑色全屏个人主页bootstrap4模板
  8. 2017.9.17 选数 失败总结
  9. java 获得 加载类_java 类的加载,与获得相应的方法
  10. Kotlin基础-对象声明和表达式
  11. SQL Server 2012安装异常:Error while enabling Windows feature: NetFx3, Error Code: -2146498298
  12. [深度学习] Python人脸识别库face_recognition使用教程
  13. 数据结构实验三 线性表的链式存储结构及实现
  14. 什么是GRE词汇红宝书?
  15. chrome内核 用h5调用高拍仪(摄像图)实现拍证件照
  16. 计算机组装图纸手画,手工绘图的方法和步骤 -工程
  17. 如何开通微信小程序在线客服系统?
  18. js 实现统计网站访问量
  19. 企业微博营销平台营销模式是怎样的?
  20. spring security http.rememberMe()使用和原理解析

热门文章

  1. CCNP-第五篇-OSPF高级版(二)
  2. 【牛客 - 21302】被3整除的子序列(线性dp)
  3. 【UVA - 10815】 Andy's First Dictionary(STL+字符处理)
  4. 从零开始学视觉Transformer(5):如何训练ViT模型、DeiT算法解析
  5. Hbase单节点安装
  6. 基于android公交车线路查询论文文献,本科毕业论文---基于android的手机公交线路查询系统.doc...
  7. list.size为1但是内容为null
  8. C:01---数据类型与ASCII
  9. 写出表格的结构html,一个面试题,根据json结构生成html表格
  10. C++学习笔记章节中 面向对象详解