🗃

MOQTのCommon Catalog Formatについて

2023/11/01に公開

はじめに

IETFに提出されているCommon Catalog Format for moq-transportというドラフトについて紹介します。
Catalogの理解を深めることで、MoQ Transport(MOQT)上でのメディアの発見方法や色んなメディアコンテナフォーマットのMOQT上での併用方法が分かると思います。

TL;DR

  • 別々に定義されていたCatalogが共通化
  • 複数メディアコンテナフォーマットの同時利用が可能に
  • その他の利便性もアップ
  • このドラフトを参照するようにWarpやLOCのドラフトがアップデートされるはず

Catalog

Catalogは名前の通りカタログであり、MOQT上で配信されているコンテンツ(Track)を購読するために必要な情報が載ったJSONです。
MOQTについて簡単に説明すると、QUICを用いてメディアを送受信するためのプロトコルで、それを可能にするためにクライアント・サーバ間で送り合うメッセージについて定めています。
メディアはTrack NamespaceTrack Nameの組からなるFull Track Nameによって識別されます。
MOQTやそのモデルについては@flano_yukiさんのMOQTについてのブログ記事が詳しいです。
Catalogは特別なMOQTのTrackであり"Catalog"というTrack Nameを持ちます。

配信側は配信しているTrackの広告のためにCatalogを使用し、購読側はTrackの購読のために使用します。そのため、メディアを受信するにはまずCatalogを取得する必要があります。
下図のようにCatalogの中身はRelayサーバでは不透明として扱われ、変更も禁止されています。そのため、中身をE2E暗号化(E2EE)可能です。


MoQ Objectが受け取られるまでの流れ

このドラフトではversion 1のCatalogを定義しています。
将来のバージョンでの後方互換性は保証されないため、より新しいバージョンが定義された場合は注意が必要です。特に購読側では理解できないバージョンのパースが禁止されているため、使っている仕様の部分に変更があるかどうかに関わらず、バージョンを上げると互換性が失われます。

Catalogの例

Catalogには32種類ものフィールドが定義されている[1]ため、例と共に一部を説明します。
以下は各フィールドに共通したルールです。

  • フィールド毎に必須または任意の項目として定義
  • フィールド毎に宣言可能な位置が異なる
    • root[2]、track内、catalog内、selectionParams内のうち1箇所以上で宣言可能
  • rootで宣言されたフィールドは子要素に継承
    • 子要素でも宣言されている場合はそちらが優先
  • フィールドは順不同
  • 理解できないフィールドの無視が必須

LOCの1組のaudio/video trackの例

1組の映像と音声という基本的なパターンです。

{
  "version": 1,
  "sequence": 0,
  "streamingFormat": 1,
  "streamingFormatVersion": "0.2",
  "namespace": "conference.example.com/conference123/alice",
  "packaging": "loc",
  "renderGroup": 1,
  "tracks": [
    {
      "name": "video",
      "selectionParams":{"codec":"av01.0.08M.10.0.110.09","width":1920,"height":1080,"framerate":30,"bitrate":1500000}
    },
    {
      "name": "audio",
      "selectionParams":{"codec":"opus","samplerate":48000,"channelConfig":"2","bitrate":32000}
    }
   ]
}

versionは前述のCatalog versionを示していて、このドラフトでは常に1です。

sequenceCatalog sequence numberでObjectの番号を表し0から1ずつ増加します。つまり、この例ではこのCatalogは最初のCatalog Objectということになります。

streamingFormatStreaming format typeを表す数値です。まだ値が定まっていないのでこの例では適当な値を入れていると思われます。[3]

streamingFormatVersionはStreaming formatのバージョンを表す文字列で、streaming format側で定義します。こちらも同様に適当な値です。

namespaceTrack namespaceを表しており、各trackのtrack nameがこの名前空間内で定義されます。例えばvideoの場合、Full Track Nameはconference.example.com/conference123/alice/videoとなります。

packagingはペイロードのカプセル化方法を示していて、このドラフトでは"cmaf"/"loc"のいずれかの値をとります。[4]

