読者です 読者をやめる 読者になる 読者になる

/dev/null/onishy

どーも。

VLC弄ってみた#1 - VLCの仕組み

初回の今回はVLCの内部構造について分析したことを紹介します。大学のレポートを兼ねた記事なので少し詳しすぎめに書きます。

tarballはこちらから入手でき、解析にはversion 2.2.1を用いました。

役に立つ資料

逆にこれくらいしか役に立つ資料がない…

VLCのスレッド管理

VLCは高度にマルチスレッド化されており、例えば、重要なスレッドとしては以下が挙げられます。

  • Playlist: src/playlist/thread.c
    • 再生リストを管理するスレッド。
  • Video Output: src/video_output/video_output.c
    • 動画の出力を行うスレッド。
  • Decoder: src/input/decoder.c
    • 動画のデコードを行うスレッド。フィルタ管理も行う。
  • Qtなどの描画処理を行うスレッド。

当然、実際には音声関連やネットワーク関連など、もっと多くのスレッドが動いていますが、さすがに追いきれません。

vlcでは、スレッドの生成はvlc_cloneという関数を通じて行われます。詳しく調べたい場合はこの関数を追うと良いでしょう。

なお、VLCマルチプラットフォームに対応しているため、スレッド管理やGUI回りでは、OSごとに違う関数が呼ばれているので注意しましょう。

VLCの動画処理

上のスレッドでも触れた4つのスレッドが、動画処理の肝となるスレッドです。

VLCが動画を処理する流れは次のようになっています。

  1. Playlistから動画が読み込まれる。
    • GUIからの再生/停止の信号処理、再生管理はこいつが行う。
  2. Decoderスレッドがffmpegを利用して動画をどんどんデコードする。
    • デコードされたフレームはpicture_fifoにpushする。
  3. VideoOutputのスレッドがDecodeと並行して動画を出力する。
    • フレームはfifoからpopされる。
    • この際、フレームにタグ付けられた時間情報から、出力されるべきフレームかどうかが決定される。

基本的にやっていることはこれだけです。

ここで、picture_fifoは何かというと、名前の通りデコードされた画像のFIFOです。デコードされた順にpushされ、再生する際にpopされます。実装はpicture_fifo.cに書かれています。

fifoと似た構造体に、picture_poolというものもあります。こちらはpicture_pool.cに実装があります。画像をまとめて管理したり、mutexやgc的な処理を行ったりしているような雰囲気も出していますが、残念ながらあまり直接触る機会はなく、深入りはしませんでした。

構造体

VLCでは独自の構造体が数多く定義されています。現代であれば、恐らくライブラリを使って補うような部分も、全て自前で書いてあります。これがVLCを複雑なソフトウェアにしている一因となっています。

構造体は死ぬほど定義されていますが、ここでは特によく見かける(興味深い)構造体を挙げておきます。

vlc_object_t: オブジェクト

vlc_common.hで定義されていて、主な役割はvlc_variables.hを見るとわかります。

VLCは2001年に開発されたソフトウェアで、当時はC++が主流でなかったのか分かりませんが、コアはC言語で書かれています。しかしこれだけ大規模なソフトウェアとなると、オブジェクト指向なしに記述するのは至難の業です。

そこで、VLCでは何を血迷ったか、独自でオブジェクト指向的なクラスを書き上げてしまいました。構造体ではprivateなメンバやメンバ関数が持てないので、苦肉の策として書いたのでしょう。

恐らく当時はワッショイワッショイ言いながら使われていたのでしょうが、今となっては少々使いにくいと言わざるを得ません。

vlc_object_tは型と名前の文字列などを与えられていて、実際の中身は次のようなunionで定義されるvlc_value_tです。

/**
 * VLC value structure
 */
typedef union
{
    int64_t         i_int;
    bool            b_bool;
    float           f_float;
    char *          psz_string;
    void *          p_address;
    vlc_object_t *  p_object;
    vlc_list_t *    p_list;
    mtime_t         i_time;
    struct { int32_t x; int32_t y; } coords;

} vlc_value_t;

vlc_variables.hには、それぞれの型に対応する関数がずらりと並べられていて壮観です。変数関連の関数には、名前空間としてvar_のプレフィクスが与えられています。基本的に、VLCの変数はこのオブジェクトによって管理されています。

block_t, picture_t: 画像関連

VLCでは、動画ファイルをブロック単位で読み込み、フレーム単位で出力します。そのために用意された構造体がblock_tやpicture_tです。

block_tは汎用的なブロックを表す構造体で、vlc_block.hで次のように定義されています。

struct block_t
{
    block_t    *p_next;

    uint8_t    *p_buffer; /**< Payload start */
    size_t      i_buffer; /**< Payload length */
    uint8_t    *p_start; /**< Buffer start */
    size_t      i_size; /**< Buffer total size */

    uint32_t    i_flags;
    unsigned    i_nb_samples; /* Used for audio */

    mtime_t     i_pts;
    mtime_t     i_dts;
    mtime_t     i_length;

    /* Rudimentary support for overloading block (de)allocation. */
    block_free_t pf_release;
};

見ての通り、線形リストで、pts/dts[用語解説]を含んでいるため、エンコード/デコード情報を格納することができます。これらのデータをブロック単位で管理したり、送受する場合に使われる構造体です。

picture_tは、その名の通り画像を格納します。こちらはデコード済みの画像限定で、このオブジェクト1つで画像の表示に必要な情報は全て揃っています。

