プログラミング > FFmpeg > MPEG-1デコーダ部ソースコード解析


※上記の広告は60日以上更新のないWIKIに表示されています。更新することで広告が下部へ移動します。

FFmpeg MPEG-1デコーダ部ソースコード解析



デコードが始まるまでの流れ

ffmpeg.cのmain関数を追っていくと、3928行付近に以下のようなav_encode関数を呼び出しているコードがあります。

if (av_encode(output_files, nb_output_files, input_files, nb_input_files,
              stream_maps, nb_stream_maps) < 0)
    av_exit(1);

この関数の内部で、入力ファイルがデコードされ、さらに適切な形式にエンコードされ、ファイル出力されているようです(名前から推測するに)。 av_encode関数の中を追ってみると、さらにoutput_packetという関数が呼ばれています。そのoutput_packet関数の 中(ffmpeg.c 1209行付近)に次のようなコードがあります。

//while we have more to decode or while the decoder did output something on EOF
while (len > 0 || (!pkt && ist->next_pts != ist->pts)) {
    ...
}

コメントを訳すと「デコードする物がまだある間ループもしくはデコーダがEOFを出力するまでループ」とあるので、 このwhileループでMPEG-1のデコードを行っていることが分かります。ループ内は、switch文がありオーディオかビデオで呼ばれる関数が異なっています。MPEG-1はビデオなので、avcodec_decode_video関数が呼ばれます。 avcodec_decode_video関数の中身は単純で、簡単なエラーチェックの後に次のような関数呼び出しがあります(utils.c 529行付近)。

ret = avctx->codec->decode(avctx, picture, got_picture_ptr,
                        buf, buf_size);

ここが、実際のデコードの呼び出しみたいです。avctx->codec->decodeは、関数ポインタになっており、指し示す先は mpeg_decode_frame関数になっています(詳しくコードを追っていませんが、初期化段階で関数ポインタに適切なデコーダの関数がセットされるようです)。したがって、実際にはmpeg_decode_frame関数が呼ばれ、これは名前の通りフレームをデコードする関数です。

ここまでの関数をまとめると、次のようなスタックフレームになっていることが分かります。

  • mpeg_decode_frame関数
  • avcodec_decode_video関数
  • output_packet関数
  • av_encode関数
  • main関数

ここまで来て、ようやくMPEG-1のフレームのデコードが始まります。

MPEG-1フレームのデコード

スタートコード

mpeg_decode_frame関数は、バッファリング調整と画像同期(?)を行った後に、実際のデコードをdecode_chunks関数を呼んでおこなっています。decode_chunks関数は、簡単に書くと次のようなコードになっています(mpeg12.c 2308行付近)。

static int decode_chunks(...)
{
    for(;;) {
        ff_find_start_code(..., &sart_code); //スタートコードを探す
        ...
        switch(start_code) { // スタートコードに応じた処理
        case SEQ_START_CODE: // シーケンス層の場合
            ...
        case ... 
        }
    }
}

MPEG-1のファイルは、[1個目の層のデータ][2個目の層のデータ][3個目の層のデータ]...のように層(処理単位の固まりのようなもの)が並んだ構造になっており、さらに各層の先頭にはスタートコードと呼ばれる16進数で0x000001xxであらわされる数値があります。そしてその数値の後に実際の処理が行われるデータが存在しています。そのためまずはスタートコードを探すことがdecode_chunks関数では行われています。また、スタートコードは番号によって層の種類が決められており、0x000001B3ならシーケンス層、0x000001b8ならGOP層のように決められています。MPEG-1ファイルによって存在したり、しなかったりする層があります。

FFMpegでは、スタート層の検索はff_find_start_code関数で行われています。その中身は単純で、データ列を走査してスタートコードを探しています。

スタートコードを見つけた後の処理

スタートコードを見つけたら、switch文でスタートコードに応じた関数を呼びます。 今回は、[シーケンス層][GOP層][ピクチャ層][スライス層][マクロブロック層]...[マクロブロック層][スライス層][マクロブロック層]...[マクロブロック層]...のように並んでいるデータをデコードした場合を考えてコードを追っていきます。したがって、まずはシーケンス層のスタートコードが読み込まれたとします。

各層を処理するソースコード

シーケンス層

ff_find_start_code関数を呼んだ後の処理は、以下のようになっています(ffmpeg12.c 2351行付近)。

