🛠️

WebRTCのAV1なRTPパケットをwebmファイルに録画する方法

2021/11/01に公開

AV1 コーデックを使っている RTP パケットを webm ファイルに、録画するために必要な知識と方法についてまとめます。(ブラウザ上で MediaRecorder を使うという話ではないです...)

WebRTC 経由で AV1 な RTP パケットを受け取って webm ファイルに録画するプログラムを TypeScript + Node.js で実際に実装しました。

https://github.com/shinyoshiaki/werift-webrtc/blob/v0.13.5/packages/rtp/src/codec/av1.ts

録画した webm ファイルは Chrome と VLC で再生できることを確認しています。

参考資料

それぞれの資料について見ていきます。

AOM AV1 codec mapping in Matroska/WebM

webm ファイルを Chrome と VLC で再生するために必要な EBML の Element を列挙します。

  • Header
  • Segment
    • Info
    • Tracks
      • Track
        • TrackNumber
        • TrackUID
        • CodecName
          • AV1
        • TrackType
          • 1
        • VideoTrack
  • Cluster
    • BlockData(SimpleBlock)

CodecName はAV1です。

1 点注意する必要があります。
BlockData(SimpleBlock)に格納する OBU で要求されているフィールドと RTP Payload に格納されている OBU のフィールドの状態がそれぞれの仕様上食い違ってしまっています。
これについては後々のセクションで解説しています。

Block Data

AOM AV1 codec mapping in Matroska/WebM の仕様書のこのセクションに BlockData で要求されている OBU のフィールドの仕様について書かれています。

RTP Payload の OBU フィールド と BlockData で要求されている OBU フィールド で、食い違っている箇所は obu_has_size_field とそれに付随した obu_size です。
RTP Payload の OBU は obu_has_size_field が 0 になっているのに対して、 BlockData の OBU では必ず 1 である必要があります。そして obu_has_size_field が 1 ということは obu_size にも値が入っている必要があります。
そのため RTP Payload から取り出した OBU を BlockData に格納するためには、obu_has_size_field を編集した上で obu_size を追加する必要があります。
他のフィールドに関しては RTP Payload の OBU を編集すること無く Chrome&VLC で再生できる webm ファイルを作ることが出来ました。

RTP Payload Format For AV1

RTP Payload を webm ファイルに、保存するために RTP Payload から取り出す必要のある情報は以下です。

  • キーフレームフラグ
  • OBU

最終的には RTP Payload から OBU のまとまりを取り出し、またその OBU のまとまりにキーフレームが含まれているかという情報も取り出せればそれらを webm の BlockData に格納し、録画が出来ます。

AV1 の RTP Payload の構造

AV1 の RTP Payload は次の 2 つのパートに別れています。

AV1 Aggregation Header

 0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+
|Z|Y| W |N|-|-|-|
+-+-+-+-+-+-+-+-+

Payload

0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Z|Y|0 0|N|-|-|-|  OBU element 1 size (leb128)  |               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+               |
:                                                               :
:                      OBU element 1 data                       :
:                                                               :
|                                                               |
|                               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                               |  OBU element 2 size (leb128)  |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
:                                                               :
:                       OBU element 2 data                      :
:                                                               :
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
:                                                               :
:                              ...                              :
:                                                               :
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|OBU e... N size|                                               |
+-+-+-+-+-+-+-+-+       OBU element N data      +-+-+-+-+-+-+-+-+
|                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

RTP Payload は先頭に AV1 Aggregation Header が有って、以後に OBU element の length フィールド + OBU element のセットであるPayloadが続きます。length フィールド + OBU element のセットは複数存在することが出来ます。

AV1 Aggregation Header

各フィールド

Z
RTP Payload の先頭の OBU element がフラグメントされているかどうかを示すフラグ。

先頭の element がフラグメントされているということは、
それすなわち先頭の OBU element が前に来た RTP Payload のフラグメントされた最後の OBU element の続きであることを示す。

Y
RTP Payload の最後の OBU element がフラグメントされていることを示すフラグ。

次に来る RTP Payload の先頭の OBU element がフラグメントされることになり、また Aggregation Header の Z フラグは 1 になる。

W
RTP Payload に含まれる OBU の数を示す。
もし W の値が 0 なら RTP Payload に含まれる各 OBU はそれに対応する length フィールドをもつ(W が 0 だからといって実際に OBU の数が 0 ではないというのが重要。ややこしい)。
ただし length フィールドを省略できるケースにおいては length フィールドを省略する。

N
OBU elements にキーフレームが含まれていると 1 になる。

Payload

