FFmpeg 使用MediaCodec

2022-04-18  本文已影响0人  Nothing_655f

FFmpeg 使用MediaCodec

环境: ffmpeg branch-3.4 @ 2021-08-31

FFmpeg 使用方法

编译的时候需要修改

include\libavutil\error.h

static inline <unknown> av_make_error_string(char *errbuf, size_t errbuf_size, int errnum)

将返回值类型改为 char *

static inline char * av_make_error_string(char *errbuf, size_t errbuf_size, int errnum)

ffmpeg中使用mediacodec的方法其实也就是常规的使用ffmpeg的方法,大致只是找查找codec的时候改成使用mediacodec插件

修改为使用MediaCodec的关键代码为

    // 查找视频解码器
    AVCodec *vCodec = NULL;
    // 可以自动查找
    // avcodec_find_decoder(ic->streams[videoStream]->codecpar->codec_id);
    // 指定为MediaCodec
    switch (vS->codecpar->codec_id)
    {
        case AV_CODEC_ID_H264:
            vCodec = avcodec_find_decoder_by_name("h264_mediacodec");
            break;
        case AV_CODEC_ID_H265:
            vCodec = avcodec_find_decoder_by_name("h265_mediacodec");
            break;
        case AV_CODEC_ID_MPEG4:
            vCodec = avcodec_find_decoder_by_name("mpeg4_mediacodec");
            break;
        default:
            break;
    }

FFmpeg 中的 MediaCodec流程

以H264为例,在ffmpeg中可以看到其解码组件的实现为如下结构体

FFmpeg/libavcodec/mediacodecdec.c

#if CONFIG_H264_MEDIACODEC_DECODER
AVCodec ff_h264_mediacodec_decoder = {
    .name           = "h264_mediacodec",
    .long_name      = NULL_IF_CONFIG_SMALL("H.264 Android MediaCodec decoder"),
    .type           = AVMEDIA_TYPE_VIDEO,
    .id             = AV_CODEC_ID_H264,
    .priv_data_size = sizeof(MediaCodecH264DecContext),
    .init           = mediacodec_decode_init,
    .decode         = mediacodec_decode_frame,
    .flush          = mediacodec_decode_flush,
    .close          = mediacodec_decode_close,
    .capabilities   = AV_CODEC_CAP_DELAY | AV_CODEC_CAP_AVOID_PROBING,
    .caps_internal  = FF_CODEC_CAP_SETS_PKT_DTS,
    .bsfs           = "h264_mp4toannexb",
};
#endif

这里重点就是这几个方法

    .init           = mediacodec_decode_init,
    .decode         = mediacodec_decode_frame,
    .flush          = mediacodec_decode_flush,
    .close          = mediacodec_decode_close,

1. 初始化 MediaCodec 解码器

stativ av_cold int mediacodec_decode_init(AVCodecContext *avctx)
{
    // MediaCodec H.264 解码相关属性
    MediaCodecH264DecContext *s = avctx->priv_data;

    // 调用 jni 创建 MediaFormat
     FFAMediaFormat *format = ff_AMediaFormat_new();   

    // 根据 codec_id 获取 mimeType 
    const char *codec_mime = "video/avc";
    // 获取 AVCodecContext 的 Extreadata 中的 sps & pps,存入H.264文件的头部 
    ret = h264_set_extradata(avctx, format);

    // 设置 MediaFormat 属性
    ff_AMediaFormat_setxxx(format, "xxx", xxx);

    ret = ff_mediacodec_dec_init(avctx, s->ctx)
}

2. 解码方法

解码方法入口为 mediacodec_process_data,如下可以看到是读取数据并传入 mediacodec_process_data处理,而 mediacodec_process_data 进一步调用了 ff_mediacodec_dec_decode

