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)
用于给用户设置一个surface
(AV_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的支持性比较好
网友评论