いものやま。

雑多な知識の寄せ集め

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

Rubyニコニコ動画のダウンロードツールを書いた話や、Ruby-FFIについて調べた話は、以下から。

下準備も整ったので、rtmpdumpのコードを読んで、どのようにlibrtmpを使っているのか調べてみた。*1

ファイル構成

今回関係があるファイルは、以下。

  • rtmpdump.c
    rtmpdumpのソースコード
    librtmpを使ってコマンドラインのインタフェースを実装している。
  • librtmp/rtmp.h
    librtmpのインタフェースが定義されているヘッダファイル。
  • librtmp/amf.h
    FLVのデータを読むためのデータ構造やインタフェースが定義されているヘッダファイル。
    (amfはAdobe Media Formatの略?)
  • librtmp/log.h
    ログ出力のインタフェースが定義されているヘッダファイル。

rtmpdump.c

rtmpdumpのソースコード

主な内容は、main()の定義と、そこから呼ばれるOpenResumeFile()、GetLastKeyframe()、Download()という関数の定義。

main()

大まかな流れは、以下。

  1. ログ出力レベルの確認(※コマンドラインの解析中にログを出力する可能性があるから)
  2. rtmpオブジェクトの初期化
  3. コマンドラインの解析、チェック
  4. 接続の設定
  5. 中断したファイルの再オープン(そうでなければ新規ファイル作成)
  6. 接続/再接続
  7. ダウンロード(失敗したり、途中だった場合、再接続へ)
  8. 切断
AVal型

各処理を追っていく前に、まずおさえておきたいのが、コード全体を通して頻出するAVal型。

これはlibrtmp/amf.hで定義されていて、貧弱なCの文字列処理を補うためのものっぽい。(といっても、文字列長の情報も持つようにしているだけだけど)

定義は以下の通り。

typedef struct AVal
{
  char *av_val;
  int av_len;
} AVal;

以下のような、ちょっとしたマクロも用意されている。

// 文字列strでAValを初期化するマクロ
#define AVC(str)   {str,sizeof(str)-1}
// AVal同士を比較するマクロ(等しければ非0を返す)
#define AVMATCH(a1,a2) ((a1)->av_len == (a2)->av_len && !memcmp((a1)->av_val,(a2)->av_val,(a1)->av_len))
1. ログ出力レベルの確認

コマンドラインオプションで-qもしくは--quietが指定されていないかを確認している。
指定されていた場合、RTMP_debuglevelをRTMP_LOGCRITにしている。

なお、RTMP_debuglevelはlibrtmp/log.hで宣言されている列挙型の変数。(実体はlibrtmp/log.cで定義されてる)

typedef enum
{ RTMP_LOGCRIT=0, RTMP_LOGERROR, RTMP_LOGWARNING, RTMP_LOGINFO,
  RTMP_LOGDEBUG, RTMP_LOGDEBUG2, RTMP_LOGALL
} RTMP_LogLevel;

extern RTMP_LogLevel RTMP_debuglevel;
2. rtmpオブジェクトの初期化

RTMP型のオブジェクトrtmpRTMP_Init()で初期化している。

RTMP_Init()のインタフェースはlibrtmp/rtmp.hで宣言されていて、以下のとおり。

void RTMP_Init(RTMP *r);

なお、rtmpはヒープ上じゃなくてスタック上に確保している。

3. コマンドラインの解析、チェック

コマンドラインを解析し、変数を適宜設定している。

ニコニコ動画のダウンロードツールからrtmpdumpを使っているときに指定しているそれぞれのオプションについては、以下のような処理を行っていた。

オプション 処理内容
-q RTMP_debuglevelをRTMP_LOGCRITに
-e 再開フラグbResumeをTRUEに
-r URL RTMP_ParseURL()でURLをパース
-t URL tcUrlとしてURLを設定
-p URL pageUrlとしてURLを設定
-y PLAYPATH playpathとしてPLAYPATHを設定
-C OPTION RTMP_SetOpt()でOPTIONをオプションとして設定
-o FILENAME 出力するファイル名flvFileとしてFILENAMEを設定

RTMP_ParseURL()のインタフェースは、以下。

int
RTMP_ParseURL(
  const char *url,    // [IN ] パースするURL
  int *protocol,      // [OUT] プロトコル
  AVal *host,         // [OUT] ホスト名
  unsigned int *port, // [OUT] ポート番号
  AVal *playpath,     // [OUT] プレイパス
  AVal *app);         // [OUT] アプリケーション名