renderGroupは同時に描写されるべきtrackのグループを示しています。この例のようにvideoとaudioを同時に描写するよう指示するのが一般的です。

tracksはTrack objectの配列です。後述のcatalogsとは排他的な関係でどちらか片方しか1つのCatalog内には存在できません。

nameTrack nameを定義しています。track nameはtrackの識別に使われ、名前空間内でユニークであることが必須です。

selectionParamsは購読時にtrackを選択するのに利用可能なkey-valueを保持するオブジェクトです。このフィールド自体は任意ですが、存在する場合は空オブジェクト(selectionParams: {})にするのは禁止です。

CMAFの1組のaudio/video trackの例

CMAFの初期化セグメントをBase64で自身に含んでいるパターンです。

{
  "version": 1,
  "sequence": 0,
  "streamingFormat": 1,
  "streamingFormatVersion": "0.2",
  "namespace": "sports.example.com/games/08-08-23/12345",
  "packaging": "cmaf",
  "renderGroup":1,
  "tracks": [
    {
      "name": "video_1080",
      "selectionParams":{"codec":"avc1.640028","mimeType":"video/mp4","width":1920,"height":1080,"framerate":30,"bitrate":9914554},
      "initData":"AAAAGGZ0eXBpc282AAAAAWlzbzZkYXNoAAAARWZyZWVJc29NZWRpYSBGaWxlIFByb2R1Y2VkIHdpdGggR1BBQyAxLjAuMS1yZXYwLWdkODUzOGU4YS1tYXN0ZXIAAAADLW1vb3YAAABsbXZoZAAAAADfdnly33Z5cgABX5AAAAAAAAEAAAEAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAA4bXZleAAAABBtZWhkAAAAAAPwOXgAAAAgdHJleAAAAAAAAAABAAAAAQAAA+gAAAAAAAEAAAAAAkd0cmFrAAAAXHRraGQAAAAB33Y1w992eXIAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAABQAAAAIWAAAAAAAkZWR0cwAAABxlbHN0AAAAAAAAAAEAAAAAAAAD6AABAAAAAAG/bWRpYQAAACBtZGhkAAAAAN92NcPfdnlyAABdwAAAAAAVxwAAAAAAQGhkbHIAAAAAAAAAAHZpZGUAAAAAAAAAAAAAAAAfTWFpbmNvbmNlcHQgVmlkZW8gTWVkaWEgSGFuZGxlcgAAAVdtaW5mAAAAFHZtaGQAAAABAAAAAAAAAAAAAAAkZGluZgAAABxkcmVmAAAAAAAAAAEAAAAMdXJsIAAAAAEAAADkc3RibAAAAJhzdHNkAAAAAAAAAAEAAACIYXZjMQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAUAAhYASAAAAEgAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABj//wAAADJhdmNDAU1AH//hABpnTUAfllKAoAi/NNQYGBkAAAMAAQAAAwAwhAEABWjpCTUgAAAAEHN0dHMAAAAAAAAAAAAAABBzdHNjAAAAAAAAAAAAAAAUc3RzegAAAAAAAAAAAAAAAAAAABBzdGNvAAAAAAAAAAAAAAAzaGRscgAAAAAAAAAAYWxpcwAAAAAAAAAAAAAAAEFsaWFzIERhdGEgSGFuZGxlcgAAAAA6dWR0YQAAABepVElNAAsAADAwOjAwOjAwOjAwAAAADqlUU0MAAgAAMjQAAAANqVRTWgABAAAx"
    },
    {
      "name": "audio_aac",
      "selectionParams":{"codec":"mp4a.40.5","mimeType":"audio/mp4","samplerate":48000,"channelConfig":"2","bitrate":67071},
      "initData":"AAAAGGZ0eXBpc282AAAAAWlzbzZkYXNoAAAARWZyZWVJc29NZWRpYSBGaWxlIFByb2R1Y2VkIHdpdGggR1BBQyAxLjAuMS1yZXYwLWdkODUzOGU4YS1tYXN0ZXIAAAACzG1vb3YAAABsbXZoZAAAAADfdnly33Z5cgABX5AAAAAAAAEAAAEAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAA4bXZleAAAABBtZWhkAAAAAAPwSAAAAAAgdHJleAAAAAAAAAACAAAAAQAABAAAAAAAAgAAAAAAAeZ0cmFrAAAAXHRraGQAAAAB33Y1w992eXIAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAGCbWRpYQAAACBtZGhkAAAAAN92NcPfdnlyAAC7gAAAAAAVxwAAAAAARGhkbHIAAAAAAAAAAHNvdW4AAAAAAAAAAAAAAAAjTWFpbmNvbmNlcHQgTVA0IFNvdW5kIE1lZGlhIEhhbmRsZXIAAAEWbWluZgAAABBzbWhkAAAAAAAAAAAAAAAkZGluZgAAABxkcmVmAAAAAAAAAAEAAAAMdXJsIAAAAAEAAACnc3RibAAAAFtzdHNkAAAAAAAAAAEAAABLbXA0YQAAAAAAAAABAAAAAAAAAAAAAgAQAAAAALuAAAAAAAAnZXNkcwAAAAADGQAAAAQRQBUABgAACBXTAATXtwUCEZAGAQIAAAAQc3R0cwAAAAAAAAAAAAAAEHN0c2MAAAAAAAAAAAAAABRzdHN6AAAAAAAAAAAAAAAAAAAAEHN0Y28AAAAAAAAAAAAAADNoZGxyAAAAAAAAAABhbGlzAAAAAAAAAAAAAAAAQWxpYXMgRGF0YSBIYW5kbGVyAAAAADp1ZHRhAAAAF6lUSU0ACwAAMDA6MDA6MDA6MDAAAAAOqVRTQwACAAAyNAAAAA2pVFNaAAEAADE="
    }
   ]
}