/**
 * Video picture
 */
struct picture_t
{
    /**
     * The properties of the picture
     */
    video_frame_format_t format;

    plane_t         p[PICTURE_PLANE_MAX];     /**< description of the planes */
    int             i_planes;                /**< number of allocated planes */

    /** \name Picture management properties
     * These properties can be modified using the video output thread API,
     * but should never be written directly */
    /**@{*/
    mtime_t         date;                                  /**< display date */
    bool            b_force;
    /**@}*/

    /** \name Picture dynamic properties
     * Those properties can be changed by the decoder
     * @{
     */
    bool            b_progressive;          /**< is it a progressive frame ? */
    bool            b_top_field_first;             /**< which field is first */
    unsigned int    i_nb_fields;                  /**< # of displayed fields */
    void          * context;          /**< video format-specific data pointer,
             * must point to a (void (*)(void*)) pointer to free the context */
    /**@}*/

    /** Private data - the video output plugin might want to put stuff here to
     * keep track of the picture */
    picture_sys_t * p_sys;

    /** This way the picture_Release can be overloaded */
    struct
    {
        atomic_uintptr_t refcount;
        void (*pf_destroy)( picture_t * );
        picture_gc_sys_t *p_sys;
    } gc;

    /** Next picture in a FIFO a pictures */
    struct picture_t *p_next;
};

基本的にOpenCVのIplImageのようなものを想像すると良いです。 ただ、IplImageにそのままキャストできるほどの互換性はありません。これについては次回以降に詳しく説明する予定です。

用語解説

  • PTS/DTS
    • PTSはPresentation Time Stampの略で、動画が実際に再生されるべきタイミングを表す。
    • DTSはDecode Time Stampの略で、動画がデコードされたタイミングを表す。
  • I/P/B frame
    • 動画ファイルのフレームを3種類に分ける。
      • ffmpegなどで一般に用いられている手法。
    • ざっくり言うと、全ての画像情報が含まれたIフレームの間に、圧縮率の異なる2つのフレームが存在して、PフレームBフレームと呼ばれている。圧縮率はB > Pで、Pフレームはひとつ前のPまたはIフレームからの差分を持っている。BフレームPフレームの間を補完するための情報を持っていて、ひとつ前と後のどちらのフレームの情報も参照する。
    • こうすることで、例えば極端に処理速度の遅いコンピュータではIフレームのみ、余裕があればPフレームBフレームをデコードするようにすれば、スペックに応じたクオリティのデコードができる仕組みになっているらしい。(from Wikipedia)
    • decoder_synchro.cに詳細が書いてある。
/*
 * DISCUSSION : How to Write an efficient Frame-Dropping Algorithm
 * ==========
 *
 * This implementation is based on mathematical and statistical
 * developments. Older implementations used an enslavement, considering
 * that if we're late when reading an I picture, we will decode one frame
 * less. It had a tendancy to derive, and wasn't responsive enough, which
 * would have caused trouble with the stream control stuff.
 *
 * 1. Structure of a picture stream
 *    =============================
 * Between 2 I's, we have for instance :
 *    I   B   P   B   P   B   P   B   P   B   P   B   I
 *    t0  t1  t2  t3  t4  t5  t6  t7  t8  t9  t10 t11 t12
 * Please bear in mind that B's and IP's will be inverted when displaying
 * (decoding order != presentation order). Thus, t1 < t0.
 *
 * 2. Definitions
 *    ===========
 * t[0..12]     : Presentation timestamps of pictures 0..12.
 * t            : Current timestamp, at the moment of the decoding.
 * T            : Picture period, T = 1/frame_rate.
 * tau[I,P,B]   : Mean time to decode an [I,P,B] picture.
 * tauYUV       : Mean time to render a picture (given by the video_output).
 * tau´[I,P,B] = 2 * tau[I,P,B] + tauYUV
 *              : Mean time + typical difference (estimated to tau/2, that
 *                needs to be confirmed) + render time.
 * DELTA        : A given error margin.
 *
 * 3. General considerations
 *    ======================
 * We define three types of machines :
 *      14T > tauI : machines capable of decoding all I pictures
 *      2T > tauP  : machines capable of decoding all P pictures
 *      T > tauB   : machines capable of decoding all B pictures
 *
 * 4. Decoding of an I picture
 *    ========================
 * On fast machines, we decode all I's.
 * Otherwise :
 * We can decode an I picture if we simply have enough time to decode it
 * before displaying :
 *      t0 - t > tau´I + DELTA
 *
 * 5. Decoding of a P picture
 *    =======================
 * On fast machines, we decode all P's.
 * Otherwise :
 * First criterion : have time to decode it.
 *      t2 - t > tau´P + DELTA
 *
 * Second criterion : it shouldn't prevent us from displaying the forthcoming
 * I picture, which is more important.
 *      t12 - t > tau´P + tau´I + DELTA
 *
 * 6. Decoding of a B picture
 *    =======================
 * On fast machines, we decode all B's. Otherwise :
 *      t1 - t > tau´B + DELTA
 * Since the next displayed I or P is already decoded, we don't have to
 * worry about it.
 *
 * I hope you will have a pleasant flight and do not forget your life
 * jacket.
 *                                                  --Meuuh (2000-12-29)
 */

要するに、処理速度に応じてどのフレームをデコードするかを決めているという話。

何か思い出したら書き足します。