いものやま。

雑多な知識の寄せ集め

rtmpdumpのコードを読んでみた。(その2)

昨日の続き。

今日はOpenResumeFile()とGetLastKeyframe()の詳細を見ていく。

FLVファイルの構造

処理の詳細を見ていく前に、まずはFLVファイルの構造を理解しておいた方がコードが分かりやすいと思う。
(もっとも、以下の説明は仕様書を読んだものではなく、コードを読んで理解したものだけど)

全体の構造

FLVファイルは、まず先頭に13バイトのファイルヘッダがあり、そのあとに複数のデータブロックが並んでいる。

0            13
+-------------+--------------+-- --+--------------+
| file header | data block 1 | ... | data block N |
+-------------+--------------+-- --+--------------+

ファイルヘッダの構造

13バイトのファイルヘッダの構造は、以下。

0                   4                   8                  12
+----+----+----+----+----+----+----+----+----+----+----+----+-----+
|'F' |'L' |'V' |0x01|type|data_offset(=9)    |prev_tag_size(=0)   |
+----+----+----+----+----+----+----+----+----+----+----+----+-----+

先頭の4バイト('F', 'L', 'V', 0x01)は、このファイルがFLVファイルであることを示すためのもの。

typeの1バイトでは、

  • 映像を含むなら0x04
  • 音声を含むなら0x01

のビットが立っている。
(他のビットにも意味があるかもしれないけど、とりあえずコードではそのビットしか見てない)

data_offsetは4バイトのデータで、ビッグエンディアンで整数が格納されている。
ここには、最初のprev_tag_sizeが格納されている位置が入っている。(prev_tag_sizeについては後述)
現状では9という数字が入っているはずなんだけれど、ファイルヘッダが拡張されたときにもprev_tag_sizeの位置が分かるようにしているのだと思う。

prev_tag_sizeは4バイトのデータで、ビッグエンディアンで整数の0が入っている。
このprev_tag_sizeというのは、ファイルを後ろから辿って行くためのデータで、ファイルヘッダの最後と、各データブロックの最後にそれぞれ入ってる。
ファイルを後ろから辿る場合、このデータを後ろから順に読んでいくのだけど、そうなると最後に「ここが先頭ですよ」と知らせる終端が必要になってくる。(C文字列の最後に入っている'\0'みたいなもの)
ファイルヘッダの最後に入っているprev_tag_sizeはまさにこのためのもので、値が0と決まっているので、ファイルの先頭にまでたどり着いたことを知ることが出来る。

データブロックの構造

データブロックの構造は、以下。

N               N+4    N+11               N+prev_tag_size     N+prev_tag_size+4
+----+---+---+---+------+-----------------------+-----+-----+-----+-----+
|type|data_size  | ...  |data (size: data_size) |prev_tag_size          |
+----+---+---+---+------+-----------------------+-----+-----+-----+-----+
|<- data block header ->|<-- data block body -->|<- data block footer ->|

このように、11バイトのデータブロックヘッダと、実際のデータが入っているデータブロックボディ(サイズはヘッダの2〜4バイト目にビッグエンディアンで入ってる)、そして4バイトのデータブロックフッタから構成されている。

ヘッダの1バイト目はブロックの種類を表すもので、

  • 0x12ならメタデータ
  • 0x08なら音声データ
  • 0x09なら映像データ

が入っていることを意味するっぽい。

フッタの4バイトはprev_tag_sizeで、このブロックのヘッダ+ボディのサイズがビッグエンディアンで入っている。
このデータを読んでその分だけファイルポジションを戻せば、このブロックの先頭に行ける。
そこからさらに4バイト戻ればそれは直前のブロックのフッタになっているので、今度はそのデータで直前のブロックの先頭に戻れる。
これを繰り返すことで、ファイルを後ろから先頭まで辿って行くことが出来る。(prev_tag_sizeに0が入っていたら、それが先頭)

OpenResumeFile()

OpenResumeFile()では、ファイルを再オープンし、ファイルの長さとメタデータ、および動画全体の時間を取得する。

流れとしては、

  1. ファイルの再オープン
  2. ファイルサイズの取得
  3. ファイルフォーマットのチェック
  4. メタデータの検索

という感じ。

1. ファイルの再オープン

ファイルを"r+b"でオープン。 ファイルが開けなかった場合、この後の処理はしないで戻ってる。

2. ファイルサイズの取得

fseek()、ftell()を使って、ファイルサイズを取得する。

3. ファイルフォーマットのチェック

先頭の13バイト(ファイルヘッダ)を読んで、ファイルフォーマットのチェックを行う。

  • 先頭の4バイトが'F', 'L', 'V', 0x01であることをチェック。
  • 5バイト目と0x05との論理積が0でないことをチェック。
  • 6〜9バイト目を読んで、data_offsetを取得。
    ファイルポジションをdata_offsetへ移動させ、最初のprev_tag_sizeを読み、その値が0であることをチェック。

4. メタデータの検索