static int mediacodec_decode_frame(AVCodecContext *avctx, void *data, int *got_frame, AVPacket *avpkt)
{
    // mediacodec 私有参数
     MediaCodecH264DecContext *s = avctx->priv_data;
    // 解码得到的数据
    AVFrame *frame    = data;
    int ret;   

    // 将要解码的 AVPacket 写入 fifo
    av_fifo_generic_write(s->fifo, &input_pkt, sizeof(input_pkt), NULL);

    while (!*got_frame) {
        /* prepare the input data */
        if (s->buffered_pkt.size <= 0) {
            // 从 fifo 读到 buffered_pkt
            av_fifo_generic_read(s->fifo, &s->buffered_pkt, sizeof(s->buffered_pkt), NULL);
        }
         // 解码
        ret = mediacodec_process_data(avctx, frame, got_frame, &s->buffered_pkt);
    }
}

3. mediacodec buffer 获取及处理

在 ff_mediacodec_dec_decode 中的buffer 的获取同mediacodec api的使用一致,这些是通过jni 反射调用的java 的mediacodec 方法,MediaCodec API 方法使用 https://zhuanlan.zhihu.com/p/45224834

int ff_mediacodec_dec_decode(AVCodecContext *avctx, MediaCodecDecContext *s,
                             AVFrame *frame, int *got_frame,
                             AVPacket *pkt)
{
    while(...) {
        // 查找是否有可用的 输入缓冲区
        index = ff_AMediaCodec_dequeueInputBuffer(codec, input_dequeue_timeout_us);
        // 获取输入缓冲区
        data = ff_AMediaCodec_getInputBuffer(codec, index, &size);
        if (need_draining) {
            // 发送 结束信号
            status = ff_AMediaCodec_queueInputBuffer(codec, index, 0, 0, pts, flags);
        } else {
            // 传送 要解码的数据
            status = ff_AMediaCodec_queueInputBuffer(codec, index, 0, size, pts, 0);
        }
    }

    // 获取输出缓冲区
    index = ff_AMediaCodec_dequeueOutputBuffer(codec, &info, output_dequeue_timeout_us);
    if (index >= 0) {
        if (info.size) {
            if (s->surface) {
                // 拷贝buffer index数据
                ret = mediacodec_wrap_hw_buffer(avctx, s, index, &info, frame);
            } else {
                // 获取解码后的数据
                data = ff_AMediaCodec_getOutputBuffer(codec, index, &size);
             // 转换格式
                ret = mediacodec_wrap_sw_buffer(avctx, s, data, size, index, &info, frame);
            }
        } else {
            // 没有输出数据,释放缓冲区
            status = ff_AMediaCodec_releaseOutputBuffer(codec, index, 0);
    }
}

如上这个方法可以看到 在获取缓冲区这块是有差别的:

在设置了Surface的前提下,是调用mediacodec_wrap_hw_buffer,在frame中填充的并不是图像数据,而只是buffer index的封装。(反之,如果没有设置Surface就可以拿到图像数据,只是效率较低)

使用FFmpeg 的显示方法

1、可以透过ANativeWindow的方法,可以参考如下两篇文档

https://blog.csdn.net/Kennethdroid/article/details/107103315

https://blog.csdn.net/ericbar/article/details/80416328

这种方法有效率问题,内存大小和拷贝的问题,不过好处是可以拿到解码后的数据做需要的渲染

2、通过MediaCodec Surface的方法

在ffmpeg mediacodec.h中定义了几个函数,专门用于处理显示(因为mediacodec在surface模式下比较特殊)。

int av_mediacodec_default_init(AVCodecContext *avctx, AVMediaCodecContext *ctx, void *surface)用于给用户设置一个surfaceAV_CODEC_HW_CONFIG_METHOD_AD_HOC体现在这)

av_mediacodec_release_buffer(AVMediaCodecBuffer *buffer, int render)用于给用户“显示”一帧画面。(实际是调用MediaCodec的releaseOutputBuffer,render为1显示这个buffer,为0丢弃这个buffer不显示)

小结

1、ffmpeg的 mediacodec 支持的是有3.4分支,而4.x的分支并没有支持,所以支持性这块还是有差异

2、mediacodec的调用通过反射和jni,新版本ndk支持natvice 调用方法 https://developer.android.google.cn/ndk/reference/group/media#amediacodec,可以优化一些性能问题,需要移植

3、用来实现做播放器流程是需要做AV同步,用在转码的场景上应该是有性能的提升,可以获取硬解后的buffer 再对其编码,且ffmpeg 对container的支持性比较好

上一篇下一篇

猜你喜欢

热点阅读