initDataInitialization dataのことでtrackの初期化データをBase64で持つ文字列です。Catalogに文字列で入っているため、そのtrackを必要としない場合(例えば音声だけ必要)でも初期化データをダウンロードしてしまいます。

以下はその問題を解消したinitTrackを用いたパターンです。(一部間違っていると思われるフィールドをドラフトから修正しています。)

{
  "version": 1,
  "sequence": 0,
  "streamingFormat": 1,
  "streamingFormatVersion": "0.2",
  "namespace": "sports.example.com/games/08-08-23/12345",
  "packaging": "cmaf",
  "renderGroup":1,
  "tracks": [
    {
      "name": "video_4k",
      "selectionParams":{"codec":"avc1.640033","mimeType":"video/mp4","width":3840,"height":2160,"framerate":30,"bitrate":14931538},
      "initTrack":"init_video_4k",
      "altGroup": 1
    },
    {
      "name": "video_1080",
      "selectionParams":{"codec":"avc1.640028","mimeType":"video/mp4","width":1920,"height":1080,"framerate":30,"bitrate":9914554},
      "initTrack":"init_video_1080",
      "altGroup": 1
    },
    {
      "name": "video_720",
      "selectionParams":{"codec":"avc1.64001f","mimeType":"video/mp4","width":1280,"height":720,"framerate":30,"bitrate":4952892},
      "initTrack":"init_video_720",
      "altGroup": 1
    },
    {
      "name": "audio_aac",
      "selectionParams":{"codec":"mp4a.40.5","mimeType":"audio/mp4","samplerate":48000,"channelConfig":"2","bitrate":67071},
      "initTrack":"init_audio_aac",
      "altGroup": 2
    },
    {
      "name": "audio_ec3",
      "selectionParms":{"codec":"ec-3","mimeType":"audio/mp4","samplerate":48000,"channelConfig":"F801","bitrate":256000},
      "initTrack":"init_audio_ec3",
      "altGroup": 2
    }
   ]
}

initTrackInitialization trackのことで初期化データを保持する別のtrackの名前の文字列です。初期化データの実体はそのtrackから取得するため、Catalogを取得するだけでは不要なデータの取得が発生しません。initTrackで指定されているtrackはtracks直下に追加することは禁止されており、initTrackからのみ参照可能です。

