C++中使用FFmpeg適配自定義編碼器的實(shí)現(xiàn)方法
1 編碼流程
FFmpeg是一個(gè)開源的多媒體框架,底層可對接實(shí)現(xiàn)多種編解碼器,下面參考文件doc/examples/encode_video.c分析編碼一幀的流程
1.1 整體流程
統(tǒng)一的編碼流程如下圖所示

FFmpeg使用的是引用計(jì)數(shù)的思想,對于一塊buffer,剛申請時(shí)引用計(jì)數(shù)為1,每有一個(gè)模塊進(jìn)行使用,引用計(jì)數(shù)加1,使用完畢后引用計(jì)數(shù)減1,當(dāng)減為0時(shí)釋放buffer。
此流程中需要關(guān)注buffer的分配,對于編碼器來說,輸入buffer是yuv,也就是上圖中的frame,輸出buffer是碼流包,也就是上圖中的pkt,下面對這兩個(gè)buffer進(jìn)行分析
- frame:這個(gè)結(jié)構(gòu)體是由
av_frame_alloc分配的,但這里并沒有分配yuv的內(nèi)存,yuv內(nèi)存是av_frame_get_buffer分配的,可見這里輸入buffer完全是來自外部的,不需要編碼器來管理,編碼器只需要根據(jù)所給的yuv地址來進(jìn)行編碼就行了 - pkt:這個(gè)結(jié)構(gòu)體是由
av_packet_alloc分配的,也沒有分配碼流包的內(nèi)存,可見這里pkt僅僅是一個(gè)引用,pkt直接傳到了avcodec_receive_packet接口進(jìn)行編碼,完成之后將pkt中碼流的內(nèi)容寫到文件,最后調(diào)用av_packet_unref接口減引用計(jì)數(shù),因此這里pkt是編碼器內(nèi)部分配的,分配完成之后會(huì)減pkt的引用計(jì)數(shù)加1,然后輸出到外部,外部使用完畢之后再減引用計(jì)數(shù)來釋放buffer
編碼一幀的相關(guān)代碼如下:
static void encode(AVCodecContext *enc_ctx, AVFrame *frame, AVPacket *pkt,
FILE *outfile)
{
int ret;
/* send the frame to the encoder */
if (frame)
printf("Send frame %3"PRId64"\n", frame->pts);
ret = avcodec_send_frame(enc_ctx, frame);
if (ret < 0) {
fprintf(stderr, "Error sending a frame for encoding\n");
exit(1);
}
while (ret >= 0) {
ret = avcodec_receive_packet(enc_ctx, pkt);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
return;
else if (ret < 0) {
fprintf(stderr, "Error during encoding\n");
exit(1);
}
printf("Write packet %3"PRId64" (size=%5d)\n", pkt->pts, pkt->size);
fwrite(pkt->data, 1, pkt->size, outfile);
av_packet_unref(pkt);
}
}
其中avcodec_receive_packet返回EAGAIN表示送下一幀,返回EOF表示編碼器內(nèi)部已經(jīng)沒有碼流。
1.2 內(nèi)部流程
此處分析編碼一幀的內(nèi)部流程,首先看FFmpeg內(nèi)部編碼器的上下文,其中有三個(gè)重要結(jié)構(gòu)體
typedef struct AVCodecInternal {
...
/**
* The input frame is stored here for encoders implementing the simple
* encode API.
*
* Not allocated in other cases.
*/
AVFrame *in_frame;
/**
* Temporary buffers for newly received or not yet output packets/frames.
*/
AVPacket *buffer_pkt;
AVFrame *buffer_frame;
...
} AVCodecInternal;
下面結(jié)合送幀和收流的接口進(jìn)行介紹
- avcodec_send_frame: 送幀接口,將yuv的幀信息賦值到
buffer_frame,然后觸發(fā)一幀編碼,將編碼出的碼流賦值到buffer_pkt - avcodec_receive_packet: 收流接口,檢查上下文中是否有已經(jīng)編碼好的碼流
buffer_pkt,如果有則將其返回,如果沒有再觸發(fā)一幀編碼,將編碼好的碼流返回
可見send和receive接口均可觸發(fā)一幀編碼,此處觸發(fā)一幀編碼分為兩個(gè)流程,receive流程和simple流程,代碼片段如下:
static int encode_receive_packet_internal(AVCodecContext *avctx, AVPacket *avpkt)
{
...
if (ffcodec(avctx->codec)->cb_type == FF_CODEC_CB_TYPE_RECEIVE_PACKET) {
ret = ffcodec(avctx->codec)->cb.receive_packet(avctx, avpkt);
if (ret < 0)
av_packet_unref(avpkt);
else
// Encoders must always return ref-counted buffers.
// Side-data only packets have no data and can be not ref-counted.
av_assert0(!avpkt->data || avpkt->buf);
} else
ret = encode_simple_receive_packet(avctx, avpkt);
...
}
如果是receive流程,則直接調(diào)用receive_packet接口的回調(diào),該接口中注冊定制編碼器的接口,完成一幀編碼。如果是simple流程,則調(diào)用的是encode_simple_receive_packet,這是FFmpeg封裝的一個(gè)簡易流程,其中調(diào)用的是encode接口,代碼片段如下,詳細(xì)分析可參考文章:
static int encode_simple_internal(AVCodecContext *avctx, AVPacket *avpkt)
{
AVFrame *frame = avci->in_frame;
const FFCodec *const codec = ffcodec(avctx->codec);
int got_packet;
...
/* 拷貝buffer_frame到in_frame */
...
if (CONFIG_FRAME_THREAD_ENCODER && avci->frame_thread_encoder) {
/* This will unref frame. */
ret = ff_thread_video_encode_frame(avctx, avpkt, frame, &got_packet);
} else {
ret = ff_encode_encode_cb(avctx, avpkt, frame, &got_packet);
#if FF_API_THREAD_SAFE_CALLBACKS
if (frame) {
av_frame_unref(frame);
}
#endif
}
...
return ret;
}
- simple流程中會(huì)把
buffer_frame的引用拷貝到in_frame,然后將in_frame送幀編碼,意味著其內(nèi)部只能緩存一幀,不支持多幀緩存。并且simple流程中,調(diào)用send之后,如果調(diào)用receive成功獲取到一包碼流,下一次調(diào)用receive將會(huì)返回EAGAIN,且不會(huì)調(diào)用encode接口,因此對于不支持多幀緩存的編碼器而言,如果send一幀后,需要receive兩包碼流,那么獲取到一包碼流之后receive接口會(huì)返回EAGAIN,循環(huán)退出進(jìn)行下一次send,此時(shí)上一幀未編碼的yuv會(huì)被覆蓋 - receive流程中沒有該限制,直接調(diào)用了
receive_packet接口,因此如果需要在ffmpeg適配層做多幀緩存,可以使用receive的流程。另外receive流程沒有上述限制,在成功收到一幀碼流之后,仍然會(huì)調(diào)用receive,比較靈活,可以做一些定制化的操作
2 適配接口
適配接口參考ffmpeg/libavcodec/nvenc_h264.c,這是英偉達(dá)的硬件編碼器接口,自定義一個(gè)編碼器只需實(shí)現(xiàn)以下結(jié)構(gòu)體
const FFCodec ff_h264_nvenc_encoder = {
.p.name = "h264_nvenc",
.p.long_name = NULL_IF_CONFIG_SMALL("NVIDIA NVENC H.264 encoder"),
.p.type = AVMEDIA_TYPE_VIDEO,
.p.id = AV_CODEC_ID_H264,
.init = ff_nvenc_encode_init,
FF_CODEC_RECEIVE_PACKET_CB(ff_nvenc_receive_packet),
.close = ff_nvenc_encode_close,
.flush = ff_nvenc_encode_flush,
.priv_data_size = sizeof(NvencContext),
.p.priv_class = &h264_nvenc_class,
.defaults = defaults,
.p.capabilities = AV_CODEC_CAP_DELAY | AV_CODEC_CAP_HARDWARE |
AV_CODEC_CAP_ENCODER_FLUSH | AV_CODEC_CAP_DR1,
.caps_internal = FF_CODEC_CAP_INIT_CLEANUP,
.p.pix_fmts = ff_nvenc_pix_fmts,
.p.wrapper_name = "nvenc",
.hw_configs = ff_nvenc_hw_configs,
};
這里面最重要三個(gè)接口是init、close和receive,還有一個(gè)比較重要的數(shù)據(jù)結(jié)構(gòu)是option,此處寫明了編碼器支持的具體配置
static const AVOption options[] = {
#ifdef NVENC_HAVE_NEW_PRESETS
{ "preset", "Set the encoding preset", OFFSET(preset), AV_OPT_TYPE_INT, { .i64 = PRESET_P4 }, PRESET_DEFAULT, PRESET_P7, VE, "preset" },
#else
{ "preset", "Set the encoding preset", OFFSET(preset), AV_OPT_TYPE_INT, { .i64 = PRESET_MEDIUM }, PRESET_DEFAULT, PRESET_LOSSLESS_HP, VE, "preset" },
#endif
{ "default", "", 0, AV_OPT_TYPE_CONST, { .i64 = PRESET_DEFAULT }, 0, 0, VE, "preset" },
{ "slow", "hq 2 passes", 0, AV_OPT_TYPE_CONST, { .i64 = PRESET_SLOW }, 0, 0, VE, "preset" },
{ "medium", "hq 1 pass", 0, AV_OPT_TYPE_CONST, { .i64 = PRESET_MEDIUM }, 0, 0, VE, "preset" },
...
};
static const AVClass h264_nvenc_class = {
.class_name = "h264_nvenc",
.item_name = av_default_item_name,
.option = options,
.version = LIBAVUTIL_VERSION_INT,
};
2.1 init、close
init是初始化編碼器的接口,在avcodec_open2中調(diào)用,定義接口如下,此接口一般是根據(jù)用戶的option配置,來對編碼器進(jìn)行相應(yīng)的初始化
int (*init)(struct AVCodecContext *)
close是關(guān)閉編碼器的接口,在avcodec_free_context中調(diào)用,定義接口如下,該接口完成編碼器內(nèi)部的一些資源釋放操作
int (*close)(struct AVCodecContext *)
2.2 option
每個(gè)編碼器有一個(gè)自定義的上下文,其作用是在編碼器初始化之前對上下文進(jìn)行配置,編碼器初始化的時(shí)候就可以按照用戶的配置來初始化,以nvenc為例該上下文的定義為
ypedef struct NvencContext
{
...
// 隊(duì)列相關(guān)的定義
...
// 編碼相關(guān)的配置信息
int preset;
int profile;
int level;
int tier;
int rc;
int cbr;
...
} NvencContext;
該上下文在avcodec內(nèi)部使用,對外不可見,因此需要option的方式開放對外配置的接口,使用一個(gè)AVOption來描述一個(gè)編碼器的配置
typedef struct AVOption {
const char *name;
/**
* short English help text
* @todo What about other languages?
*/
const char *help;
/**
* The offset relative to the context structure where the option
* value is stored. It should be 0 for named constants.
*/
int offset;
enum AVOptionType type;
/**
* the default value for scalar options
*/
union {
int64_t i64;
double dbl;
const char *str;
/* TODO those are unused now */
AVRational q;
} default_val;
double min; ///< minimum valid value for the option
double max; ///< maximum valid value for the option
int flags;
const char *unit;
} AVOption;
其中關(guān)鍵的是offset和type成員,offset描述了這個(gè)option在上下文中的偏移量,type描述了成員占據(jù)的長度,有這兩個(gè)信息就可以在不對外暴露內(nèi)部上下文的情況下,修改其中的值,用戶配置option的示例如下
av_opt_set(c->priv_data, "preset", "slow", 0);
2.3 receive
nvenc在avcodec層實(shí)現(xiàn)了多幀緩存,因此他實(shí)現(xiàn)的是receive接口,代碼片段如下,需要注意這里輸入輸出都存在拷貝
int ff_nvenc_receive_packet(AVCodecContext *avctx, AVPacket *pkt)
{
NvencSurface *tmp_out_surf;
int res, res2;
NvencContext *ctx = avctx->priv_data;
AVFrame *frame = ctx->frame; // 這個(gè)是init中申請的
if (!frame->buf[0]) {
// 將buffer_frame引用拷貝到frame中
res = ff_encode_get_frame(avctx, frame);
if (res < 0 && res != AVERROR_EOF)
return res;
}
// 編碼一幀,推測是阻塞的,nv相關(guān)的函數(shù)沒有找到介紹,其中存在拷貝
res = nvenc_send_frame(avctx, frame);
if (res < 0) {
if (res != AVERROR(EAGAIN))
return res;
} else
av_frame_unref(frame);
if (output_ready(avctx, avctx->internal->draining)) {
// 從ready隊(duì)列中取編碼好的surface
av_fifo_read(ctx->output_surface_ready_queue, &tmp_out_surf, 1);
res = nvenc_push_context(avctx);
if (res < 0)
return res;
// 拷貝到pkt中
res = process_output_surface(avctx, pkt, tmp_out_surf);
res2 = nvenc_pop_context(avctx);
if (res2 < 0)
return res2;
if (res)
return res;
// surface再放回unused隊(duì)列
av_fifo_write(ctx->unused_surface_queue, &tmp_out_surf, 1);
} else if (avctx->internal->draining) {
return AVERROR_EOF;
} else {
return AVERROR(EAGAIN);
}
return 0;
}
2.4 encode
nvenc沒有實(shí)現(xiàn)encode接口,這里參考libavcodec/libx264.c的實(shí)現(xiàn),libx264的流程比較繁瑣,總結(jié)為流程圖如下,x264_encoder_encode為非阻塞接口,內(nèi)部存在yuv的拷貝,調(diào)用后不一定會(huì)獲取到一幀編碼好的碼流,但獲取到之后,同樣需要拷貝到輸出pkt中

