Android NDK MediaCodec在ijkplayer中的实践

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/450917.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

leetcode-49-字母异位词分组(神奇的哈希)

题目描述&#xff1a; 给定一个字符串数组&#xff0c;将字母异位词组合在一起。字母异位词指字母相同&#xff0c;但排列不同的字符串。 示例: 输入: ["eat", "tea", "tan", "ate", "nat", "bat"], 输出: [[&quo…

【精心总结】java内存模型和多线程必会知识

内存模型 &#xff08;1&#xff09;java内存模型到底是个啥子东西&#xff1f; java内存模型是java虚拟机规范定义的一种特定模型&#xff0c;用以屏蔽不同硬件和操作系统的内存访问差异&#xff0c;让java在不同平台中能达到一致的内存访问效果&#xff0c;是在特定的协议下…

工作流 activity 视频教程 + redis 视频教程 百度网盘分享地址

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 云盘下载都没有密码&#xff0c;直接下载&#xff0c;解压有密码&#xff1a;chongxiangmengxiangjiaoyu&#xff0c; 解压完成后就可以…

快速解决 GRADLE 项目下载 gradle-*-all.zip 慢的问题

1、首先根据项目中 gradle\wrapper\gradle-wrapper.properties 文件的 distributionUrl 属性的值 #Tue Feb 06 12:27:20 CET 2018 distributionBaseGRADLE_USER_HOME distributionPathwrapper/dists zipStoreBaseGRADLE_USER_HOME zipStorePathwrapper/dists distributionUrlht…

[Python] 程序结构与控制流

1. 条件语句 if、else与elif语句用于控制条件代码的执行。条件语句的一般格式如下&#xff1a; if expression:statements elif expression:statements elif expression:statements ... else:statements 如果不需要执行任何操作&#xff0c;可以省略条件语句的else和elif子句。…

webrtc 源码结构

apiWebRTC 接口层。包括 DataChannel, MediaStream, SDP相关的接口。各浏览器都是通过该接口层调用的 WebRTC。call存放的是 WebRTC “呼叫&#xff08;Call&#xff09;” 相关逻辑层的代码。audio存放音频网络逻辑层相关的代码。音频数据逻辑上的发送&#xff0c;接收等代码。…

mysql查询流程解析及重要知识总结

时光荏苒啊&#xff01;在过两个月我就工作满三年了&#xff0c;大学毕业的情景还历历在目&#xff0c;而我已经默默的向油腻中年大叔进发了。作为一名苦逼的后端工程师&#xff0c;我搞过一段时间python&#xff0c;现在靠java糊口&#xff0c;但后来才发现&#xff0c;始终不…

界面无小事(八):RecyclerView增删item

界面无小事(一): RecyclerViewCardView了解一下 界面无小事(二): 让RecyclerView展示更多不同视图 界面无小事(三):用RecyclerView Toolbar做个文件选择器 界面无小事(四):来写个滚动选择器吧! 界面无小事(五):自定义TextView 界面无小事(六):来做个好看得侧拉菜单! 界面无小事…

Failed to install Tomcat7 service 解决

见&#xff1a; http://blog.csdn.net/desow/article/details/21446197 tomcat 安装时出现 Failed to install Tomcat7 service 今天在安装tomcat时提示 Failed to install Tomcat7 service了&#xff0c;花了大半天的时间找到了原因&#xff0c;下面分享给大家&#xff0c;希望…

保守官僚 诺基亚就这样迷失在智能机时代?

7月19日&#xff0c;诺基亚发布了二季度财报&#xff0c;净亏损达到了17亿美元&#xff0c;其中智能手机份额和销售量进一步下滑&#xff0c;这个智能手机的领导者&#xff0c;正在因智能手机而急速坠落。诺记亚领先业界近十年就把握住了智能手机的趋势&#xff0c;并推出了首款…

django集成ansibe实现自动化

动态生成主机列表和相关参数 def create_admin_domain(admin_node):workpath BASE_DIR /tools/ansible/scripthosts_file BASE_DIR /tools/ansible/host/ createhostfile()yml_file BASE_DIR /tools/ansible/yml/ create_admin_domain.ymldomain_path admin_node.doma…

extend 对象继承

function extend(o, n, override) {for (var p in n) {if (n.hasOwnProperty(p) && (!o.hasOwnProperty(p) || override))o[p] n[p];} }// 默认参数 var options {pageIndex: 1,pageTotal: 2 };// 新设置参数 var userOptions {pageIndex: 3,pageSize: 10 }extend(o…

【spring容器启动】之bean的实例化和初始化(文末附:spring循环依赖原理)

本次我们通过源码介绍ApplicationContext容器初始化流程&#xff0c;主要介绍容器内bean的实例化和初始化过程。ApplicationContext是Spring推出的先进Ioc容器&#xff0c;它继承了旧版本Ioc容器BeanFactory&#xff0c;并进一步扩展了容器的功能&#xff0c;增加了bean的自动识…

如何将自己的Java项目部署到外网

见&#xff1a;http://jingyan.baidu.com/article/90bc8fc864699af653640cf7.html 做b/s模式的web开发不同于c/s模式的客户端开发&#xff0c;c/s模式我们只要做好生成可执行文件发送给其他人&#xff0c;其他人就可以用了。但是c/s模式不同&#xff0c;在同一局域网下&#xf…

[Swift]LeetCode916.单词子集 | Word Subsets

★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★➤微信公众号&#xff1a;山青咏芝&#xff08;shanqingyongzhi&#xff09;➤博客园地址&#xff1a;山青咏芝&#xff08;https://www.cnblogs.com/strengthen/&#xff09;➤GitHub地址&a…

揭秘腾讯研究院输出策略:产品和人才的孵化器

直到现在&#xff0c;腾讯研究院创始人郑全战仍坚持面试招入研究院的每一个人&#xff0c;并做详细记录。天赋上的灵性、性格中的包容是他看重的&#xff0c;当然首先人要踏实。大约6年前&#xff0c;郑全战加入腾讯&#xff0c;负责筹建中国互联网公司中的第一个研究院&#x…

java后端必会【基础知识点】

&#xff08;一&#xff09;java集合类&#xff08;done&#xff09; 在java集合类中最常用的是Collection和Map的接口实现类。Collection又分为List和Set两类接口&#xff0c;List的实现类有ArrayList、LinkedList、Vector、Stack&#xff0c;Set接口的实现类有HashSet、Tree…

无法连接虚拟设备ide1:0,主机上没有相对应的设备... 解决

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 运行虚拟机出现报错&#xff1a; 无法连接虚拟设备ide1:0&#xff0c;主机上没有相对应的设备&#xff0c;您 要在每次开启此虚拟机时都…

缴满15年能领多少钱 养老金计算公式网上疯传

社保人员称我省计算方式与各设区市平均工资挂钩&#xff0c;与网上不同 最近&#xff0c;关于“延迟退休”引起各方高度关注&#xff0c;成为广大居民十分关心的话题。是否延迟退休尚无定论&#xff0c;但在网上有不少关于养老金的计算。那网上流传的计算方法是否科学&#xff…

48_并发编程-线程-资源共享/锁

一、数据共享多个线程内部有自己的数据栈&#xff0c;数据不共享&#xff1b;全局变量在多个线程之间是共享的。1 # 线程数据共享不安全加锁2 3 import time4 from threading import Thread, Lock5 6 7 num 1008 9 def func(t_lock): 10 global num 11 t_lock.acquire…