CMAFとLOCの併用例

映像はCMAFで、音声はLOCのパターンです。

{
  "version": 1,
  "sequence": 0,
  "streamingFormat": 1,
  "streamingFormatVersion": "0.2",
  "namespace": "output.example.com/event/12345",
  "renderGroup":1,
  "tracks": [
    {
      "name": "video0",
      "selectionParams":{"codec":"avc1.64001f","mimeType":"video/mp4","width":1280,"height":720,"framerate":30,"bitrate":4952892},
      "initTrack":"init_video_720",
      "packaging":"cmaf",
    },
    {
      "name": "audio",
      "selectionParams":{"codec":"opus","samplerate":48000,"channelConfig":"2","bitrate":32000},
      "packaging": "loc",
    }
   ]
}

packagingはtrackレベルでも指定できるため、この例ではtrack毎にcmafとlocに分かれています。
個別のフォーマット毎のCatalogの定義ではこのような用法は不可能でしたが、Common Catalog Formatとして両方を扱うことで可能になりました。

その他

3つのvideo trackと1つのaudio trackの例

映像がサイマルキャストのパターンです。

{
  "version": 1,
  "sequence": 0,
  "streamingFormat": 1,
  "streamingFormatVersion": "0.2",
  "namespace": "conference.example.com/conference123/alice",
  "renderGroup": 1,
  "packaging": "loc",
  "tracks":[
    {
      "name": "hd",
      "selectionParams": {"codec":"av01","width":1920,"height":1080,"bitrate":5000000,"framerate":30},
      "altGroup":1
    },
    {
      "name": "md",
      "selectionParams": {"codec":"av01","width":720,"height":640,"bitrate":3000000,"framerate":30},
      "altGroup":1
    },
    {
      "name": "sd",
      "selectionParams": {"codec":"av01","width":192,"height":144,"bitrate":500000,"framerate":30},
      "altGroup":1
    },
    {
      "name": "audio",
      "selectionParams":{"codec":"opus","samplerate":48000,"channelConfig":"2","bitrate":32000},
    }
   ]
}

altGroupはAlternate groupでお互いに代替のバージョンであるtrackのグループを示します。renderGroupとは異なり、購読側では典型的にグループ内から購読するtrackを1つ選びます。一般的にはこの例のように同じコンテンツを異なるビットレートで提示するような使い方をします。


サイマルキャストの例

Scalable Video Codec(SVC)の例

音声と時間的に2層、空間的にも2層になったSVCの組み合わせのパターンです。

{
  "version": 1,
  "sequence": 0,
  "streamingFormat": 1,
  "streamingFormatVersion": "0.2",
  "namespace": "conference.example.com/conference123/alice",
  "renderGroup": 1,
  "packaging": "loc",
  "tracks":[
    {
      "name": "480p15",
      "selectionParams": {"codec":"av01.0.01M.10.0.110.09","width":640,"height":480,"bitrate":3000000,"framerate":15},
    },
    {
      "name": "480p30",
      "selectionParams": {"codec":"av01.0.04M.10.0.110.09","width":640,"height":480,"bitrate":3000000,"framerate":30},
      "depends": ["480p15"],
    },
    {
      "name": "1080p15",
      "selectionParams": {"codec":"av01.0.05M.10.0.110.09","width":1920,"height":1080,"bitrate":3000000,"framerate":15},
      "depends":["480p15"]
    },

    {
      "name": "1080p30",
      "selectionParams": {"codec":"av01.0.08M.10.0.110.09","width":1920,"height":1080,"bitrate":5000000,"framerate":30},
      "depends": ["480p30", "1080p15"]
    },
    {
      "name": "audio",
      "selectionParams":{"codec":"opus","samplerate":48000,"channelConfig":"2","bitrate":32000},
    }
   ]
}

dependsはDependenciesを表す配列で、そのtrackの依存先のtrack nameを保持しています。track nameしか指定できないため、依存先も同じ名前空間にあると仮定されます。
この例のdependsが表す依存関係は以下のようになります。[5]