ただ、ニコ動のURLの形式だとプレイパスが取得できないっぽいので、-yオプションで別途指定してやる必要があるっぽい。
また、ポート番号は明示的に指定されていなかった場合に0が返ってくるので、その場合はプロトコルからデフォルトのポート番号を設定してやる必要がある感じ。(RTMPやRTMPEなら1935番)

そして、RTMP_SetOpt()のインタフェースは、以下。

int
RTMP_SetOpt(
  RTMP *r,         // [IN ] RTMPオブジェクト
  const AVal *opt, // [IN ] オプションの種類
  AVal *arg);      // [IN ] オプションの値

-Cで指定されたオプションは、オプションの種類は"conn"として、この関数に渡される。

4. 接続の設定

RTMP_SetupStream()で、接続の設定を行っている。

RTMP_SetupStream()のインタフェースは、以下。(もうちょいなんとかならなかったものなんだろうか?(^^;)

void
RTMP_SetupStream(
  RTMP *r,              // [IN ] RTMPオブジェクト
  int protocol,         // [IN ] プロトコル
  AVal *hostname,       // [IN ] ホスト名
  unsigned int port,    // [IN ] ポート番号
  AVal *sockshost,      // [IN ] (よく分からない)
  AVal *playpath,       // [IN ] プレイパス
  AVal *tcUrl,          // [IN ] tcURL
  AVal *swfUrl,         // [IN ] (よく分からない)
  AVal *pageUrl,        // [IN ] pageURL
  AVal *app,            // [IN ] アプリケーション名
  AVal *auth,           // [IN ] (よく分からない)
  AVal *swfSHA256Hash,  // [IN ] (よく分からない)
  uint32_t swfSize,     // [IN ] (よく分からない)
  AVal *flashVer,       // [IN ] (よく分からない)
  AVal *subscribepath,  // [IN ] (よく分からない)
  AVal *usherToken,     // [IN ] (よく分からない)
  int dStart,           // [IN ] 開始するタイムスタンプ(ミリ秒)(指定しないなら0)
  int dStop,            // [IN ] 終了するタイムスタンプ(ミリ秒)(指定しないなら0)
  int bLiveStream,      // [IN ] ライブかどうか
  long int timeout);    // [IN ] タイムアウト(秒)

よく分からないものが多すぎるけど、実際のところ、指定しているオプションではこれらにはNULLしか渡らないので、特に問題なし。

そのあと、ライブでない場合に、次のようにフラグを立てている。(RTMPの構造については省略)

rtmp.Link.lFlags |= RTMP_LF_BUFX;
5. 中断したファイルの再オープン(そうでなければ新規ファイル作成)

前回の接続で途中までしかダウンロードできていない場合、そのファイルを再オープンして、再接続に必要な情報を取り出す必要がある。
ということで、その処理を行っている。

処理はOpenResumeFile()とGetLastKeyframe()という関数で行ってるので、詳細はあとで見ていく。

ちなみに、OpenResumeFile()とGetLastKeyframeのインタフェースは以下のとおり。

int
OpenResumeFile(
  const char *flvFile,        // [IN ] 再オープンするファイル名
  FILE **file,                // [OUT] 再オープンしたファイル
  off_t *size,                // [OUT] ファイルのサイズ
  char **metaHeader,          // [OUT] ファイルのメタデータ
  uint32_t *nMetaHeaderSize,  // [OUT] メタデータのサイズ
  double *duration);          // [OUT] 動画全体の時間(秒)

int
GetLastKeyframe(
  FILE *file,                   // [IN ] 再オープンしたファイル
  int nSkipKeyFrames,           // [IN ] スキップするキーフレーム数
  uint32_t *dSeek,              // [OUT] 最後のキーフレームのタイムスタンプ(ミリ秒)
  char **initialFrame,          // [OUT] 最後のキーフレームのデータ
  int *initialFrameType,        // [OUT] 最後のキーフレームの種類(音声/映像)
  uint32_t *nInitialFrameSize); // [OUT] 最後のキーフレームのデータのサイズ

なお、GetLastKeyframe()の仮引数名がinitialFrame...となっているのは、最後のキーフレームがすなわち再接続時の初期のキーフレームになるから。

それと、これらはlibrtmpで提供されている関数ではないので、アプリケーション側で同等の機能を実装する必要があることに注意。

もし、中断したファイルがなかったり、ファイルが不正だった場合、新たにファイルを作成している。

6. 接続/再接続

最初の場合は接続の処理を、ダウンロードが失敗したり途中だった場合、再接続の処理を行う。

まず、いずれの場合もRTMP_SetBufferMS()を呼ぶ。

void RTMP_SetBufferMS(
  RTMP *r,    // [IN ] RTMPオブジェクト
  int size); // [IN ] バッファ時間(ミリ秒)

バッファ時間はデフォルトでは10時間を設定している。

そして、接続する場合、まずRTMP_Connect()を、次にRTMP_ConnectStream()を呼ぶ。

int
RTMP_Connect(
  RTMP *r,         // [IN ] RTMPオブジェクト
  RTMPPacket *cp); // [IN ] RTMPパケット

int
RTMP_ConnectStream(
  RTMP *r,       // [IN ] RTMPオブジェクト
  int seekTime); // [IN ] 最初のキーフレームのタイムスタンプ(ミリ秒)

なお、RTMPパケットにはNULLを指定してる。

再接続する場合は、RTMPオブジェクトのメンバm_pausingの値に応じて、RTMP_ReconnectStream()もしくはRTMP_ToggleStream()を呼んでいる。

int
RTMP_ReconnectStream(
  RTMP *r,       // [IN ] RTMPオブジェクト
  int seekTime); // [IN ] 最初のキーフレームのタイムスタンプ(ミリ秒)

int
RTMP_ToggleStream(
  RTMP *r); // [IN ] RTMPオブジェクト

なお、RTMP_ReconnectStream()は一度ストリームを削除し、新たにストリームを作り直す感じ。
一方、RTMP_ToggleStream()は、(ストリームが一時停止状態でなければ一時停止してちょっと待ったあと、)一時停止を解除する感じ。

7. ダウンロード(失敗したり、途中だった場合、再接続へ)

接続/再接続が終わったら、ダウンロードを行う。

ダウンロードの処理はDownload()という関数で行っているので、詳細はあとで見ていく。

Download()のインタフェースは、以下。

int
Download(
  RTMP *rtmp,           // [IN ] RTMPオブジェクト
  FILE *file,           // [IN ] データを保存するファイル
  uint32_t dSeek,       // [IN ] 開始するタイムスタンプ(ミリ秒)
  uint32_t dStopOffset, // [IN ] 終了するタイムスタンプ(ミリ秒)(指定しないなら0)
  double duration,      // [IN ] 動画全体の長さ(秒)
  int bResume,          // [IN ] 再開かどうか
  char *metaHeader,     // [IN ] ファイルのメタデータ
  uint32_t nMetaHeaderSize, // [IN ] メタデータのサイズ
  char *initialFrame,   // [IN ] 最初のキーフレームのデータ
  int initialFrameType, //[IN ] 最初のキーフレームの種類(音声/映像)
  uint32_t nInitialFrameSize, //[IN ] 最初のキーフレームのデータのサイズ
  int nSkipKeyFrames,   // [IN ] スキップするキーフレーム数
  int bStdoutMode,      // [IN ] 標準出力に出力するかどうか
  int bLiveStream,      // [IN ] ライブかどうか
  int bRealtimeStream,  // [IN ] リアルタイムかどうか
  int bHashes,          // [IN ] 進捗具合の出力フォーマットの種類
  int bOverrideBufferTime, //[IN ] ユーザがバッファ時間を指定したかどうか
  uint32_t bufferTime,  // [IN ] バッファ時間(ミリ秒)
  double *percent);     // [OUT] 進捗の度合(%)

これもlibrtmpで提供されている関数ではないので、アプリケーション側で同等の機能を実装する必要がある。

ダウンロードが無事終わった場合は、切断処理へ。
そうでない場合、再接続を試みることになる。(ただし、再接続がうまくいかなかったら、切断処理へ)

8. 切断

RTMP_Close()で接続を切り、開いていたファイルのクローズを行う。

RTMP_Close()のインタフェースは、以下。

void
RTMP_Close(
  RTMP *r); // [IN ] RTMPオブジェクト

今日はここまで!

*1:なお、読んだのは、2015-01-14にコミットされたもの。(commit a107cef9b392616dff54fabfd37f985ee2190a6f)