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)を多重化して送受信します.これによって,例えば映像などの大きいデータと音声などの小さいデータを送受信する際に,大きいデータがボトルネックとなって本来先に処理を行えるはずの小さいデータが中々届かないというケースを防ぐことができます.
この多重化されて送られてくるchunkは,ChunkStreamと名付けられたストリームの単位で保持されます.このchunkには実際にはChunkStreamIDと呼ばれるIDがついており,上の図のようにChunkStream毎に振り分けられた後,読み込みが終了したものからデコードが開始できます.また,上記ではAudioとVideoのみですが,実際にはRTMPで処理されるメッセージ全てが対象となります(ハンドシェイクは除く).
RTMP論理ストリーム概要 (MessageStream)
上のChunkStreamはデータの送受信に使われる概念でしたが,RTMPには更にその上のレイヤでメッセージを振り分けるMessageStreamの概念が存在します.
送受信を別の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で送られてくるデータは以下のようになっていました.
赤い部分(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は既にコントロールのために用いているので,データ用に新しいストリームを作るということです.
4: クライアント → サーバー: publishコマンドの送信
クライアントは,映像の配信を行うためにサーバーにpublish
コマンド(仕様: p45, 7.2.2.6)を送信します.
このとき,クライアントは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の用語と意味を噛み砕くのに少し苦労したので,今後迷わないように残しておきます.
参考文献
- Adobe’s Real Time Messaging Protocol
- RTMPの仕様です.所々用語が分かりづらくてつらい.実装も仕様にないデータが飛んでくるのでつらい.つらいです.
- Adobe’s Real Time Messaging Protocol
- sile/rtmp_handshake
- Erlangを書きながら学んでいるRTMP入門(ハンドシェイク編)
- RTMP 1.0 準拠のサーバーをGo言語で実装する
- これもRTMPのハンドシェイクが詳しいです.
- RTMPパケットの仕組み
- ChunkStreamの実装を知らないときに,Payloadに入る謎の
c3
がc4
に困り果ててRTMP c3 c4
で検索して見つけた記事.これでChunkStreamのヘッダに気が付きました…
- ChunkStreamの実装を知らないときに,Payloadに入る謎の
おまけ
GoでRTMPサーバーを書いているので,途中の知見を書いた.そのうちコードを公開するので,そのときにもっと細かく書けたら嬉しい.