また、この例ではrenderGroupがrootで定義されており、すべてのtrackで同じグループであることがわかります。

                  +----------+
     +----------->|  S1T1    |
     |            | 1080p30  |
     |            +----------+
     |                  ^
     |                  |
+----------+            |
|  S1TO    |            |
| 1080p15  |            |
+----------+      +-----+----+
      ^           |  SOT1    |
      |           | 480p30   |
      |           +----------+
      |               ^
+----------+          |
|  SOTO     |         |
| 480p15    |---------+
+----------+

カスタムフィールドの利用の例

trackでカスタムフィールドを指定しているパターンです。

{
  "version": 1,
  "sequence": 0,
  "streamingFormat": 1,
  "streamingFormatVersion": "0.2",
  "namespace": "conference.example.com/conference123/alice",
  "packaging": "loc",
  "renderGroup": 1,
  "tracks": [
    {
      "name": "video",
      "selectionParams":{"codec":"av01.0.08M.10.0.110.09","width":1920,"height":1080,"framerate":30,"bitrate":1500000},
      // 以下がカスタムフィールド
      "com.example-billing-code": 3201,
      "com.example-tier": "premium",
      "com.example-debug": "h349835bfkjfg82394d945034jsdfn349fns"
    },
    {
      "name": "audio",
      "selectionParams":{"codec":"opus","samplerate":48000,"channelConfig":"2","bitrate":32000}
    }
   ]
}

com.example-で始まっているフィールドがCustom fieldです。カスタムフィールドは将来のバージョンも含めてこのドラフトで定義されるフィールドとの名前衝突を防ぐため、reverse domain name notationを先頭につけるべきとしています。これはJavaのパッケージ名などで使われていて、上の例ではexample.comからcom.example-が使われています。

Catalogの差分表現

ここまでいくつかのCatalogの例を挙げてきました。映像や音声を後から追加・削除するユースケースを考えると、一部の更新のために同じサイズのCatalogをもう一度読み込むのは無駄が大きいです。そこでCatalogの一部のフィールドのみを変更することを考えます。そのためにDelta UpdateというCatalogの表現方式も用意されています。
Delta Updateは前回からの変更のみを記述できる方式です。一方、ここまで紹介していた単独で解釈可能なCatalogはDelta Updateに対して、Independentと呼ばれています。Delta UpdateはIndependentのCatalogからの差分だけではなくDelta UpdateのCatalogからの差分としても表現可能です。


Delta Updateの関係性の例

Delta Updateは以下のルールに従って処理する必要があります。

  • parent sequence number fieldが無い場合はIndependentとして扱う
  • parent sequence number fieldが有る場合はDelta Updateとして扱う
    • parent sequence number fieldで示されるCatalogに状態を追加したかのようにパースが必要
      • この場合、古い値は新しい値で上書き
  • 以下のフィールドの変更は禁止
    • Track namespace
    • Track name
    • Track selection properties
      • これらを変えたい場合、一度trackを削除してから新しく追加が必要

trackを追加した差分の例

確立済みのビデオカンファレンスにスライドのtrackを追加する差分を表現したパターンです。

{
  "sequence": 1,
  "parentSequence":0,
  "tracks": [
    {
      "name": "slides",
      "selectionParams":{"codec":"av01.0.08M.10.0.110.09","width":1920,"height":1080,"framerate":15,"bitrate":750000},
      "renderGroup":1
    }
   ]
}

parentSequenceParent sequence numberで差分の元となるCatalogを表す整数です。前述の通り、このフィールドの有無によってCatalogが独立しているかどうかを判断します。

trackを削除した差分の例

確立済みのビデオカンファレンスからスライドのtrackを削除する差分を表現したパターンです。

{
  "sequence": 2,
  "parentSequence":1,
  "tracks": [
    {
      "name": "slides",
      "operation": "delete"
    }
   ]
}

operationTrack operationsのことです。入る値はAddDeleteの2種類で、それぞれトラックの追加と削除を表しています。デフォルト値がAddであるため、追加差分の例ではこのフィールドは省略されています。

