您好,登錄后才能下訂單哦!
本篇內容主要講解“C++中怎么使用FFmpeg適配自定義編碼器”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“C++中怎么使用FFmpeg適配自定義編碼器”吧!
FFmpeg是一個開源的多媒體框架,底層可對接實現多種編解碼器,下面參考文件doc/examples/encode_video.c
分析編碼一幀的流程
統一的編碼流程如下圖所示
FFmpeg使用的是引用計數的思想,對于一塊buffer,剛申請時引用計數為1,每有一個模塊進行使用,引用計數加1,使用完畢后引用計數減1,當減為0時釋放buffer。
此流程中需要關注buffer的分配,對于編碼器來說,輸入buffer是yuv,也就是上圖中的frame,輸出buffer是碼流包,也就是上圖中的pkt,下面對這兩個buffer進行分析
frame:這個結構體是由av_frame_alloc
分配的,但這里并沒有分配yuv的內存,yuv內存是av_frame_get_buffer
分配的,可見這里輸入buffer完全是來自外部的,不需要編碼器來管理,編碼器只需要根據所給的yuv地址來進行編碼就行了
pkt:這個結構體是由av_packet_alloc
分配的,也沒有分配碼流包的內存,可見這里pkt僅僅是一個引用,pkt直接傳到了avcodec_receive_packet
接口進行編碼,完成之后將pkt中碼流的內容寫到文件,最后調用av_packet_unref
接口減引用計數,因此這里pkt是編碼器內部分配的,分配完成之后會減pkt的引用計數加1,然后輸出到外部,外部使用完畢之后再減引用計數來釋放buffer
編碼一幀的相關代碼如下:
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表示編碼器內部已經沒有碼流。
此處分析編碼一幀的內部流程,首先看FFmpeg內部編碼器的上下文,其中有三個重要結構體
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;
下面結合送幀和收流的接口進行介紹
avcodec_send_frame: 送幀接口,將yuv的幀信息賦值到buffer_frame
,然后觸發一幀編碼,將編碼出的碼流賦值到buffer_pkt
avcodec_receive_packet: 收流接口,檢查上下文中是否有已經編碼好的碼流buffer_pkt
,如果有則將其返回,如果沒有再觸發一幀編碼,將編碼好的碼流返回
可見send和receive接口均可觸發一幀編碼,此處觸發一幀編碼分為兩個流程,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流程,則直接調用receive_packet
接口的回調,該接口中注冊定制編碼器的接口,完成一幀編碼。如果是simple流程,則調用的是encode_simple_receive_packet
,這是FFmpeg封裝的一個簡易流程,其中調用的是encode
接口,代碼片段如下,詳細分析可參考文章:
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流程中會把buffer_frame
的引用拷貝到in_frame
,然后將in_frame
送幀編碼,意味著其內部只能緩存一幀,不支持多幀緩存。并且simple流程中,調用send之后,如果調用receive成功獲取到一包碼流,下一次調用receive將會返回EAGAIN,且不會調用encode接口,因此對于不支持多幀緩存的編碼器而言,如果send一幀后,需要receive兩包碼流,那么獲取到一包碼流之后receive接口會返回EAGAIN,循環退出進行下一次send,此時上一幀未編碼的yuv會被覆蓋
receive流程中沒有該限制,直接調用了receive_packet
接口,因此如果需要在ffmpeg適配層做多幀緩存,可以使用receive
的流程。另外receive流程沒有上述限制,在成功收到一幀碼流之后,仍然會調用receive,比較靈活,可以做一些定制化的操作
適配接口參考ffmpeg/libavcodec/nvenc_h364.c
,這是英偉達的硬件編碼器接口,自定義一個編碼器只需實現以下結構體
const FFCodec ff_h364_nvenc_encoder = { .p.name = "h364_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 = &h364_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, };
這里面最重要三個接口是init、close和receive,還有一個比較重要的數據結構是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 h364_nvenc_class = { .class_name = "h364_nvenc", .item_name = av_default_item_name, .option = options, .version = LIBAVUTIL_VERSION_INT, };
init是初始化編碼器的接口,在avcodec_open2
中調用,定義接口如下,此接口一般是根據用戶的option配置,來對編碼器進行相應的初始化
int (*init)(struct AVCodecContext *)
close是關閉編碼器的接口,在avcodec_free_context
中調用,定義接口如下,該接口完成編碼器內部的一些資源釋放操作
int (*close)(struct AVCodecContext *)
每個編碼器有一個自定義的上下文,其作用是在編碼器初始化之前對上下文進行配置,編碼器初始化的時候就可以按照用戶的配置來初始化,以nvenc為例該上下文的定義為
ypedef struct NvencContext { ... // 隊列相關的定義 ... // 編碼相關的配置信息 int preset; int profile; int level; int tier; int rc; int cbr; ... } NvencContext;
該上下文在avcodec內部使用,對外不可見,因此需要option的方式開放對外配置的接口,使用一個AVOption
來描述一個編碼器的配置
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;
其中關鍵的是offset
和type
成員,offset
描述了這個option在上下文中的偏移量,type
描述了成員占據的長度,有這兩個信息就可以在不對外暴露內部上下文的情況下,修改其中的值,用戶配置option的示例如下
av_opt_set(c->priv_data, "preset", "slow", 0);
nvenc在avcodec層實現了多幀緩存,因此他實現的是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; // 這個是init中申請的 if (!frame->buf[0]) { // 將buffer_frame引用拷貝到frame中 res = ff_encode_get_frame(avctx, frame); if (res < 0 && res != AVERROR_EOF) return res; } // 編碼一幀,推測是阻塞的,nv相關的函數沒有找到介紹,其中存在拷貝 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隊列中取編碼好的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隊列 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; }
nvenc沒有實現encode接口,這里參考libavcodec/libx264.c
的實現,libx264的流程比較繁瑣,總結為流程圖如下,x264_encoder_encode為非阻塞接口,內部存在yuv的拷貝,調用后不一定會獲取到一幀編碼好的碼流,但獲取到之后,同樣需要拷貝到輸出pkt中
通過以上分析,發現兩種編碼器的實現都存在拷貝,下面分析零拷貝實現的可能性
首先是輸入零拷貝,輸入yuv是外部申請的,編碼器只是使用,對于一個阻塞的編碼器(即送幀后需要阻塞等待該幀編碼完成),這個設計是相對簡單的,只需要將frame的地址告訴編碼器即可,從編碼開始到結束只有一個yuv buffer,編碼完成后意味這一幀也消耗完了;如果是非阻塞的編碼器涉及多個buffer緩存在編碼器中,該設計過于復雜此處不討論
然后是輸出零拷貝,輸出的碼流buffer是編碼器自己申請的,要實現零拷貝,上層使用完畢之后就需要將該buffer還給編碼器,參考FFmpeg的example是有這個動作的,即調用unref減引用計數
void av_packet_unref(AVPacket *pkt)
AVPacket
中實際的碼流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
的引用計數減到零之后,會進行釋放操作,對于AVBufferRef
而言,釋放操作是可以定制的,只需要將free賦值即可
struct AVBuffer { ... void (*free)(void *opaque, uint8_t *data); ... };
FFmpeg有相關接口可以生成一個定制的AVBufferRef
AVBufferRef *av_buffer_create(uint8_t *data, size_t size, void (*free)(void *opaque, uint8_t *data), void *opaque, int flags)
這里data
是已經分配好的buffer的地址,size
是已經分配的buffer的大小,free
是對應的釋放函數
因此,輸出buffer零拷貝可以這樣實現,通過相關編碼器接口獲取到一包碼流之后,通過av_buffer_create
來生成AVBufferRef
,傳入的是這包碼流的地址和大小,注冊free函數為還碼流buffer給編碼器的函數,將生成的AVBufferRef
賦值到AVPacket
中返回給上層,上層使用完畢后,調用av_packet_unref
即可向編碼器還碼流。
到此,相信大家對“C++中怎么使用FFmpeg適配自定義編碼器”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。