Payload には OBU と OBU の length フィールドが入っています。length フィールドは Leb128 という符号化形式で符号化されています。
OBU と length フィールドのセット は Payload に複数入る可能性があります。
Chrome の場合だと RTP Payload の大きさがおよそ 1200byte を越えると最後の OBU はフラグメントされます。このしきい値は恐らく MTU に由来していると思われます。
OBU がフラグメントされると、その OBU が属する RTP Payload の Aggregation Header の Y フラグは 1 となり、次の RTP Payload の Z フラグは 1 となります。

AV1 の RTP Payload は各 OBU に対応する length フィールドを原則持ちますが、AV1 の OBU Header にも同じような用途の obu_has_size_field と obu_size が存在します(この obu_size も Leb128 で符号化されています)。RTP Payload の length フィールドと OBU Header の obu_size は目的が重複していて冗長なので RTP Payload における OBU Header の obu_has_size_field は常に 0 であり、obu_size は存在しません。

この仕様は webm の仕様と食い違っていて相性が悪いです。先程 Block Data のセクションでも述べたように Block Data 中の OBU Header は obu_has_size_field と obu_size が必須なので、RTP Payload の OBU を webm の Block Data に、格納するために OBU フィールドの書き換えが必要です。

OBU syntax

RTP Payload に含まれる OBU のフィールドと webm の BlockData が要求する OBU のフィールドは仕様が食い違っているので、RTP Payload の OBU を webm の BlockData に格納するためには、RTP Payload の OBU のフィールドの書き換えが必要になります。そのためには OBU の文法に従って、OBU のデシリアライズ/シリアライズできる必要があります。

OBU の文法の決まりは凄い長いので本記事で使う部分だけを見ていきます。

function open_bitstream_unit(sz) {
  obu_forbidden_bit; // 1bit
  obu_type; // 4bit
  obu_extension_flag; // 1bit
  obu_has_size_field; // 1bit
  obu_reserved_1bit; // 1bit
  if (obu_extension_flag == 1) {
    obu_extension_header();
  }
  if (obu_has_size_field) {
    obu_size; // leb128
  }
}

各フィールド

obu_forbidden_bit
使いません。

obu_type
次の Type があります。Chrome が出力する RTP で基本流れてくるのは 1 番の OBU_SEQUENCE_HEADER と 6 番の OBU_FRAME です。他は見たことがないです。
OBU_SEQUENCE_HEADER はキーフレームが流れるタイミングで来るみたいです。

interface OBU_TYPES {
  0: "Reserved";
  1: "OBU_SEQUENCE_HEADER";
  2: "OBU_TEMPORAL_DELIMITER";
  3: "OBU_FRAME_HEADER";
  4: "OBU_TILE_GROUP";
  5: "OBU_METADATA";
  6: "OBU_FRAME";
  7: "OBU_REDUNDANT_FRAME_HEADER";
  8: "OBU_TILE_LIST";
  15: "OBU_PADDING";
}

obu_extension_flag
Chrome がこのフラグを使っているところを今の所、見たことがないです。そのうち使われるようになるかもです。

obu_has_size_field
このフラグが 1 になっていると OBU は obu_size フィールドを持ちます。

obu_reserved_1bit
使いません。

obu_size
OBU のペイロード(OBU における OBU Header と obu_size 以外の領域)の大きさを Leb128 で符号化した値が入っています。

OBU のフィールドの編集

RTP Payload の OBU を webm 向けに変換するためのステップについてまとめます。

  • OBU のバイナリをデシリアライズ
  • obu_has_size_field を 1 にする
  • obu_size に OBU のペイロードサイズを入れる
  • OBU をバイナリにシリアライズ

ここまでのまとめ

最後に RTP Payload を webm ファイルに録画するステップをまとめます。

  • webm の EBML の先頭部分を組み立てる
    • Header
    • Segment
    • Cluster
  • RTP を受け取る
    • RTP Payload をバッファーに貯める。
    • RTP Header のマーカーフラグが立っていたら
      • バッファ中の RTP Payload の AV1 Aggregation Header をデシリアライズ
        • N フラグを見てキーフレームか含まれているか調べる
        • Z フラグか Y フラグが立っていたらフラグメントされた OBU 同士を結合する
        • OBU 達をデシリアライズ
      • バッファをクリア
      • OBU 達の obu_has_size_field を 1 にして obu_size に OBU のペイロードサイズを入れる
      • OBU 達をシリアライズ
  • OBU 達とキーフレーム情報を webm の Block Data に入れる

最後に

「AV1 RTP webm 録画」と直球なタイトルで Google 検索しても、そのまんまな記事が見つからなかったので今回この記事を書いてみました。
仕様書を読む練習になってよかったです。

Discussion