FFmpeg 使用MediaCodec
環(huán)境: 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的方法其實也就是常規(guī)的使用ffmpeg的方法,大致只是找查找codec的時候改成使用mediacodec插件
修改為使用MediaCodec的關(guān)鍵代碼為
// 查找視頻解碼器
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中可以看到其解碼組件的實現(xiàn)為如下結(jié)構(gòu)體
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 解碼相關(guān)屬性
MediaCodecH264DecContext *s = avctx->priv_data;
// 調(diào)用 jni 創(chuàng)建 MediaFormat
FFAMediaFormat *format = ff_AMediaFormat_new();
// 根據(jù) codec_id 獲取 mimeType
const char *codec_mime = "video/avc";
// 獲取 AVCodecContext 的 Extreadata 中的 sps & pps,存入H.264文件的頭部
ret = h264_set_extradata(avctx, format);
// 設(shè)置 MediaFormat 屬性
ff_AMediaFormat_setxxx(format, "xxx", xxx);
ret = ff_mediacodec_dec_init(avctx, s->ctx)
}
2. 解碼方法
解碼方法入口為 mediacodec_process_data,如下可以看到是讀取數(shù)據(jù)并傳入 mediacodec_process_data處理,而 mediacodec_process_data 進一步調(diào)用了 ff_mediacodec_dec_decode
static int mediacodec_decode_frame(AVCodecContext *avctx, void *data, int *got_frame, AVPacket *avpkt)
{
// mediacodec 私有參數(shù)
MediaCodecH264DecContext *s = avctx->priv_data;
// 解碼得到的數(shù)據(jù)
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 反射調(diào)用的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(...) {
// 查找是否有可用的 輸入緩沖區(qū)
index = ff_AMediaCodec_dequeueInputBuffer(codec, input_dequeue_timeout_us);
// 獲取輸入緩沖區(qū)
data = ff_AMediaCodec_getInputBuffer(codec, index, &size);
if (need_draining) {
// 發(fā)送 結(jié)束信號
status = ff_AMediaCodec_queueInputBuffer(codec, index, 0, 0, pts, flags);
} else {
// 傳送 要解碼的數(shù)據(jù)
status = ff_AMediaCodec_queueInputBuffer(codec, index, 0, size, pts, 0);
}
}
// 獲取輸出緩沖區(qū)
index = ff_AMediaCodec_dequeueOutputBuffer(codec, &info, output_dequeue_timeout_us);
if (index >= 0) {
if (info.size) {
if (s->surface) {
// 拷貝buffer index數(shù)據(jù)
ret = mediacodec_wrap_hw_buffer(avctx, s, index, &info, frame);
} else {
// 獲取解碼后的數(shù)據(jù)
data = ff_AMediaCodec_getOutputBuffer(codec, index, &size);
// 轉(zhuǎn)換格式
ret = mediacodec_wrap_sw_buffer(avctx, s, data, size, index, &info, frame);
}
} else {
// 沒有輸出數(shù)據(jù),釋放緩沖區(qū)
status = ff_AMediaCodec_releaseOutputBuffer(codec, index, 0);
}
}
如上這個方法可以看到 在獲取緩沖區(qū)這塊是有差別的:
在設(shè)置了Surface的前提下,是調(diào)用mediacodec_wrap_hw_buffer,在frame中填充的并不是圖像數(shù)據(jù),而只是buffer index的封裝。(反之,如果沒有設(shè)置Surface就可以拿到圖像數(shù)據(jù),只是效率較低)
使用FFmpeg 的顯示方法
1、可以透過ANativeWindow的方法,可以參考如下兩篇文檔
https://blog.csdn.net/Kennethdroid/article/details/107103315
https://blog.csdn.net/ericbar/article/details/80416328
這種方法有效率問題,內(nèi)存大小和拷貝的問題,不過好處是可以拿到解碼后的數(shù)據(jù)做需要的渲染
2、通過MediaCodec Surface的方法
在ffmpeg mediacodec.h中定義了幾個函數(shù),專門用于處理顯示(因為mediacodec在surface模式下比較特殊)。
int av_mediacodec_default_init(AVCodecContext *avctx, AVMediaCodecContext *ctx, void *surface)用于給用戶設(shè)置一個surface(AV_CODEC_HW_CONFIG_METHOD_AD_HOC體現(xiàn)在這)
av_mediacodec_release_buffer(AVMediaCodecBuffer *buffer, int render)用于給用戶“顯示”一幀畫面。(實際是調(diào)用MediaCodec的releaseOutputBuffer,render為1顯示這個buffer,為0丟棄這個buffer不顯示)
小結(jié)
1、ffmpeg的 mediacodec 支持的是有3.4分支,而4.x的分支并沒有支持,所以支持性這塊還是有差異
2、mediacodec的調(diào)用通過反射和jni,新版本ndk支持natvice 調(diào)用方法 https://developer.android.google.cn/ndk/reference/group/media#amediacodec,可以優(yōu)化一些性能問題,需要移植
3、用來實現(xiàn)做播放器流程是需要做AV同步,用在轉(zhuǎn)碼的場景上應(yīng)該是有性能的提升,可以獲取硬解后的buffer 再對其編碼,且ffmpeg 對container的支持性比較好