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

时间: 2024-08-05 04:17:11

Android NDK MediaCodec在ijkplayer中的实践的相关文章

Android NDK 在eclipse中生成C/C++头文件

我们知道在Android NDK开发中,让原生函数名及参数列表与java类文件的原始定义一致是繁杂而多余的,因为JDK自带一个名为javah的命令行工具来执行任务,javah工具可以为原生方法解析Java类文件并生成由原生方法声明组成的头文件.但是在开发中如果来回切换命令终端的话,可以说是很繁琐的,今天的这篇博文就来阐述一下如何在eclipse中生成头文件. (1)打开Eclipse IDE,在顶部菜单栏选择Run|External Tools|External Tools Configurat

Android NDK中的C++调试踩坑标记

RT, Android NDK中的C++调试, GDB调试比较麻烦,在ADT Eclipse中: 1.配置好NDK给工程加上Native Support 2.编译中加上NDK_DEBUG=1 3.然后改造下mk文件: #APP_DEBUG will be set by android-ndk if NDK_DEBUG=1 is set. ifdef APP_DEBUG ifeq ($(APP_DEBUG),true) CFLAGS+= -O0 -g LOCAL_CFLAGS+= -D_DEBUG

Android NDK中的Apache ANT和GUN Make

Android NDK是Android软件开发包SDK的相关工具集,用来扩展Android SDK的功能,从而使开发人员能够使用机器代码生成的编程语言(如C.c++和汇编语言)实现一些对代码性能要求较高的模块并将这些模块嵌入到Android应用程序中. Apache ANT是命令行构建工具,可以驱动任何根据目标和任务描述的过程. 安装命令 sudo apt-get install ant GNU Make是一种构建工具,用于控制应用源代码的可执行代码和其他部分代码的生成. 安装命令 sudo a

【转载】cocos2dx 中 Android NDK 加载动态库的问题

原文地址:http://blog.csdn.net/sozell/article/details/10551309 cocos2dx 中 Android NDK 加载动态库的问题 闲聊 最近在接入各个平台的SDK,遇到了不少问题,也从中了解了不少知识,之前一直觉得没啥好写的,毕竟做了4个月的游戏开发,也没有碰上什么真正的大问题,cocos2dx的引擎包得也很好,能让人把大部分时间都关注在游戏逻辑.效果的处理上,当然,之前的libevent还是小坑一下,但是和后来遇到的相比,也算不上什么了. 我最

android NDK开发在本地C/C++源码中设置断点单步调试具体教程

近期在学android NDK开发,折腾了一天,最终可以成功在ADT中设置断点单步调试本地C/C++源码了.网上关于这方面的资料太少了,并且大都不全,并且调试过程中会出现各种各样的问题,真是非常磨人.程序员就得有耐心. 把自己的调试过程记录下来.希望对须要的朋友有帮助. 在看本文之前,请先确保你已经成功编译了一个android NDKproject,而且可以在模拟器或者真机上执行.至于怎么编译NDKproject,包含配置.生成.so文件等等.可以參考我的前一篇博客:http://blog.cs

android ndk中的工具使用

1. 动态so处理: arm-linux-androideabi-readelf.exe -a XX.so > xx.txt 输出所有函数 arm-linux-androideabi-objdump.exe -dx XX.so > xx.txt 反汇编so包,此时使用 $(JNI_PROJ_PATH)/obj\local\armeabi下面带符号表的so包.JNI_PROJ_PATH为编译so包时jni文件夹的根目录 2. 静态a处理: arm-linux-androideabi-ar.exe

如何定位Android NDK开发中遇到的错误

做android应用的调试,最怕就是报错,crash,看到这篇好文章,记录一下: 转自:http://www.csdn.net/article/2014-12-30/2823366-Locate-Android-NDK Android NDK是什么? Android NDK 是在SDK前面又加上了“原生”二字,即Native Development Kit,因此又被Google称为“NDK”.众所周知,Android程序运行在Dalvik虚拟机中,NDK允许用户使用类似C / C++之类的原生代

android NDK开发在本地C/C++源代码中设置断点单步调试详细教程

最近在学android NDK开发,折腾了一天,终于能够成功在ADT中设置断点单步调试本地C/C++源代码了.网上关于这方面的资料太少了,而且大都不全,并且调试过程中会出现各种各样的问题,真是很磨人,程序猿就得有耐心.把自己的调试过程记录下来,希望对需要的朋友有帮助. 在看本文之前,请先确保你已经成功编译了一个android NDK工程,并且能够在模拟器或者真机上运行.至于怎么编译NDK工程,包括配置.生成.so文件等等,可以参考我的前一篇博客:http://blog.csdn.net/u013

Android NDK开发(八)——应用监听自身卸载,弹出用户反馈调查

转载请注明出处:http://blog.csdn.net/allen315410/article/details/42521251 监听卸载情景和原理分析 1,情景分析 在上上篇博客中我写了一下NDK开发实践项目,使用开源的LAME库转码MP3,作为前面几篇基础博客的加深理解使用的,但是这样的项目用处不大,除了练练NDK功底.这篇博客,我将讲述一下一个各大应用中很常见的一个功能,同样也是基于JNI开发的Android应用小Demo,看完这个之后,不仅可以加深对NDK开发的理解,而且该Demo也可