switch(start_code) {
case SEQ_START_CODE: // (= 0x000001B3)
    mpeg1_decode_sequence(avctx, buf_ptr,
                            input_size);
    break;
case PICTURE_START_CODE:
    ...

つまり、スタートコードが0x000001B3ならmpeg_decode_sequence関数が呼ばれます。スタートコードの後には実際のデータが入っており、関数内でそのデータを処理しているみたいです。 mpeg1_decode_sequence関数は次のようになっています(mpeg12.c 1975行付近)。

static int mpeg1_decode_sequence(AVCodecContext *avctx,
                                 const uint8_t *buf, int buf_size)
{
    ...
    width = get_bits(&s->gb, 12);
    height = get_bits(&s->gb, 12);
    if (width <= 0 || height <= 0)
        return -1;
    ...

シーケンス層は、[0x000001B3][12ビットのデータ(画像幅)][12ビットのデータ(画像高さ)]... のようなデータの並びになっており、画像のサイズやフレームレートなどの情報を持っている層になります。 したがって、mpeg1_decode_sequence関数内では、それを順番に読み込んでいってます。get_bits(& s->gb, n)と言う関数は、データをnビット読み込む関数です(中身は高度に最適化されているため、読むのは難しいです)。標準のファイル処理の関数はバイト単位でしか読み込めませんので、FFMpegではビット単位で読み込める関数を自前で作っているみたいです。

必要な情報をすべて読み終えると関数はリターンされ、上で示したfor(;;)のループの先頭に戻り、またff_find_start_code関数でスタートコードを探しに行きます。そして次の層の処理に移ります。次はGOP層であったとします。

GOP層

スタートコードがGOP層を示していた場合は、mpeg_decode_gop関数が呼ばれます。中身はシーケンス層の処理と同様に必要なデータをget_bits関数を使って読み込んでいます。これが終わると再び次のスタートコードの検索を行います。次はピクチャ層であったとします。

横道:ピクチャ構造

ピクチャ層処理のソースコードを見る前に、簡単にMPEG-1の画面のデータ構造について解説しておきます (詳しくは、デジタル放送教科書(上)等の専門書を読んだ方が良いと思いますが)。 MPEG-1の一画面(ピクチャ)の構造は以下のようになります。

下から見ていきますと、一番小さな単位は8x8画素の輝度や色差を表すブロックと呼ばれるものです。 さらにブロックが6個集まりマクロブロックになります。マクロブロックは16x16画素のの輝度と色差を表します。輝度Yは、8x8のブロックが4つ集まり16x16のマクロブロックになりますが、色差CbとCrは間引かれて(4:2:0)いるため、それぞれブロック一個で16x16の色差を表します。マクロブロックを帯状(左上から右下に)に並べたものがスライスになります。 そして最終的にスライスを並べると一枚のピクチャになります。

ピクチャ層

スタートコードがピクチャ層を示していた場合は、mpeg1_docode_picture関数が呼ばれます。 mpeg1_decode_picture関数は次のようになっています。

static int mpeg1_decode_picture(AVCodecContext *avctx,
                                const uint8_t *buf, int buf_size)
{
    Mpeg1Context *s1 = avctx->priv_data;
    MpegEncContext *s = &s1->mpeg_enc_ctx;
    int ref, f_code, vbv_delay;
 
    if(mpeg_decode_postinit(s->avctx) < 0)
       return -2;
 
    init_get_bits(&s->gb, buf, buf_size*8);
 
    ref = get_bits(&s->gb, 10); /* temporal ref */
    s->pict_type = get_bits(&s->gb, 3);
    ...

ピクチャ層は、[0x00000100][10ビットのデータ(時間のリファレンス)][3ビットのデータ(ピクチャタイプ)]... のようなデータの並びになっています。

ピクチャタイプは、今のピクチャがどのように符号化されているかを表します。001bはIピクチャ、010bがPピクチャ、011bはBピクチャになります。Iピクチャは現在のフレームだけを使ってデコードします。今回はIピクチャであったとしてコードを解析していきます。ピクチャタイプ情報はavctx->priv_data->mpeg_enc_ctx->pict_typeにセットしています。

これが終わると再び次のスタートコードの検索を行います。次はスライス層であったとします。

スライス層

スライス層だった場合の処理は、これまでと多少違っているようです。もう一度、スタートコードを繰り返し検索しているfor(;;)の部分を示します(mpeg2.c 2308行付近)。

static int decode_chunks(...)
{
    for(;;) {
        ff_find_start_code(..., &sart_code); //スタートコードを探す
        ...
        switch(start_code) { // スタートコードに応じた処理
        case SEQ_START_CODE:
            ...
        default:
            if (start_code >= SLICE_MIN_START_CODE &&
                start_code <= SLICE_MAX_START_CODE) { // スライス層だった場合
                int mb_y= start_code - SLICE_MIN_START_CODE;
                ...
                if(avctx->thread_count > 1){ // マルチスレッドか
                    ... // こっちはマルチスレッド
                }else{
                   ret = mpeg_decode_slice(s, mb_y, &buf_ptr, input_size);
                   ...
                }
            }
            break;
        } // switchの終わり
    } // forの終わり

スライス層を表すスタートコードは、0x00000101から0x000001AFまでであり、これまでと違い一つの値と言うわけではありません。 そのためFFMpegでは、default部分にスライス層かをチェックするif文を入れてそこで処理を行っているみたいです。 スライスそうだった場合は、まずスライス層の水平位置mb_yを計算しています。これは"スタートコード - 0x00000101"で計算されます。

そこからさらにコードを読み進めると、マルチスレッドかどうかをチェックするコードがあります。スライスのデコードは負荷が高いので、マルチスレッディングで高速化を図っているようです。 ただしマルチスレッドでのスライスの処理はコードが複雑なので、今回はシングルスレッドだった場合のコードを追っていくことにします。シングルスレッドの場合はmpeg_decode_slice関数を呼び出してその関数内で実際の処理を行っています。

(続きは執筆中)