yutopp's blog

サンドバッグになりたい

RTMPのChunkStream, MessageStreamの話

yutoppです.

今回は,RTMPのChunkStreamとMessageStreamの2種類のストリームについて書きます.

それでは,一旦ストリームの種類の概要を紹介した後,RTMPの通信が2つのストリームの種類をどのように使い分けて通信するのかという説明していきます.細かいフロー(ChunkSizeやBandwidthの設定など)は割愛します.また,RTMPは実装によってパラメータがまちまちであるため,環境によってIDの番号などが異なると思いますので,適宜読み替えてください.

※この記事の指す"RTMP"は,AdobeのRTMPの仕様(v1.0)に基づくものとします.

RTMPのデータの送受信概要 (ChunkStream)

RTMPは,1つのTCPコネクションの上に細切れにしたデータ(Chunk)を多重化して送受信します.これによって,例えば映像などの大きいデータと音声などの小さいデータを送受信する際に,大きいデータがボトルネックとなって本来先に処理を行えるはずの小さいデータが中々届かないというケースを防ぐことができます.

f:id:yutopp:20180611040426p:plain

この多重化されて送られてくるchunkは,ChunkStreamと名付けられたストリームの単位で保持されます.このchunkには実際にはChunkStreamIDと呼ばれるIDがついており,上の図のようにChunkStream毎に振り分けられた後,読み込みが終了したものからデコードが開始できます.また,上記ではAudioとVideoのみですが,実際にはRTMPで処理されるメッセージ全てが対象となります(ハンドシェイクは除く).

RTMP論理ストリーム概要 (MessageStream)

上のChunkStreamはデータの送受信に使われる概念でしたが,RTMPには更にその上のレイヤでメッセージを振り分けるMessageStreamの概念が存在します.

f:id:yutopp:20180611071830p:plain

送受信を別のChunkStreamで行っていたとしても,例えば同じ動画に対するデータを処理する場合にMessageStreamとしては同じになるようなイメージです.

詳細

それでは,上記を踏まえてRTMPの通信を例にとって実際のデータ構造について説明していきます.

その前に,2つのストリームについての細かい補足です.

まず,ChunkStreamについてです.ChunkStreamは,ChunkStreamIDと呼ばれるIDによって区別されます.
このIDは2〜65599まで使うことが可能です(計65598ストリーム).しかし,その中でもChunkStreamID 2はRTMPによって予約されているため,ユーザーが自由に使うことはできません.それを除けば,ChunkStreamは生成や削除を明示的に行わなくても使うことが出来ます.

次に,MessageStreamについてです.MessageStreamはMessageStreamIDと呼ばれるIDによって区別されます.
このIDはuint32型の範囲で指定することが可能です.RTMPは,コネクションごとにコントロールストリームと呼ばれるストリームを必ず持ちます(MessageStreamIDは0).それ以外のストリームについては,自由に使うことはできませんcreateStreamコマンド(仕様: p36, 7.2.1.3)を用いて,サーバーからMessageStreamIDを発行してもらう必要があります.

では,実際のデータの流れを見ながらデータ構造を追っていきます.
前提として,通信の流れは以下のようなコマンドで得られたデータを用いています.

ffmpeg -re -i test.mp4 -codec copy -f flv rtmp://localhost:1935/live/hogehoge

また,ChunkStreamのChunkSize(Chunkのpayloadの大きさ)はデフォルトの128Bytesと仮定し,Ackなどのメッセージは無視するものとします.

1: ハンドシェイク

割愛.後述の参考文献にハンドシェイクについてのリンクがあります.

2: クライアント → サーバー: connectコマンドの送信

クライアントは,まずサーバーにconnectコマンド(仕様: p29, 7.2.1.1)を送信します.ffmpegではMessageStreamにはID 0(コントロールストリーム),ChunkStreamにはID 3を指定するようですね.
また,このconnectコマンドを含むメッセージはpayloadが157Bytesあるようで,ChunkSizeの128Bytesを上回っているため早速Chunkに分割されて送られてきています.

TCPで送られてくるデータは以下のようになっていました.

f:id:yutopp:20180611061247p:plain

赤い部分(03とc3の部分)がChunkの切れ目です.丁度payloadの長さが128Bytes(0x80Bytes)になるところでChunkが区切れているのが分かります.
これらのChunkのHeaderを見たら取り除き,payloadの部分のみをChunkStreamとしてバッファに保持していきます.メッセージの長さの分Chunkを読み終わったら,バッファをデコードし,MessageStreamIDに紐づくMessageStreamのメッセージとしてデータを処理します.
今回はMessageStreamIDは0なので,コントロールストリームにconnectコマンドのメッセージが来たものとして処理します(接続済みのステートにコネクションを変更する).

2.1: サーバー → クライアント: _resultコマンドの送信

割愛.connectに成功したので,NetConnectionの_resultをクライアントに返します.

3: クライアント → サーバー: createStreamコマンドの送信

動画データの送信を行うために,クライアントはcreateStreamコマンドをサーバーに送信します.メッセージの内容は特にありませんが,この時点でもMessageStreamIDは0を利用します.

3.1: サーバー → クライアント: _resultコマンドの送信

connectに成功したので,NetConnectionの_resultをクライアントに返します.
ここで,新しいMessageStreamとしてStreamIDとして1(とりあえず0以外)をクライアントに返します.MessageStreamID 0は既にコントロールのために用いているので,データ用に新しいストリームを作るということです.

f:id:yutopp:20180611064641p:plain

4: クライアント → サーバー: publishコマンドの送信

クライアントは,映像の配信を行うためにサーバーにpublishコマンド(仕様: p45, 7.2.2.6)を送信します.

f:id:yutopp:20180611065613p:plain

このとき,クライアントはcreateStreamで受け取ったStreamID 1をMessageStreamIDに指定していることが分かります.これ以降のNetStream系の操作は,createStreamによって得られたMessageStreamに対して行うようになります.

4.1: サーバー→クライアント: onStatusコマンドの送信

割愛.publishに成功したので,NetStreamのonStatusをクライアントに返します.

5: それ以降

クライアントは,ChunkStreamをvideoとaudio,metadataで変更しながら,MessageStreamIDはcreateStreamで得られたIDに固定でデータを送るようになります.

最後に

RTMPの仕様に出てくる2種類のストリームについて,実際に送受信するデータを追いつつストリームの用途を確認しました.

初めてRTMPの仕様を読んだときに,ChunkStreamとMessageStreamの用語と意味を噛み砕くのに少し苦労したので,今後迷わないように残しておきます.

参考文献

おまけ

GoでRTMPサーバーを書いているので,途中の知見を書いた.そのうちコードを公開するので,そのときにもっと細かく書けたら嬉しい.