Read OSS

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:デコード済みデータのコンテナ

AVFrameAVPacket の対となる存在で、デコード済みの非圧縮データを保持します。ビデオとオーディオの両方に対応した一つの構造体で、それぞれのフィールドが用意されています。型とサイドデータの 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_dataAV_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のほぼすべてのコンテキスト構造体——AVCodecContextAVFormatContextAVFilterContext など——の先頭フィールドには 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 を持ちます。オプションは子へ自動的に伝播させることができます。

ヒント: 新しいコーデックやフィルターを実装するとき、設定可能なパラメーターを公開する方法が AVClassAVOption テーブルの定義です。パース、バリデーション、シリアライゼーションはすべてフレームワークが処理してくれます。

パブリック/プライベート構造体の分割:詳細

第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

このパターンは複数レベルに入れ子になっています。FormatContextInternalFFFormatContext を埋め込み、FFFormatContextAVFormatContext を埋め込みます。DecodeContextAVCodecInternal を埋め込みます。各レベルがキャストチェーンを保持しつつ、さらなるプライベート状態を追加しています。

たとえば、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_typelibavcodec/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-L247cb 共用体は、六種類の関数ポインタ型のいずれか一つを保持します。

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.cencode.c のディスパッチロジックはこのフィールドを確認し、適切な共用体メンバーを呼び出します。libavcodec/codec_internal.h#L347-L370 にある FF_CODEC_DECODE_CB(func)FF_CODEC_ENCODE_CB(func) といった便利マクロは、cb_type 判別子とコールバックポインタの両方を単一のイニシャライザでセットします。これにより不整合を防ぐことができます。

DECODERECEIVE_FRAME の違いは重要です。同期コーデックは decode() を実装し、1パケットを受け取って1フレームを生成します。一方、ハードウェアアクセラレーション型のような非同期コーデックは receive_frame() を実装し、内部でパケットを引き込みます。独自のバッファリングを管理するため、パケットの到着レートとは異なるレートでフレームを生成することがあります。

次回予告

AVBuffer による参照カウントメモリ管理、パイプラインの共通言語としての AVPacketAVFrame、リフレクションのための AVClass、そしてコールバックディスパッチの仕組み——これらのデータ構造が身についたところで、いよいよ実際の使われ方を追いかけましょう。

第3回では、libavcodec におけるデコード・エンコードの完全なパスをトレースし、libavformat でのデマックス/マックスのライフサイクルを追い、さらにFFmpegと外部世界をつなぐ階層型I/Oプロトコルスタックを探っていきます。