FFmpegのコアデータ構造:AVFrame、AVPacket、AVBuffer、そしてAVClassシステム
前提知識
- ›第1回:アーキテクチャとコードベースのナビゲーション
- ›Cプログラミング:構造体、共用体、関数ポインタ、ビットフィールド
- ›参照カウントの基本的な理解
FFmpegのコアデータ構造:AVFrame、AVPacket、AVBuffer、そしてAVClassシステム
メディア処理パイプラインが本質的にやることは2つです。データをステージ間で受け渡し、そのデータを変換する。FFmpegでは、この「データ」はちょうど2つの形態をとります。圧縮データを運ぶ AVPacket と、非圧縮データを保持する AVFrame です。どちらも AVBuffer という参照カウント層の上に構築されており、ゼロコピーでのデータ共有を実現しています。そして、ロギング・オプション処理・親子コンテキスト関係といった仕組みを束ねているのが、AVClass/AVOption によるリフレクションシステムです。
これら4つの型を理解することは不可欠です。プロジェクト内のすべてのデマクサー、デコーダー、フィルター、エンコーダー、マクサーは、これらの型を共通言語として使っています。本記事では、それぞれについて詳しく解説します。
AVBuffer と AVBufferRef:参照カウントの土台
FFmpegは膨大な量のデータを処理します。非圧縮の4Kビデオフレーム1枚だけで、おおよそ12 MBになります。これをパイプラインのステージ間でコピーし続けると、パフォーマンスは壊滅的なことになります。この問題を解決するのが AVBuffer です。libavutil/buffer.h#L31-L95 で定義された参照カウント付きバッファシステムです。
設計上、バッファ本体とバッファへの参照は分離されています。
AVBuffer— 実際のデータバッファ。不透明(opaque)であり、直接アクセスすることはないAVBufferRef— AVBufferへの参照。実際に操作するのはこちらである
typedef struct AVBufferRef {
AVBuffer *buffer;
uint8_t *data; // The data buffer
size_t size; // Size of data in bytes
} AVBufferRef;
ライフサイクルはシンプルです。
sequenceDiagram
participant Caller
participant AVBuffer
participant Ref1 as AVBufferRef #1
participant Ref2 as AVBufferRef #2
Caller->>AVBuffer: av_buffer_alloc(size)
AVBuffer->>Ref1: Returns initial reference (refcount=1)
Caller->>Ref1: av_buffer_ref(ref1)
Ref1->>Ref2: Creates new reference (refcount=2)
Caller->>Ref2: av_buffer_unref(&ref2)
Note over AVBuffer: refcount=1, buffer lives
Caller->>Ref1: av_buffer_unref(&ref1)
Note over AVBuffer: refcount=0, buffer freed
重要な不変条件があります。バッファへの参照がちょうど1つのときに限り、そのバッファは書き込み可能とみなされます。これを確認するのが av_buffer_is_writable() です。パイプラインのあるステージが共有されている可能性のあるデータを変更したい場合、av_buffer_make_writable() を呼び出します。参照カウントが1より大きければコピーが作られる——これはいわゆるコピーオンライト (copy-on-write) パターンです。
ヒント: 同じバッファへの異なる参照から取得した
AVBufferRef.dataポインタが同じ値を指している必要はありません。参照はバッファの途中を指すことができます。たとえば、デマクサーが大きなブロックを読み込み、その中の異なるオフセットを指すパケット参照を複数作成する、といった使われ方をします。
FFmpegはさらに AVBufferPool も提供しています。同サイズのバッファを再利用するプールアロケーターです。高いレートでフレームバッファを確保・解放するデコーダーにとって、これは非常に重要です。プーリングがなければ、アロケーションのオーバーヘッドがボトルネックになってしまいます。
AVPacket:エンコード済みデータのコンテナ
AVPacket は、デマクサーとデコーダーの間(またはエンコーダーとマクサーの間)で圧縮・エンコード済みのデータを運びます。libavcodec/packet.h で定義されており、主要なフィールドは次の通りです。
classDiagram
class AVPacket {
+buf: AVBufferRef*
+data: uint8_t*
+size: int
+pts: int64_t
+dts: int64_t
+duration: int64_t
+stream_index: int
+flags: int
+side_data: AVPacketSideData*
+side_data_elems: int
}
class AVBufferRef {
+buffer: AVBuffer*
+data: uint8_t*
+size: size_t
}
AVPacket --> AVBufferRef : buf
data/size のペアが実際のエンコード済みビットストリームを保持します。buf フィールドが NULL でない場合、データが参照カウントされていることを示します。av_read_frame() が返すパケットは常にこの状態です。
タイムスタンプはストリーム固有のタイムベースによる int64_t 値です。pts はプレゼンテーションタイムスタンプ(表示するタイミング)、dts はデコードタイムスタンプ(デコードするタイミング)です。Bフレームを使うコーデックでは、表示順とは異なる順序でデコードが必要なため、DTSとPTSが異なる値になることがあります。
参照のセマンティクスは次の通りです。
av_packet_ref()— 新しい参照を作成します(バッファの参照カウントをインクリメント)av_packet_unref()— 参照を解放しますav_packet_move_ref()— 参照カウントに触れることなく所有権を移譲します
サイドデータは、メインデータに付随するコーデック固有のメタデータです。字幕テキスト、パレット更新、暗号化パラメータ、スキップカウントなどが含まれます。
AVFrame:デコード済みデータのコンテナ
AVFrame は AVPacket の対となる存在で、デコード済みの非圧縮データを保持します。ビデオとオーディオの両方に対応した一つの構造体で、それぞれのフィールドが用意されています。型とサイドデータの enum は libavutil/frame.h#L40-L263 から始まります。
ビデオの主要フィールドは次の通りです。
data[AV_NUM_DATA_POINTERS]— ピクセルデータプレーンへのポインタ(YUVならY・U・V、RGBならR・G・B・A)linesize[AV_NUM_DATA_POINTERS]— 各プレーンのストライド(1行あたりのバイト数)width,height— フレームの寸法format— ピクセルフォーマット(enum AVPixelFormat)
オーディオの主要フィールドは次の通りです。
data[AV_NUM_DATA_POINTERS]— サンプルデータへのポインタ(プラナーフォーマットではチャンネルごとに1つ)nb_samples— チャンネルあたりのオーディオサンプル数format— サンプルフォーマット(enum AVSampleFormat)ch_layout— チャンネルレイアウト(AVChannelLayout)extended_data—AV_NUM_DATA_POINTERSを超えるプレーンが必要な場合(例:9チャンネル以上のオーディオ)に、拡張配列を指します
AVFrame の各データプレーンは、buf[] 配列内のそれぞれの AVBufferRef によってバッキングされています。つまり、各プレーンを個別に共有することが可能です。YUVフレームの輝度プレーン (luma) だけを変更するフィルターであれば、色差プレーン (chroma) は参照を共有したまま使い回せます。
classDiagram
class AVFrame {
+data[8]: uint8_t*
+linesize[8]: int
+buf[8]: AVBufferRef*
+extended_data: uint8_t**
+width: int
+height: int
+nb_samples: int
+format: int
+pts: int64_t
+duration: int64_t
+ch_layout: AVChannelLayout
+side_data: AVFrameSideData**
}
class AVBufferRef {
+buffer: AVBuffer*
+data: uint8_t*
+size: size_t
}
class AVFrameSideData {
+type: AVFrameSideDataType
+data: uint8_t*
+size: size_t
+buf: AVBufferRef*
}
AVFrame "1" --> "0..8" AVBufferRef : buf[]
AVFrame "1" --> "0..*" AVFrameSideData : side_data
データフロー:AVPacket → デコード → AVFrame → エンコード → AVPacket
この二つのコア型は、パイプライン全体を流れていきます。AVBuffer によるゼロコピー共有を伴いながら、どのように移動するかを見てみましょう。
flowchart LR
subgraph Demux
D[Demuxer] -->|AVPacket| P1[Encoded Data]
end
subgraph Decode
P1 -->|avcodec_send_packet| DEC[Decoder]
DEC -->|avcodec_receive_frame| F1[AVFrame]
end
subgraph Filter
F1 -->|buffersrc| FG[Filter Graph]
FG -->|buffersink| F2[AVFrame]
end
subgraph Encode
F2 -->|avcodec_send_frame| ENC[Encoder]
ENC -->|avcodec_receive_packet| P2[AVPacket]
end
subgraph Mux
P2 -->|av_interleaved_write_frame| M[Muxer]
end
あらゆる境界において、参照カウントが不要なコピーを防いでいます。デマクサーがパケットを生成するとき、デコーダーは同じ内部バッファへの参照を受け取るだけです。デコーダーがフレームを出力するとき、フレームのデータプレーンはデコーダー内部のバッファプールを直接指していることさえあります。フィルターは参照を移譲または共有することでフレームを下流へ渡します。
データが実際にコピーされるのは、参照が2つ以上あるデータをステージが変更しようとするときだけです。これが av_buffer_make_writable() のコピーオンライト保証です。
AVClass/AVOption リフレクションシステム
FFmpegのほぼすべてのコンテキスト構造体——AVCodecContext、AVFormatContext、AVFilterContext など——の先頭フィールドには AVClass ポインタが置かれています。この一見些細な設計上の決定が、三つの強力な機能を実現しています。構造化されたロギング、リフレクティブなオプション探索、そして親子コンテキスト階層です。
AVClass 構造体は libavutil/log.h#L76-L177 で定義されています。
typedef struct AVClass {
const char* class_name;
const char* (*item_name)(void* ctx);
const struct AVOption *option;
int version;
int log_level_offset_offset;
int parent_log_context_offset;
AVClassCategory category;
// ... more fields
} AVClass;
classDiagram
class AVClass {
+class_name: const char*
+item_name(): const char*
+option: AVOption*
+category: AVClassCategory
+child_next(): void*
+child_class_iterate(): AVClass*
}
class AVCodecContext {
+av_class: AVClass*
+codec: AVCodec*
+width: int
+height: int
}
class AVFormatContext {
+av_class: AVClass*
+iformat: AVInputFormat*
+oformat: AVOutputFormat*
}
class AVFilterContext {
+av_class: AVClass*
+filter: AVFilter*
+name: char*
}
AVCodecContext --> AVClass : first field
AVFormatContext --> AVClass : first field
AVFilterContext --> AVClass : first field
構造化ロギング: av_log(ctx, AV_LOG_ERROR, "something failed") を呼び出すと、FFmpegは AVClass ポインタを使ってメッセージにコンテキスト情報を付加します。出力は [libx264 @ 0x5634a812] something failed のような形式になります。item_name コールバックがインスタンス固有の名前を提供し、category がターミナルでの色分けを担います。
オプションリフレクション: option フィールドは、コンテキストの設定可能なパラメーターをすべて記述した AVOption エントリのNULL終端配列を指しています。av_opt_set() / av_opt_get() 関数群を使うと、具体的な構造体レイアウトを知らなくても、実行時に名前でオプションを設定・取得できます。コマンドラインで -codec:v libx264 -preset slow が動作するのも、この仕組みのおかげです。
親子階層: parent_log_context_offset フィールドと child_next/child_class_iterate コールバックがコンテキストのツリーを構成します。AVFormatContext は子として AVCodecContext を持ち、AVFilterGraph は子として AVFilterContext を持ちます。オプションは子へ自動的に伝播させることができます。
ヒント: 新しいコーデックやフィルターを実装するとき、設定可能なパラメーターを公開する方法が
AVClassとAVOptionテーブルの定義です。パース、バリデーション、シリアライゼーションはすべてフレームワークが処理してくれます。
パブリック/プライベート構造体の分割:詳細
第1回ではこのパターンを紹介しました。ここでは主要なサブシステム全体にわたって検証し、なぜこれが機能するのかを理解しましょう。
C標準(§6.7.2.1)は次のことを保証しています。「構造体オブジェクトへのポインタは、適切に変換されたとき、その先頭メンバーを指す。」つまり、FFCodec が最初のメンバーとして AVCodec p を持つなら、(AVCodec*)ffcodec_ptr == &ffcodec_ptr->p が成り立ちます。このキャストは合法であり、定義された動作であり、コストゼロです。
全サブシステムにわたる全体像は次の通りです。
| サブシステム | パブリック | プライベート | ヘッダー |
|---|---|---|---|
| コーデック | AVCodec |
FFCodec |
codec_internal.h |
| デマクサー | AVInputFormat |
FFInputFormat |
demux.h |
| マクサー | AVOutputFormat |
FFOutputFormat |
mux.h |
| フィルター | AVFilter |
FFFilter |
filters.h |
| フィルターコンテキスト | AVFilterContext |
FFFilterContext |
avfilter_internal.h |
| フィルターグラフ | AVFilterGraph |
FFFilterGraph |
avfilter_internal.h |
| デコード状態 | AVCodecInternal |
DecodeContext |
decode.c |
| エンコード状態 | AVCodecInternal |
EncodeContext |
encode.c |
| フォーマット状態 | FFFormatContext |
FormatContextInternal |
avformat_internal.h |
このパターンは複数レベルに入れ子になっています。FormatContextInternal は FFFormatContext を埋め込み、FFFormatContext は AVFormatContext を埋め込みます。DecodeContext は AVCodecInternal を埋め込みます。各レベルがキャストチェーンを保持しつつ、さらなるプライベート状態を追加しています。
たとえば、libavformat/demux.h#L66-L70 のデマクサー分割を見てみましょう。
typedef struct FFInputFormat {
AVInputFormat p;
enum AVCodecID raw_codec_id;
int priv_data_size;
int flags_internal;
int (*read_probe)(const AVProbeData *);
int (*read_header)(struct AVFormatContext *);
int (*read_packet)(struct AVFormatContext *, AVPacket *pkt);
int (*read_close)(struct AVFormatContext *);
// ...
} FFInputFormat;
パブリックな AVInputFormat にはメタデータ(name、long_name、MIMEタイプ、拡張子)しか含まれていません。実際のデマックス処理を行うコールバック——つまり振る舞い——はすべて FFInputFormat の中に隠れています。外部コードは利用可能なフォーマットを列挙してその名前を読むことはできますが、実際のI/O関数には一切アクセスできません。
コーデックのコールバックディスパッチ:cb_type と共用体
構造体分割パターンの最も洗練された応用例が FFCodec です。ここでは、判別共用体 (discriminated union) がすべてのコーデックコールバックシグネチャを処理します。判別子 cb_type は libavcodec/codec_internal.h#L106-L125 で定義されています。
enum FFCodecType {
FF_CODEC_CB_TYPE_DECODE, // synchronous decode
FF_CODEC_CB_TYPE_DECODE_SUB, // subtitle decode
FF_CODEC_CB_TYPE_RECEIVE_FRAME, // pull-based decode
FF_CODEC_CB_TYPE_ENCODE, // synchronous encode
FF_CODEC_CB_TYPE_ENCODE_SUB, // subtitle encode
FF_CODEC_CB_TYPE_RECEIVE_PACKET, // pull-based encode
};
libavcodec/codec_internal.h#L189-L247 の cb 共用体は、六種類の関数ポインタ型のいずれか一つを保持します。
flowchart TD
A["cb_type value"] --> B{Which callback?}
B -->|DECODE| C["cb.decode(avctx, frame, got_frame, pkt)"]
B -->|DECODE_SUB| D["cb.decode_sub(avctx, sub, got_frame, pkt)"]
B -->|RECEIVE_FRAME| E["cb.receive_frame(avctx, frame)"]
B -->|ENCODE| F["cb.encode(avctx, pkt, frame, got_pkt)"]
B -->|ENCODE_SUB| G["cb.encode_sub(avctx, buf, size, sub)"]
B -->|RECEIVE_PACKET| H["cb.receive_packet(avctx, pkt)"]
cb_type フィールドは3ビットのビットフィールドです。800以上のコーデック構造体が存在するため、空間効率は重要です。decode.c と encode.c のディスパッチロジックはこのフィールドを確認し、適切な共用体メンバーを呼び出します。libavcodec/codec_internal.h#L347-L370 にある FF_CODEC_DECODE_CB(func) や FF_CODEC_ENCODE_CB(func) といった便利マクロは、cb_type 判別子とコールバックポインタの両方を単一のイニシャライザでセットします。これにより不整合を防ぐことができます。
DECODE と RECEIVE_FRAME の違いは重要です。同期コーデックは decode() を実装し、1パケットを受け取って1フレームを生成します。一方、ハードウェアアクセラレーション型のような非同期コーデックは receive_frame() を実装し、内部でパケットを引き込みます。独自のバッファリングを管理するため、パケットの到着レートとは異なるレートでフレームを生成することがあります。
次回予告
AVBuffer による参照カウントメモリ管理、パイプラインの共通言語としての AVPacket と AVFrame、リフレクションのための AVClass、そしてコールバックディスパッチの仕組み——これらのデータ構造が身についたところで、いよいよ実際の使われ方を追いかけましょう。
第3回では、libavcodec におけるデコード・エンコードの完全なパスをトレースし、libavformat でのデマックス/マックスのライフサイクルを追い、さらにFFmpegと外部世界をつなぐ階層型I/Oプロトコルスタックを探っていきます。