配信終了の差分の例

すべてのtrackの削除を表現したパターンです。

{
  "sequence": 3,
  "parentSequence":2,
  "operation": "delete",
  "tracks": [{"name": "audio"},{"name": "video"}]
}

もし全てのtrackを削除している場合(operation=delete)は、送信自体を終了したと読み取るべきです。

他のカタログへの参照

Catalogでは自身でtrackを含むのではなく、他のCatalogへの参照という形でも記述できます。ただし、これらは両立できず、参照がある場合はCatalog内に参照しか含むことができません。

他の2つのCatalogを参照するCatalogの例

異なるnamespaceで同時に2つのstreaming formatsを参照するパターンです。

{
  "version": 1,
  "sequence": 0,
  "catalogs": [
    {
      "name": "catalog-for-format-one",
      "namespace": "sports.example.com/games/08-08-23/live",
      "streamingFormat":1,
      "streamingFormatVersion": "0.2"
    },
    {
      "name": "catalog-for-format-five",
      "namespace": "chat.example.com/games/08-08-23/chat",
      "streamingFormat":5,
      "streamingFormatVersion": "1.6.2"
    }
  ]
}


参照を持つカタログ

補足

Catalogの暗号化

Catalogのコンテンツは暗号化できます。実際の暗号化方式や鍵のシグナリング方法についてはCatalogを用いるstreaming format側に委ねられます。

MoQ Streaming Format Typeの登録

例ではStreaming Format Typeに適当な値が入っていましたが、実際には登録された値を使う必要があります。
各streaming formatが値を"MoQ Streaming Format Type"という新しいregistryに登録するのが必須となります。登録後にその値を使うことができます。
ただし、登録するには根拠となるRFCが必要なため、実際に値が追加されるのはかなり先になると思われます。
このドラフトでは0x0000としてReservedな値が提案されています。

Catalogの歴史


MoQとCatalogの歴史

Catalogが初めて登場したのはdraft-lcurley-warp-03というドラフトです。[6] このドラフトではまだGroupの概念はなく、載っている情報も最低限でした。
その次の版のdraft-lcurley-warp-04ではObject/Group/Trackが登場してます。
その後Warpのドラフトでより多くの定義がされるようになります。例えば、"catalog"というtrack nameにすることが必須となりました。
その後に出たLOCのドラフトでも個別に定義をしていますが、差分更新の仕組みが無いなどの違いがあります。[7]
そしてIETF117を経てこのCommon Catalog Formatのドラフトとして定義されました。Common Catalog FormatはWarpとLOCで個別に定義されていたCatalogを統合し、改良したものになっています。

まとめ

ドラフトで説明されているCatalog自体やそれらのフィールドについて例と共に紹介しました。どのようにこれらが使われるのか想像できるようになっていると嬉しいです。
今後WarpやLOCでは自身で定義するCatalogからこのCommon Catalog Formatを参照するようになると考えられます。Common Catalog Format自身もだんだんと洗練されていくと思われるので楽しみです。

脚注
  1. https://datatracker.ietf.org/doc/html/draft-wilaw-moq-catalogformat-01#section-3.2 ↩︎

  2. JSONのトップレベル階層のこと ↩︎

  3. 実際に後ろのcmafの例でもこのlocの例と同じく1が使われています。 ↩︎

  4. LOCのパッケージング手法は増えないと思いますが、WarpではCMAF以外が定義されることもあるのかもしれません ↩︎

  5. 依存される側から矢印が出ているのに注意が必要です ↩︎

  6. このドラフトが指す"Warp"は現在のWarpとは異なるので注意が必要です。このドラフトは現在のMOQTのドラフトへと置き換えられ、Warp特有の部分が現在のWarpのドラフトとなっています。また、このドラフト内でも02と03の間でタイトルが変わるなどの変化があるので注意が必要です。同様に、現在のMOQTと別のMOQTのドラフトもあるので注意が必要です。 ↩︎

  7. LOCについてはLOC紹介の記事で紹介しています。 ↩︎

Discussion