2.5 零拷貝的設(shè)計(jì)
通過以上分析,發(fā)現(xiàn)兩種編碼器的實(shí)現(xiàn)都存在拷貝,下面分析零拷貝實(shí)現(xiàn)的可能性
首先是輸入零拷貝,輸入yuv是外部申請的,編碼器只是使用,對于一個(gè)阻塞的編碼器(即送幀后需要阻塞等待該幀編碼完成),這個(gè)設(shè)計(jì)是相對簡單的,只需要將frame的地址告訴編碼器即可,從編碼開始到結(jié)束只有一個(gè)yuv buffer,編碼完成后意味這一幀也消耗完了;如果是非阻塞的編碼器涉及多個(gè)buffer緩存在編碼器中,該設(shè)計(jì)過于復(fù)雜此處不討論
然后是輸出零拷貝,輸出的碼流buffer是編碼器自己申請的,要實(shí)現(xiàn)零拷貝,上層使用完畢之后就需要將該buffer還給編碼器,參考FFmpeg的example是有這個(gè)動(dòng)作的,即調(diào)用unref減引用計(jì)數(shù)
void av_packet_unref(AVPacket *pkt)
AVPacket中實(shí)際的碼流buffer在buf成員中
typedef struct AVPacket {
/**
* A reference to the reference-counted buffer where the packet data is
* stored.
* May be NULL, then the packet data is not reference-counted.
*/
AVBufferRef *buf;
...
} AVPacket;
該接口將buf的引用計(jì)數(shù)減到零之后,會(huì)進(jìn)行釋放操作,對于AVBufferRef而言,釋放操作是可以定制的,只需要將free賦值即可
struct AVBuffer {
...
void (*free)(void *opaque, uint8_t *data);
...
};
FFmpeg有相關(guān)接口可以生成一個(gè)定制的AVBufferRef
AVBufferRef *av_buffer_create(uint8_t *data, size_t size,
void (*free)(void *opaque, uint8_t *data),
void *opaque, int flags)
這里data是已經(jīng)分配好的buffer的地址,size是已經(jīng)分配的buffer的大小,free是對應(yīng)的釋放函數(shù)
因此,輸出buffer零拷貝可以這樣實(shí)現(xiàn),通過相關(guān)編碼器接口獲取到一包碼流之后,通過av_buffer_create來生成AVBufferRef,傳入的是這包碼流的地址和大小,注冊free函數(shù)為還碼流buffer給編碼器的函數(shù),將生成的AVBufferRef賦值到AVPacket中返回給上層,上層使用完畢后,調(diào)用av_packet_unref即可向編碼器還碼流。
到此這篇關(guān)于C++中使用FFmpeg適配自定義編碼器的實(shí)現(xiàn)方法的文章就介紹到這了,更多相關(guān)C++ FFmpeg適配編碼器內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C語言實(shí)現(xiàn)數(shù)據(jù)結(jié)構(gòu)迷宮實(shí)驗(yàn)
這篇文章主要為大家詳細(xì)介紹了C語言實(shí)現(xiàn)數(shù)據(jù)結(jié)構(gòu)迷宮實(shí)驗(yàn),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-03-03
C/C++實(shí)現(xiàn)快速排序算法的兩種方式實(shí)例
快速排序是一種采用分治思想,在實(shí)踐中通常運(yùn)行較快一種排序算法,這篇文章主要給大家介紹了關(guān)于C/C++實(shí)現(xiàn)快速排序的兩種方式的相關(guān)資料,文中給出了詳細(xì)的示例代碼,需要的朋友可以參考下2021-08-08
一篇文章帶你入門C語言數(shù)據(jù)結(jié)構(gòu):緒論
這篇文章主要介紹了C語言的數(shù)據(jù)解構(gòu)基礎(chǔ),希望對廣大的程序愛好者有所幫助,同時(shí)祝大家有一個(gè)好成績,需要的朋友可以參考下,希望能給你帶來幫助2021-08-08
基于Qt實(shí)現(xiàn)簡易GIF播放器的示例代碼
這篇文章主要介紹了如何利用Qt設(shè)計(jì)一個(gè)簡易GIF播放器,可以播放GIF動(dòng)畫。其基本功能有載入文件、播放、暫停、停止、快進(jìn)和快退,感興趣的可以了解一下2022-06-06
C語言結(jié)構(gòu)體成員賦值的深拷貝與淺拷貝詳解
C語言中的淺拷貝是指在拷貝過程中,對于指針型成員變量只拷貝指針本身,而不拷貝指針?biāo)赶虻哪繕?biāo),它按字節(jié)復(fù)制的。深拷貝除了拷貝其成員本身的值之外,還拷貝成員指向的動(dòng)態(tài)內(nèi)存區(qū)域內(nèi)容。本文將通過示例和大家詳細(xì)說說C語言的深拷貝與淺拷貝,希望對你有所幫助2022-09-09
C語言實(shí)現(xiàn)飛機(jī)售票系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了C語言實(shí)現(xiàn)飛機(jī)售票系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-05-05