ファイルポジションを最初のデータブロックの先頭(data_offset+4の位置)へ移動し、そこからメタデータの検索を行っていく。

データブロックのヘッダ(11バイト)を読み取り、その2〜4バイト目からデータのサイズを取得。

もしヘッダの1バイト目が0x12ならメタデータのブロックなので、そのデータを読み取る。
そうでなければデータのサイズを元にファイルポジションを次のブロックの先頭に移動させる。

メタデータのデータを読み込んだら、そのデータをAMF_Decode()を使ってAMFObjectのオブジェクトにデコードし、AMF_GetProp()でAMFObjectPropertyのオブジェクトを得て、その値を文字列としてAMFProp_GetString()で得る。

この値が"onMetaData"と一致したら、それが探していたメタデータなので、AMF_Dump()を呼び出す。
(そうでなかったら外れなので、次のブロックへ移動)

さらに、AMFObjectのオブジェクトからRTMP_FindFirstMatchingProperty()を使って、動画全体の長さの情報を取得する。

ここで使われている関数のインタフェース(のほとんど)はlibrtmp/amf.hで宣言されていて、以下のとおり。

int
AMF_Decode(
  AMFObject *obj,      // [OUT] AMFObjectオブジェクト
  const char *pBuffer, // [IN ] デコード対象のデータ
  int nSize,           // [IN ] データのサイズ
  int bDecodeName);    // [IN ] プロパティ名をデコードするかどうか(?)

AMFObjectProperty *
AMF_GetProp(
  AMFObject *obj,   // [IN ] AMFObjectオブジェクト
  const AVal *name, // [IN ] 対象のプロパティ名(もしくはNULL)
  int nIndex);      // [IN ] 何番目のプロパティか

void
AMFProp_GetString(
  AMFObjectProperty *prop,  // [IN ] AMFObjectPropertyオブジェクト
  AVal * str);              // [OUT] プロパティの文字列

double
AMFProp_GetNumber(
  AMFObjectProperty * prop); // [IN ] AMFObjectPropertyオブジェクト

void
AMF_Dump(
  AMFObject *obj); // [IN ] AMFObjectオブジェクト

// これのみlibrtmp/rtmp.hで宣言されてる
int
RTMP_FindFirstMatchingProperty(
  AMFObject *obj,        // [IN ] AMFObjectオブジェクト
  const AVal *name,      // [IN ] 検索対象のプロパティ名
  AMFObjectProperty *p); // [OUT] 見つかったAMFObjectPropertyオブジェクト

GetLastKeyframe()

GetLastKeyframe()では、ファイルを後ろから辿り、最後のキーフレームの情報を取得する。

流れとしては、

  1. 音声のみかどうかのチェック
  2. 最後のキーフレームの検索
  3. 最後のキーフレームの情報の読み出し
  4. ファイルポジションの移動

という感じ。

1. 音声のみかどうかのチェック

ファイルヘッダの5バイト目を見て、3ビット目(0x04)が立っていて、1ビット目(0x01)が立っていなかったら、音声のみと判断する。

2. 最後のキーフレームの検索

一番後ろのデータブロックから順に、最後のキーフレームを探していく。

ブロックフッタの値から、そのデータブロックの先頭に移動する。
そして、ブロックヘッダ+1バイト(合計12バイト)を読んで、

  • 音声のみの場合、ブロックヘッダの1バイト目が0x08なら、それが最後のキーフレーム
  • 映像もある場合、ブロックヘッダの1バイト目が0x09かつ、データの1バイト目と0xf0との論理積が0x10なら、それが最後のキーフレーム

と判断する。

もし、最後のキーフレームでなかった場合、1つ前のデータブロックについて同様のことを行う。

3. 最後のキーフレームの情報の読み出し

最後のキーフレームが見つかったら、その情報を読み出す。

  • そのブロックのブロックヘッダの1バイト目が、最後のキーフレームのタイプとなる
  • そのブロックのprev_tag_size - 11(※ブロックヘッダ分を除いてる)が、最後のキーフレームのサイズとなる
  • そのブロックのブロックヘッダの5〜8バイト目の情報が、最後のキーフレームのタイムスタンプ(ミリ秒)になる
  • 最後のキーフレームのサイズ分のバッファを用意し、そこに最後のキーフレームのデータを読み込む

なお、タイムスタンプの情報はちょっと変な形で入ってる。

0     4                       7       8
+-- --+-------+-------+-------+-------+--
| ... |[23:16]| [15:8]| [7:0] |[31:24]|
+-- --+-------+-------+-------+-------+--
// [M:N] は、32ビットのうちのN〜Mビット

つまり、32ビットのデータのうち、0〜23ビットまでは5〜7バイト目にビッグエンディアンで格納されていて、24〜31ビットだけは8バイト目に入っている。

4. ファイルポジションの移動

ファイルポジションを、最後のキーフレームのブロックの一番最後(の次)に移動させる。
ここから続きのデータを書き込んでいくことになる。

今日はここまで!