diff --git a/.gitignore b/.gitignore index 7841c4680..aaf03e8b3 100644 --- a/.gitignore +++ b/.gitignore @@ -19,16 +19,6 @@ result node_modules /.direnv -# Don't leak dev stuff -*.mp4 -*.fmp4 -*.crt -*.key -*.hex -*.jwk -*.jwt -*.m3u8 -*.m4s # We're using bun package-lock.json diff --git a/CLAUDE.md b/CLAUDE.md index 7f090e198..ac037e796 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -116,6 +116,7 @@ match version { - Run `just fix` to automatically fix formating and easy things. - Rust tests are integrated within source files - Async tests that sleep should call `tokio::time::pause()` at the start to simulate time instantly +- Use `tokio::time::sleep()` (not `advance()`) to move time forward in paused-time tests — `sleep` both advances the clock and yields to the runtime so spawned tasks can run ## Branching Strategy @@ -123,6 +124,7 @@ match version { - **`dev`**: Development branch for breaking API changes. PRs with major API changes should target `dev`. - When ready for a new minor/major release, merge `dev` into `main`. - `cargo-semver-checks` enforces this on PRs to `main`. +- When removing a public method on `dev`, mark it `#[deprecated]` first so downstream code gets warnings before the next breaking release. ## Workflow diff --git a/Cargo.lock b/Cargo.lock index 1d1f97781..a44fb5be4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -737,9 +737,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.58" +version = "1.2.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" +checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283" dependencies = [ "find-msvc-tools", "jobserver", @@ -2217,6 +2217,7 @@ dependencies = [ "hex", "lazy_static", "moq-lite", + "moq-mux", "moq-native", "regex", "serde", @@ -3167,6 +3168,7 @@ dependencies = [ "moq-lite", "moq-mux", "moq-native", + "serde_json", "thiserror 2.0.18", "tokio", "tracing", @@ -3393,10 +3395,12 @@ dependencies = [ "anyhow", "axum", "axum-server", + "base64 0.22.1", "clap", "hang", "moq-mux", "moq-native", + "mp4-atom", "rustls", "sd-notify", "tokio", @@ -3433,6 +3437,7 @@ dependencies = [ "moq-mux", "moq-native", "pollster", + "serde_json", "thiserror 2.0.18", "tokio", "tracing", @@ -3486,6 +3491,8 @@ dependencies = [ "reqwest", "scuffle-av1", "scuffle-h265", + "serde", + "serde_json", "thiserror 2.0.18", "tokio", "tracing", @@ -6247,9 +6254,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.50.0" +version = "1.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +checksum = "2bd1c4c0fc4a7ab90fc15ef6daaa3ec3b893f004f915f2392557ed23237820cd" dependencies = [ "bytes", "libc", @@ -6265,9 +6272,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.1" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", diff --git a/bun.lock b/bun.lock index a5841d5f8..3bea58490 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 1, "workspaces": { "": { "name": "moq", @@ -167,7 +166,7 @@ }, "js/token": { "name": "@moq/token", - "version": "0.1.2", + "version": "0.2.0", "bin": { "moq-token": "./src/cli.ts", }, @@ -208,6 +207,7 @@ "@moq/lite": "workspace:^", "@moq/signals": "workspace:^", "@moq/ui-core": "workspace:^", + "zod": "^4.1.5", }, "devDependencies": { "@types/audioworklet": "^0.0.77", @@ -1669,7 +1669,7 @@ "unified-args/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], - "unified-engine/@types/node": ["@types/node@22.19.15", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg=="], + "unified-engine/@types/node": ["@types/node@22.19.17", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q=="], "unified-engine/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], diff --git a/cdn/input.tf b/cdn/input.tf index 64187d887..51b7e7aee 100644 --- a/cdn/input.tf +++ b/cdn/input.tf @@ -36,17 +36,25 @@ variable "webhook" { # instance types: https://api.linode.com/v4/linode/types locals { relays = { - usc = { - region = "us-central" # Dallas, TX - type = "g6-standard-2" # 4GB RAM, 2 vCPU, $24/mo, 4TB out + usw = { + region = "us-west" # Fremont, CA + type = "g6-standard-2" # 4GB RAM, 2 vCPU, $24/mo, 4TB out + connect = ["use"] + } + use = { + region = "us-east" # Newark, NJ + type = "g6-standard-2" + connect = ["usw", "euc"] } euc = { - region = "eu-central" # Frankfurt, Germany - type = "g6-standard-2" + region = "eu-central" # Frankfurt, Germany + type = "g6-standard-2" + connect = ["use"] } sea = { - region = "ap-south" # Singapore - type = "g6-standard-2" + region = "ap-south" # Singapore + type = "g6-standard-2" + connect = ["use"] } } } diff --git a/cdn/main.tf b/cdn/main.tf index 34121ffea..7b505350b 100644 --- a/cdn/main.tf +++ b/cdn/main.tf @@ -68,6 +68,7 @@ module "relay" { module "pub" { source = "./pub" domain = var.domain + relay = "use.${var.domain}" ssh_keys = var.ssh_keys stackscript_id = linode_stackscript.bootstrap.id gcp_account_key = google_service_account_key.relay.private_key diff --git a/cdn/pub/demo-bbb.service.tftpl b/cdn/pub/demo-bbb.service.tftpl index fbfec0a6b..a42240730 100644 --- a/cdn/pub/demo-bbb.service.tftpl +++ b/cdn/pub/demo-bbb.service.tftpl @@ -23,7 +23,7 @@ ExecStart=/bin/bash -c '\ -movflags cmaf+separate_moof+delay_moov+skip_trailer+frag_every_frame \ - | \ /var/lib/moq/pkg/bin/moq-cli publish \ - --url "https://${domain}/demo?jwt=$(cat /var/lib/moq/demo-pub.jwt)" \ + --url "https://${relay}/demo?jwt=$(cat /var/lib/moq/demo-pub.jwt)" \ --name bbb \ fmp4' diff --git a/cdn/pub/main.tf b/cdn/pub/main.tf index 860f80240..23c9207ba 100644 --- a/cdn/pub/main.tf +++ b/cdn/pub/main.tf @@ -1,7 +1,7 @@ # Generate systemd service files from templates resource "local_file" "demo_bbb_service" { content = templatefile("${path.module}/demo-bbb.service.tftpl", { - domain = var.domain + relay = var.relay }) filename = "${path.module}/gen/demo-bbb.service" } @@ -9,7 +9,7 @@ resource "local_file" "demo_bbb_service" { # Publisher instance resource "linode_instance" "publisher" { label = "publisher-moq" - region = "us-central" # Dallas, TX + region = "us-east" # Newark, NJ (colocated with use relay) type = "g6-nanode-1" # Use Debian 12 as base, will be converted to NixOS via bootstrap diff --git a/cdn/pub/variables.tf b/cdn/pub/variables.tf index 05b348484..b5a5ace33 100644 --- a/cdn/pub/variables.tf +++ b/cdn/pub/variables.tf @@ -3,6 +3,11 @@ variable "domain" { type = string } +variable "relay" { + description = "Relay hostname to publish to (e.g. use.example.com)" + type = string +} + variable "ssh_keys" { description = "SSH public keys for root access" type = list(string) diff --git a/cdn/relay/dns.tf b/cdn/relay/dns.tf index 4056c3e05..cded1c990 100644 --- a/cdn/relay/dns.tf +++ b/cdn/relay/dns.tf @@ -32,7 +32,8 @@ resource "google_dns_record_set" "relay_global" { # GCP uses region codes like "us-east1", "us-west1", "europe-west3", "asia-southeast1" locals { relay_gcp_regions = { - usc = "us-central1" # Dallas, TX -> closest GCP region + usw = "us-west1" # Fremont, CA -> closest GCP region + use = "us-east1" # Newark, NJ -> closest GCP region euc = "europe-west3" # Frankfurt -> closest GCP region sea = "asia-southeast1" # Singapore -> closest GCP region } diff --git a/cdn/relay/justfile b/cdn/relay/justfile index a3c72c478..cbc218b33 100644 --- a/cdn/relay/justfile +++ b/cdn/relay/justfile @@ -26,13 +26,14 @@ host node: # List the available nodes. nodes: - @echo "usc: Dallas, TX" + @echo "usw: Fremont, CA" + @echo "use: Newark, NJ" @echo "euc: Frankfurt, Germany" @echo "sea: Singapore" # Deploy moq-relay and moq-cert to all nodes [parallel] -deploy-all: (deploy "usc") (deploy "euc") (deploy "sea") +deploy-all: (deploy "usw") (deploy "use") (deploy "euc") (deploy "sea") # Deploy moq-relay and moq-cert to a specific node deploy node: @@ -78,7 +79,7 @@ status node: echo "=== $HOST ===" ssh root@$HOST "systemctl status moq-relay --no-pager" -status-all: (status "usc") (status "euc") (status "sea") +status-all: (status "usw") (status "use") (status "euc") (status "sea") ssh node: #!/usr/bin/env bash diff --git a/cdn/relay/main.tf b/cdn/relay/main.tf index 05ff209cf..1a5f8c259 100644 --- a/cdn/relay/main.tf +++ b/cdn/relay/main.tf @@ -1,9 +1,12 @@ -# Generate systemd service files from templates +# Generate per-node systemd service files from templates resource "local_file" "moq_relay_service" { + for_each = var.relays + content = templatefile("${path.module}/moq-relay.service.tftpl", { - domain = var.domain + domain = var.domain + connect = each.value.connect }) - filename = "${path.module}/gen/moq-relay.service" + filename = "${path.module}/gen/${each.key}/moq-relay.service" } resource "local_file" "moq_cert_service" { diff --git a/cdn/relay/moq-relay.service.tftpl b/cdn/relay/moq-relay.service.tftpl index 8fe247453..d934d05e4 100644 --- a/cdn/relay/moq-relay.service.tftpl +++ b/cdn/relay/moq-relay.service.tftpl @@ -20,7 +20,9 @@ ExecStart=/var/lib/moq/pkg/bin/moq-relay \ --web-https-listen [::]:443 \ --web-https-cert /etc/letsencrypt/live/${domain}/fullchain.pem \ --web-https-key /etc/letsencrypt/live/${domain}/privkey.pem \ - --cluster-root usc.${domain} \ +%{ for peer in connect ~} + --cluster-connect ${peer}.${domain} \ +%{ endfor ~} --cluster-node %H \ --cluster-token /var/lib/moq/cluster.jwt diff --git a/cdn/relay/variables.tf b/cdn/relay/variables.tf index 7be566435..e69728a97 100644 --- a/cdn/relay/variables.tf +++ b/cdn/relay/variables.tf @@ -16,8 +16,9 @@ variable "ssh_keys" { variable "relays" { description = "Map of relay node configurations" type = map(object({ - region = string - type = string + region = string + type = string + connect = list(string) })) } diff --git a/dev/.gitignore b/dev/.gitignore new file mode 100644 index 000000000..c6c8158db --- /dev/null +++ b/dev/.gitignore @@ -0,0 +1,9 @@ +*.mp4 +*.fmp4 +*.crt +*.key +*.hex +*.jwk +*.jwt +*.m3u8 +*.m4s diff --git a/dev/boy/src/audio.rs b/dev/boy/src/audio.rs index e992f4551..d29ac7082 100644 --- a/dev/boy/src/audio.rs +++ b/dev/boy/src/audio.rs @@ -4,7 +4,7 @@ use anyhow::{Context, Result}; /// /// Uses ffmpeg-next for Opus encoding (same dependency as video H.264). pub struct AudioEncoder { - opus: moq_mux::import::Opus, + opus: moq_mux::producer::Opus, ffmpeg_encoder: ffmpeg_next::encoder::audio::Encoder, resampler: Option, sample_buffer: Vec, @@ -26,10 +26,10 @@ impl AudioEncoder { catalog: moq_mux::CatalogProducer, input_sample_rate: u32, ) -> Result { - let opus = moq_mux::import::Opus::new( + let opus = moq_mux::producer::Opus::new( broadcast, catalog, - moq_mux::import::OpusConfig { + moq_mux::producer::OpusConfig { sample_rate: OPUS_SAMPLE_RATE, channel_count: CHANNELS, }, diff --git a/dev/boy/src/index.ts b/dev/boy/src/index.ts index 3d33f989f..b77783107 100644 --- a/dev/boy/src/index.ts +++ b/dev/boy/src/index.ts @@ -195,7 +195,7 @@ class GameCard { const sync = new Watch.Sync({ jitter: 50 as Moq.Time.Milli }); this.#signals.cleanup(() => sync.close()); - const videoSource = new Watch.Video.Source(sync, { broadcast }); + const videoSource = new Watch.Video.Source({ sync, broadcast }); this.#signals.cleanup(() => videoSource.close()); // Set pixel budget based on expanded state. @@ -220,7 +220,7 @@ class GameCard { this.#signals.cleanup(() => videoRenderer.close()); // Set up audio — play on hover at 50% volume. - const audioSource = new Watch.Audio.Source(sync, { broadcast }); + const audioSource = new Watch.Audio.Source({ sync, broadcast }); this.#signals.cleanup(() => audioSource.close()); const audioDecoder = new Watch.Audio.Decoder(audioSource); diff --git a/dev/boy/src/input.rs b/dev/boy/src/input.rs index 1947afe4e..666eb99fb 100644 --- a/dev/boy/src/input.rs +++ b/dev/boy/src/input.rs @@ -40,33 +40,21 @@ pub async fn handle_viewers( cmd_tx: &tokio::sync::mpsc::Sender, ) -> anyhow::Result<()> { loop { - let Some((path, broadcast)) = viewer_origin.announced().await else { - break; - }; + let (path, broadcast) = viewer_origin.announced().await?; let viewer_id = path.to_string(); - if let Some(broadcast) = broadcast { - tracing::info!(%viewer_id, "viewer connected"); - let cmd_tx = cmd_tx.clone(); - let vid = viewer_id.clone(); - tokio::spawn(async move { - if let Err(e) = handle_viewer_commands(&vid, broadcast, &cmd_tx).await { - tracing::warn!(viewer_id = %vid, error = %e, "viewer command error"); - } - tracing::info!(viewer_id = %vid, "viewer disconnected"); - let _ = cmd_tx.send(Command::ViewerLeft { viewer_id: vid }).await; - }); - } else { - tracing::info!(%viewer_id, "viewer went offline"); - let _ = cmd_tx - .send(Command::ViewerLeft { - viewer_id: viewer_id.clone(), - }) - .await; - } + tracing::info!(%viewer_id, "viewer connected"); + let cmd_tx = cmd_tx.clone(); + let vid = viewer_id.clone(); + tokio::spawn(async move { + if let Err(e) = handle_viewer_commands(&vid, broadcast, &cmd_tx).await { + tracing::warn!(viewer_id = %vid, error = %e, "viewer command error"); + } + tracing::info!(viewer_id = %vid, "viewer disconnected"); + let _ = cmd_tx.send(Command::ViewerLeft { viewer_id: vid }).await; + }); } - Ok(()) } async fn handle_viewer_commands( @@ -74,12 +62,10 @@ async fn handle_viewer_commands( broadcast: moq_lite::BroadcastConsumer, cmd_tx: &tokio::sync::mpsc::Sender, ) -> anyhow::Result<()> { - let command_track = moq_lite::Track { - name: "command".to_string(), - priority: 0, - }; + let command_track = moq_lite::Track::new("command"); + let sub = moq_lite::Subscription::default(); - let mut track = broadcast.subscribe_track(&command_track)?; + let mut track = broadcast.subscribe_track(&command_track, sub)?; while let Some(mut group) = track.next_group().await? { while let Some(frame) = group.read_frame().await? { diff --git a/dev/boy/src/main.rs b/dev/boy/src/main.rs index 5b45d40d9..19fc666ff 100644 --- a/dev/boy/src/main.rs +++ b/dev/boy/src/main.rs @@ -54,7 +54,7 @@ async fn run(config: &Config) -> Result<()> { let client = config.client.clone().init()?; // Create the broadcast producer. - let mut broadcast = moq_lite::BroadcastProducer::default(); + let broadcast = moq_lite::BroadcastProducer::default(); // Publish origin: the GB session broadcast. let publish_origin = moq_lite::Origin::produce(); @@ -78,14 +78,11 @@ async fn run(config: &Config) -> Result<()> { .await?; // Set up catalog and video encoder. - let catalog = moq_mux::CatalogProducer::new(&mut broadcast)?; + let catalog = moq_mux::CatalogProducer::new(&broadcast)?; let video_encoder = video::VideoEncoder::spawn(broadcast.clone(), catalog.clone()); // Create status track. - let status_track = moq_lite::Track { - name: "status".to_string(), - priority: 10, - }; + let status_track = moq_lite::Track::new("status"); let mut status_producer = broadcast.create_track(status_track)?; // Run the emulator on a blocking thread. @@ -219,7 +216,7 @@ async fn run(config: &Config) -> Result<()> { #[tokio::main] async fn main() -> Result<()> { let config = Config::parse(); - config.log.init(); + let _ = config.log.init(); tokio::select! { res = run(&config) => res, diff --git a/dev/boy/src/video.rs b/dev/boy/src/video.rs index 614fcb63e..542d2f0ff 100644 --- a/dev/boy/src/video.rs +++ b/dev/boy/src/video.rs @@ -23,7 +23,7 @@ impl VideoEncoder { catalog: moq_mux::CatalogProducer, ) -> Self { let (tx, rx) = tokio::sync::mpsc::channel(4); - let avc3 = moq_mux::import::Avc3::new(broadcast, catalog); + let avc3 = moq_mux::producer::Avc3::new(broadcast, catalog); let thread = std::thread::Builder::new() .name("video-encoder".into()) @@ -44,7 +44,7 @@ impl VideoEncoder { fn encoder_thread( mut rx: tokio::sync::mpsc::Receiver, - mut avc3: moq_mux::import::Avc3, + mut avc3: moq_mux::producer::Avc3, ) { let mut encoder: Option = None; let mut scaler: Option = None; @@ -173,7 +173,7 @@ impl Encoder { &mut self, yuv: &ffmpeg_next::frame::Video, ts: hang::container::Timestamp, - output: &mut moq_mux::import::Avc3, + output: &mut moq_mux::producer::Avc3, ) -> Result<()> { self.encoder.send_frame(yuv)?; diff --git a/dev/relay/leaf0.toml b/dev/relay/leaf0.toml index 833341cea..966f16bd0 100644 --- a/dev/relay/leaf0.toml +++ b/dev/relay/leaf0.toml @@ -16,35 +16,16 @@ tls.generate = ["localhost"] # Listen for HTTP and WebSocket (TCP) connections on the given address. listen = "[::]:4444" -# This clustering scheme is very very simple for now. -# -# There is a root node that is used to connect leaf nodes together. -# Announcements flow from leaf -> root -> leaf, but any subscriptions are leaf -> leaf. -# The root node can serve (user) subscriptions too. -# -# The root node is either missing the "root" field below or it's identifical to the "node" field. -# This node acts a server only, accepting incoming connections from leaf nodes and users alike. -# -# There can be any number of leaf nodes. -# These nodes will connect to the specified root address and announce themselves via MoQ as a "broadcast". -# All nodes will discover these broadcasts and connect to other nodes. -# -# This forms an NxN mesh of nodes. -# Broadcasts are announced between all nodes with no collision detection, so duplicates are possible. -# Subscriptions will be relayed from leaf to leaf, so at most you can have: -# user -> leaf -> leaf -> user +# Clustering: connect to the root node. +# In this example, leaf nodes don't connect directly to each other; traffic gets proxied through the root. +# `connect` can be an array to connect to multiple peers, creating a tiered CDN. [cluster] -# Connect to this hostname in order to discover other nodes. connect = "localhost:4443" # Use the token in this file when connecting to other nodes. # `just auth-token` will populate this file. token = "root.jwt" -# My hostname, which must be accessible from other nodes. -node = "localhost:4444" - -# Each leaf node will connect to the root node and other nodes, using this configuration. [client] # QUIC uses TLS to have the client verify the server's identity. # However if you're not worried about man-in-the-middle attacks, you can disable verification: diff --git a/dev/relay/leaf1.toml b/dev/relay/leaf1.toml index 33fa1f7da..392954b6c 100644 --- a/dev/relay/leaf1.toml +++ b/dev/relay/leaf1.toml @@ -16,35 +16,16 @@ tls.generate = ["localhost"] # Listen for HTTP and WebSocket (TCP) connections on the given address. listen = "[::]:4445" -# This clustering scheme is very very simple for now. -# -# There is a root node that is used to connect leaf nodes together. -# Announcements flow from leaf -> root -> leaf, but any subscriptions are leaf -> leaf. -# The root node can serve (user) subscriptions too. -# -# The root node is either missing the "root" field below or it's identical to the "node" field. -# This node acts a server only, accepting incoming connections from leaf nodes and users alike. -# -# There can be any number of leaf nodes. -# These nodes will connect to the specified root address and announce themselves via MoQ as a "broadcast". -# All nodes will discover these broadcasts and connect to other nodes. -# -# This forms an NxN mesh of nodes. -# Broadcasts are announced between all nodes with no collision detection, so duplicates are possible. -# Subscriptions will be relayed from leaf to leaf, so at most you can have: -# user -> leaf -> leaf -> user +# Clustering: connect to the root node. +# In this example, leaf nodes don't connect directly to each other; traffic gets proxied through the root. +# `connect` can be an array to connect to multiple peers, creating a tiered CDN. [cluster] -# Connect to this hostname in order to discover other nodes. connect = "localhost:4443" # Use the token in this file when connecting to other nodes. # `just auth-token` will populate this file. token = "root.jwt" -# My hostname, which must be accessible from other nodes. -node = "localhost:4445" - -# Each leaf node will connect to the root node and other nodes, using this configuration. [client] # QUIC uses TLS to have the client verify the server's identity. # However if you're not worried about man-in-the-middle attacks, you can disable verification: diff --git a/doc/js/@moq/lite.md b/doc/js/@moq/lite.md index e1cb6b07f..577aab0a9 100644 --- a/doc/js/@moq/lite.md +++ b/doc/js/@moq/lite.md @@ -81,7 +81,7 @@ if (request) { // Read data as it arrives for (;;) { - const group = await track.nextGroup(); + const group = await track.recvGroup(); if (!group) break; for (;;) { diff --git a/doc/rs/crate/libmoq.md b/doc/rs/crate/libmoq.md index 09ff47c9d..360dcf39d 100644 --- a/doc/rs/crate/libmoq.md +++ b/doc/rs/crate/libmoq.md @@ -7,15 +7,19 @@ description: C bindings for MoQ [![docs.rs](https://docs.rs/libmoq/badge.svg)](https://docs.rs/libmoq) -C bindings for `moq-lite` via FFI, enabling MoQ integration in C/C++ applications and other languages. +C bindings for MoQ via FFI, providing media publish/subscribe functionality for C/C++ applications and other languages. ## Overview -`libmoq` provides: +`libmoq` provides a C API for real-time media delivery over QUIC. It wraps the Rust [moq-lite](/rs/crate/moq-lite) and [moq-mux](/rs/crate/moq-mux) crates, handling: -- **C API** - Header files for C integration -- **FFI bindings** - Safe Rust-to-C interface -- **Build system integration** - CMake and pkg-config support +- **Sessions** - QUIC/WebTransport connections to MoQ relays +- **Origins** - Containers for broadcast discovery and routing +- **Publishing** - Encoding and sending audio/video tracks +- **Consuming** - Receiving, decoding, and rendering media tracks +- **Catalogs** - Discovering available audio/video renditions + +All functions use opaque integer handles to reference resources. Negative return values indicate errors, zero indicates success, and positive values are resource handles. ## Installation @@ -27,76 +31,183 @@ cd moq/rs/libmoq cargo build --release ``` -The library will be in `target/release/libmoq.a` (static) or `target/release/libmoq.so` (dynamic). +The static library will be at `target/release/libmoq.a`. + +### Linking + +With CMake: -### Using Cargo +```cmake +find_package(moq REQUIRED) +target_link_libraries(myapp moq) +``` + +With pkg-config: ```bash -cargo install libmoq +gcc -o myapp myapp.c $(pkg-config --cflags --libs moq) ``` -## Usage +## API -### C Header +### Initialization -```c -#include +| Function | Description | +|----------|-------------| +| `moq_log_level(level, level_len)` | Set log level: `"error"`, `"warn"`, `"info"`, `"debug"`, `"trace"` | -int main() { - // Initialize connection - moq_connection_t* conn = moq_connect("https://relay.example.com/demo"); - if (!conn) { - fprintf(stderr, "Failed to connect\n"); - return 1; - } +### Sessions - // Create broadcast - moq_broadcast_t* broadcast = moq_broadcast_new("my-broadcast"); +Connect to a MoQ relay server over QUIC/WebTransport. - // Create track - moq_track_t* track = moq_track_new(broadcast, "chat"); +| Function | Description | +|----------|-------------| +| `moq_session_connect(url, url_len, origin_publish, origin_consume, on_status, user_data)` | Connect to a relay. Provide origin handles for publish/consume, or `0` to disable. Calls `on_status` on connect (code 0) and close (code non-zero). | +| `moq_session_close(session)` | Close a session and cancel its background task. | - // Publish data - moq_group_t* group = moq_group_append(track); - moq_frame_write(group, "Hello, MoQ!", 11); - moq_group_close(group); +### Origins - // Publish to relay - moq_publish(conn, broadcast); +Origins group broadcasts by path. They can be shared across sessions for fanout/relaying. - // Cleanup - moq_broadcast_free(broadcast); - moq_connection_free(conn); +| Function | Description | +|----------|-------------| +| `moq_origin_create()` | Create a new origin. | +| `moq_origin_publish(origin, path, path_len, broadcast)` | Publish a broadcast to an origin at the given path. | +| `moq_origin_consume(origin, path, path_len)` | Consume a broadcast from an origin by path. Returns a broadcast handle. | +| `moq_origin_announced(origin, on_announce, user_data)` | Discover broadcasts published to an origin. Calls `on_announce` with an announced ID for each broadcast. | +| `moq_origin_announced_info(announced, dst)` | Query the path and active status of an announced broadcast. | +| `moq_origin_announced_close(announced)` | Stop listening for announcements. | +| `moq_origin_close(origin)` | Close an origin. | - return 0; -} -``` +### Publishing -### Linking +Create broadcasts and write media frames. -With CMake: +| Function | Description | +|----------|-------------| +| `moq_publish_create()` | Create a new broadcast for publishing. | +| `moq_publish_media_ordered(broadcast, format, format_len, init, init_size)` | Add a media track to a broadcast. `format` specifies the encoding. `init` is codec-specific initialization data. | +| `moq_publish_media_frame(media, payload, payload_size, timestamp_us)` | Write a frame to a media track. Frames must be in decode order. Timestamp is in microseconds. | +| `moq_publish_media_close(media)` | Remove a media track from a broadcast. | +| `moq_publish_close(broadcast)` | Close a broadcast. | -```cmake -find_package(moq REQUIRED) -target_link_libraries(myapp moq) +### Consuming + +Subscribe to broadcasts and receive decoded media frames. + +| Function | Description | +|----------|-------------| +| `moq_consume_catalog_subscribe(broadcast, on_catalog, user_data)` | Subscribe to catalog updates. Calls `on_catalog` with a catalog snapshot ID when the catalog changes. | +| `moq_consume_catalog_unsubscribe(catalog)` | Stop the catalog subscription background task. Previously delivered snapshots remain valid. | +| `moq_consume_catalog_close(catalog)` | Close a catalog snapshot. Invalidates any borrowed pointers from config queries. | +| `moq_consume_video_config(catalog, index, dst)` | Query video rendition info: name, codec, description, dimensions. | +| `moq_consume_audio_config(catalog, index, dst)` | Query audio rendition info: name, codec, description, sample rate, channels. | +| `moq_consume_video_ordered(catalog, index, max_latency_ms, on_frame, user_data)` | Subscribe to a video track. Delivers frames in order, skipping GoPs when latency exceeds `max_latency_ms`. | +| `moq_consume_audio_ordered(catalog, index, max_latency_ms, on_frame, user_data)` | Subscribe to an audio track. Same latency behavior as video. | +| `moq_consume_video_close(track)` | Close a video track subscription. | +| `moq_consume_audio_close(track)` | Close an audio track subscription. | +| `moq_consume_frame_chunk(frame, index, dst)` | Read a chunk of frame payload. Call with increasing `index` to get all chunks. | +| `moq_consume_frame_close(frame)` | Close a frame and release its memory. | +| `moq_consume_close(consume)` | Close a broadcast consumer. | + +### Data Structures + +```c +// Video rendition configuration +typedef struct { + const char *name; // Track name (NOT null-terminated) + size_t name_len; + const char *codec; // Codec string (NOT null-terminated) + size_t codec_len; + const uint8_t *description; // Codec-specific init data, or NULL + size_t description_len; + const uint32_t *coded_width; // Encoded width, or NULL + const uint32_t *coded_height; // Encoded height, or NULL +} moq_video_config; + +// Audio rendition configuration +typedef struct { + const char *name; + size_t name_len; + const char *codec; + size_t codec_len; + const uint8_t *description; // Codec-specific init data, or NULL + size_t description_len; + uint32_t sample_rate; // Sample rate in Hz + uint32_t channel_count; // Number of channels +} moq_audio_config; + +// A frame of media data +typedef struct { + const uint8_t *payload; // Frame data, or NULL if stream ended + size_t payload_size; + uint64_t timestamp_us; // Presentation timestamp in microseconds + bool keyframe; // True if this starts a new GoP +} moq_frame; + +// An announced broadcast +typedef struct { + const char *path; // Broadcast path (NOT null-terminated) + size_t path_len; + bool active; // Whether the broadcast is currently active +} moq_announced; ``` -With pkg-config: +## Usage Example -```bash -gcc -o myapp myapp.c $(pkg-config --cflags --libs moq) +### Publishing + +```c +#include + +// Initialize logging +moq_log_level("info", 4); + +// Create origin and session +int origin = moq_origin_create(); +int session = moq_session_connect(url, url_len, origin, 0, on_status, NULL); + +// Create a broadcast with a video track +int broadcast = moq_publish_create(); +int video = moq_publish_media_ordered(broadcast, "h264", 4, init_data, init_size); + +// Write frames +moq_publish_media_frame(video, frame_data, frame_size, timestamp_us); + +// Publish to the relay +moq_origin_publish(origin, "my-stream", 9, broadcast); ``` -## API Reference +### Consuming + +```c +// Create origin and connect +int origin = moq_origin_create(); +int session = moq_session_connect(url, url_len, 0, origin, on_status, NULL); -Full API documentation: [docs.rs/libmoq](https://docs.rs/libmoq) +// Consume a broadcast +int broadcast = moq_origin_consume(origin, "my-stream", 9); -## Use Cases +// Subscribe to the catalog +moq_consume_catalog_subscribe(broadcast, on_catalog, NULL); -- **C/C++ applications** - Native integration without Rust toolchain -- **Language bindings** - Build bindings for Python, Go, etc. -- **Legacy systems** - Integrate MoQ into existing C codebases -- **Embedded systems** - Where Rust runtime isn't available +// In the catalog callback: +void on_catalog(void *user_data, int catalog) { + moq_video_config config; + moq_consume_video_config(catalog, 0, &config); + + // Subscribe to the first video track + moq_consume_video_ordered(catalog, 0, 500, on_frame, NULL); +} + +// In the frame callback: +void on_frame(void *user_data, int frame) { + moq_frame chunk; + moq_consume_frame_chunk(frame, 0, &chunk); + // process chunk.payload, chunk.timestamp_us, chunk.keyframe + moq_consume_frame_close(frame); +} +``` ## Next Steps diff --git a/doc/rs/env/native.md b/doc/rs/env/native.md index b6a2a9dae..48d0ced05 100644 --- a/doc/rs/env/native.md +++ b/doc/rs/env/native.md @@ -123,17 +123,18 @@ See the full [subscribe.rs](https://github.com/moq-dev/moq/blob/main/rs/hang/exa ## Reading Frames -Subscribe to a media track and read frames using [`OrderedConsumer`](https://docs.rs/hang/latest/hang/container/struct.OrderedConsumer.html): +Subscribe to a media track and read frames using [`OrderedConsumer`](https://docs.rs/moq-mux/latest/moq_mux/consumer/struct.OrderedConsumer.html): ```rust let track_consumer = broadcast.subscribe_track(&track); -let mut ordered = hang::container::OrderedConsumer::new( +let mut ordered = moq_mux::consumer::OrderedConsumer::new( track_consumer, + moq_mux::consumer::Legacy, Duration::from_millis(500), // max latency before skipping groups ); while let Some(frame) = ordered.read().await? { - // frame.timestamp, frame.keyframe, frame.payload + // frame.timestamp, frame.is_keyframe(), frame.payload } ``` diff --git a/doc/rs/index.md b/doc/rs/index.md index db6332a45..c929ec6f7 100644 --- a/doc/rs/index.md +++ b/doc/rs/index.md @@ -253,7 +253,7 @@ async fn main() -> Result<(), Box> { let mut track = broadcast.subscribe("chat").await?; // Read groups and frames - while let Some(group) = track.next_group().await? { + while let Some(group) = track.recv_group().await? { while let Some(frame) = group.read().await? { println!("Received: {:?}", frame); } diff --git a/doc/spec/draft-ietf-moq-cmsf-00.txt b/doc/spec/draft-ietf-moq-cmsf-00.txt new file mode 100644 index 000000000..872d1aac9 --- /dev/null +++ b/doc/spec/draft-ietf-moq-cmsf-00.txt @@ -0,0 +1,554 @@ + + + + +Media Over QUIC W. Law +Internet-Draft Akamai +Intended status: Informational 1 December 2025 +Expires: 4 June 2026 + + + CMSF- a CMAF compliant implementation of MOQT Streaming Format + draft-ietf-moq-cmsf-00 + +Abstract + + This document updates [MSF] by defining a new optional feature for + the streaming format. It specifies the syntax and semantics for + adding CMAF-packaged media [CMAF] to MSF. + +About This Document + + This note is to be removed before publishing as an RFC. + + The latest revision of this draft can be found at https://moq- + wg.github.io/cmsf/draft-wilaw-moq-cmsf.html. Status information for + this document may be found at https://datatracker.ietf.org/doc/draft- + ietf-moq-cmsf/. + + Discussion of this document takes place on the Media Over QUIC + Working Group mailing list (mailto:moq@ietf.org), which is archived + at https://mailarchive.ietf.org/arch/browse/moq/. Subscribe at + https://www.ietf.org/mailman/listinfo/moq/. + + Source for this draft and an issue tracker can be found at + https://github.com/moq-wg/cmsf. + +Status of This Memo + + This Internet-Draft is submitted in full conformance with the + provisions of BCP 78 and BCP 79. + + Internet-Drafts are working documents of the Internet Engineering + Task Force (IETF). Note that other groups may also distribute + working documents as Internet-Drafts. The list of current Internet- + Drafts is at https://datatracker.ietf.org/drafts/current/. + + Internet-Drafts are draft documents valid for a maximum of six months + and may be updated, replaced, or obsoleted by other documents at any + time. It is inappropriate to use Internet-Drafts as reference + material or to cite them other than as "work in progress." + + This Internet-Draft will expire on 4 June 2026. + + + +Law Expires 4 June 2026 [Page 1] + +Internet-Draft CMSF- a CMAF compliant implementation of December 2025 + + +Copyright Notice + + Copyright (c) 2025 IETF Trust and the persons identified as the + document authors. All rights reserved. + + This document is subject to BCP 78 and the IETF Trust's Legal + Provisions Relating to IETF Documents (https://trustee.ietf.org/ + license-info) in effect on the date of publication of this document. + Please review these documents carefully, as they describe your rights + and restrictions with respect to this document. Code Components + extracted from this document must include Revised BSD License text as + described in Section 4.e of the Trust Legal Provisions and are + provided without warranty as described in the Revised BSD License. + +Table of Contents + + 1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . 2 + 2. MSF Extension . . . . . . . . . . . . . . . . . . . . . . . . 3 + 3. CMAF Packaging . . . . . . . . . . . . . . . . . . . . . . . 3 + 3.1. Initialization headers . . . . . . . . . . . . . . . . . 3 + 3.2. Switching sets and tracks . . . . . . . . . . . . . . . . 3 + 3.3. Object Packaging . . . . . . . . . . . . . . . . . . . . 3 + 3.4. Group Packaging . . . . . . . . . . . . . . . . . . . . . 4 + 3.5. Catalog description . . . . . . . . . . . . . . . . . . . 4 + 3.5.1. CMAF packaging type . . . . . . . . . . . . . . . . . 4 + 3.5.2. Max SAP starting types . . . . . . . . . . . . . . . 4 + 3.6. Event Timelines . . . . . . . . . . . . . . . . . . . . . 5 + 3.6.1. SAP Type timeline . . . . . . . . . . . . . . . . . . 5 + 3.6.2. SAP-type timeline track example . . . . . . . . . . . 6 + 4. Catalog Examples . . . . . . . . . . . . . . . . . . . . . . 6 + 4.1. Simulcast video tracks - 3 alternate video qualities along + with audio . . . . . . . . . . . . . . . . . . . . . . . 7 + 5. Conventions and Definitions . . . . . . . . . . . . . . . . . 8 + 6. Security Considerations . . . . . . . . . . . . . . . . . . . 8 + 7. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 8 + 8. Normative References . . . . . . . . . . . . . . . . . . . . 8 + Acknowledgments . . . . . . . . . . . . . . . . . . . . . . . . . 9 + Author's Address . . . . . . . . . . . . . . . . . . . . . . . . 9 + +1. Introduction + + CMAF compliant MOQT Streaming Format (CMSF) is a media format + designed to deliver CMAF [CMAF] and LOC [LOC] compliant media content + over MOQ Transport (MOQT) [MoQTransport]. CMSF extends MSF and + retains all the scope, capabilities and features of MSF including the + catalog format, timeline, ABR switching and LOC support. MSF is + targeted at real-time and interactive levels of live latency, as well + as VOD content. + + + +Law Expires 4 June 2026 [Page 2] + +Internet-Draft CMSF- a CMAF compliant implementation of December 2025 + + + This document describes version 1 of the CMSF streaming format. + +2. MSF Extension + + All of the specifications, requirements, and terminology defined in + [MSF] apply to implementations of this extension unless explicitly + noted otherwise in this document. + +3. CMAF Packaging + +3.1. Initialization headers + + A CMAF header is a sequence of CMAF constrained ISO BMFF boxes that + do not reference any media samples, but are associated with a CMAF + track and are necessary for initializing the decoding of the + subsequent CMAF fragments. + + The header for a given MOQT Track MUST be packaged by encoding the + header using [BASE64] and then inserting that payload as the value of + the Initialization data "initData" field in the catalog entry for + that Track. + +3.2. Switching sets and tracks + + This specification defines a direct mapping between CMAF Tracks ( + [CMAF] Sect 3.2.1) and MOQT tracks ([MoQTransport] Sect 2.3). + + CMAF switching sets are a set of one or more CMAF tracks (3.2.1), + where each track is an alternative encoding of the same source + content and are constrained to enable seamless track switching + (3.3.9). + + Each CMAF track in a switching set MUST be transmitted as a separate + MOQT Track. The catalog entry for each of these tracks in the + switching set MUST carry a Alternate group (altGroup) key with a + common value. + + The MOQT Group numbers within these switching set tracks MUST be + media time-aligned. Mandating the track being media time-aligned + requires that the presentation time of the first media sample + contained within the first MOQT Object of each MOQT Group is + identical. + +3.3. Object Packaging + + The payload of each Object is subject to the following requirements: + + + + + +Law Expires 4 June 2026 [Page 3] + +Internet-Draft CMSF- a CMAF compliant implementation of December 2025 + + + * MUST contain at least one Movie Fragment Box (moof) followed by a + Media Data Box (mdat). This is equivalent to requiring that each + Object hold at least one CMAF Chunk. The Media Fragment Box + (moof) MUST contain a Movie Fragment Header Box (mfhd) and Track + Box (trak) with a Track ID (track_ID) matching a Track Box in the + initialization fragment. + + * MAY contain multiple successive CMAF Chunks. + + * MUST contain a single [ISOBMFF] track. + + * MUST contain media content encoded in decode order. + +3.4. Group Packaging + + Each MOQT Group + + * MUST begin with an Object containing a stream access point (SAP + type 1 or 2). + + * MUST contain one or more contiguous Groups of Pictures (GOPs). + + * The Group boundary MUST align with a CMAF Fragment boundary. CMAF + Fragments and CMAF Chunks MUST not span Groups. + +3.5. Catalog description + +3.5.1. CMAF packaging type + + This specification extends the allowed packaging values defined in + [MSF] to include one new entry, as defined in Table 1 below: + + +======+=======+===========+ + | Name | Value | Reference | + +======+=======+===========+ + | CMAF | cmaf | This RFC | + +------+-------+-----------+ + + Table 1 + + Every Track entry in a CMSF catalog carrying CMAF-packaged media data + MUST declare a "packaging" type value of "cmaf". + +3.5.2. Max SAP starting types + + This specification adds two track-level catalog fields, as defined in + Table 2 below: + + + + +Law Expires 4 June 2026 [Page 4] + +Internet-Draft CMSF- a CMAF compliant implementation of December 2025 + + + +=============================+=======================+============+ + | Field | Name | Definition | + +=============================+=======================+============+ + | Max Group SAP starting type | maxGrpSapStartingType | Section | + | | | 3.5.2.1 | + +-----------------------------+-----------------------+------------+ + | Max Object SAP starting | maxObjSapStartingType | Section | + | type | | 3.5.2.2 | + +-----------------------------+-----------------------+------------+ + + Table 2 + +3.5.2.1. Max Group SAP starting type + + Location: T Required: Optional JSON Type: Number + + A number indicating the maximum SAP type the MOQT Groups in the track + start with. + +3.5.2.2. Max Object SAP starting type + + Location: T Required: Optional JSON Type: Number + + A number indicating the maximum SAP type the MOQT Objects in the + track start with. + +3.6. Event Timelines + +3.6.1. SAP Type timeline + + CMSF defines a special instance of an Event Timeline track, termed + the SAP Type timeline track. Its purpose is to convey information + about the distribution of Stream Access Point types and their + associated Earliest Presentation Times. + + In the catalog, the SAP-type timeline track MUST include a + 'packaging' value of 'eventtimeline' and MUST include an 'eventType' + value of 'org.ietf.moq.cmsf.sap'. + + In the SAP Type timeline JSON payload: + + * The index reference MUST be 'l' for Location + + * The data field is a JSON Array containing two integers. The first + integer defines SAP type with an allowed value of 0,1,2 or 3. The + value 0 indicates that the Object does not start with an ISOBMFF + stream access point. The value equal to 1, 2, or 3 indicates that + the Object begins with a stream access point of SAP type 1, 2, or + + + +Law Expires 4 June 2026 [Page 5] + +Internet-Draft CMSF- a CMAF compliant implementation of December 2025 + + + 3, respectively. When the Object is the first Object in the + Group, the value MUST be equal to 1 or 2. The second integer + defines the earliest media presentation timestamp, rounded to the + nearest millisecond, of all media samples in the Object defined by + the Location of that record. + +3.6.2. SAP-type timeline track example + + This shows an example of 30-fps HEVC-encoded content, in which each + 4s Group begins with SAP-type 2 (i.e., the first picture in the Group + is an IDR picture, while there may be one or more pictures in the + Group following the IDR picture in decoding order but preceding it in + output order). After 2 seconds in each Group, there is a SAP-type 3, + i.e., a CRA picture, which is associated with one or more Random + Access Skipped Leading (RASL) pictures. A small buffer of frames (10 + frames at 30 fps) is skipped/discarded (RASL pictures) when the + streaming session starts from the SAP-type 3 location. In this + example, the EPT is the presentation time of the first picture after + the RASL pictures in decoding order; all pictures after the RASL + pictures can be fully correctly decoded and are thus presentable when + the streaming session starts from the SAP-type 3 location. Note that + if the streaming session starts from the start of the Group, then + these RASL pictures can be fully correctly decoded and are thus + presentable. + + [ + { + "l": [0,0], + "data": [2,0] + }, + { + "l": [0,60], + "data": [3,2100] + }, + { + "l": [1,0], + "data": [2,4000] + }, + { + "l": [1,60], + "data": [3,6100] + } + ] + +4. Catalog Examples + + The following section provides non-normative JSON examples of various + catalogs compliant with this draft. + + + +Law Expires 4 June 2026 [Page 6] + +Internet-Draft CMSF- a CMAF compliant implementation of December 2025 + + +4.1. Simulcast video tracks - 3 alternate video qualities along with + audio + + This example shows catalog for a media producer capable of sending 3 + time-aligned video tracks for high definition, low definition and + medium definition video qualities, along with an audio track. + + { + "version": 1, + "generatedAt": 1746104606044, + "tracks":[ + { + "name": "hd", + "renderGroup": 1, + "packaging": "cmaf", + "isLive": true, + "initData": "AAAAIGZ0eXBpc281AAA...AAAAAAAAAAAAA", + "role": "video", + "codec":"avc1.640028", + "width":1920, + "height":1080, + "bitrate":5000000, + "framerate":30, + "altGroup":1 + }, + { + "name": "md", + "renderGroup": 1, + "packaging": "cmaf", + "isLive": true, + "initData": "AAAAHGZ0eXBpc281AAA...AAAAAAAAAAAAAA", + "role": "video", + "codec":"avc1.64001e", + "width":720, + "height":640, + "bitrate":3000000, + "framerate":30, + "altGroup":1 + }, + { + "name": "sd", + "renderGroup": 1, + "packaging": "cmaf", + "isLive": true, + "initData": "AAAAHGZ0eXBpc281AAA...AAAAAAAAAAAAAA", + "role": "video", + "codec":"avc1.64000d", + "width":192, + + + +Law Expires 4 June 2026 [Page 7] + +Internet-Draft CMSF- a CMAF compliant implementation of December 2025 + + + "height":144, + "bitrate":500000, + "framerate":30, + "altGroup":1 + }, + { + "name": "audio", + "renderGroup": 1, + "packaging": "cmaf", + "isLive": true, + "initData": "AAAAHGZ0eXBpc281AAA...AAAAAAAAAAAAAA", + "role": "audio", + "codec":"mp4a.40.5", + "samplerate":48000, + "channelConfig":"2", + "bitrate":67071 + } + ] + } + +5. Conventions and Definitions + + The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and + "OPTIONAL" in this document are to be interpreted as described in + BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all + capitals, as shown here. + +6. Security Considerations + + CMSF inherits the security properties of the underlying MoQ + Transport protocol [MoQTransport] and the WebTransport or QUIC + session over which it operates. Implementations MUST ensure that + the transport layer provides confidentiality, integrity, and + authentication (e.g., via TLS 1.3). + + Threat Model: An attacker may be on-path or off-path and may + attempt to eavesdrop, inject, modify, or replay CMSF messages. + Attackers may also attempt to disrupt availability through + resource exhaustion. + + Confidentiality: Media content carried within CMSF Groups and + Objects is visible to relays and any entity with access to the + QUIC session keys. Applications requiring end-to-end media + confidentiality SHOULD encrypt media payloads above the + transport layer (e.g., using Secure Frame [SFrame]). + + Integrity: CMSF metadata (SAP types, event timelines, timing + information) MUST be integrity-protected by the underlying + transport. Receivers SHOULD validate that CMSF metadata is + consistent with the media content (e.g., declared SAP types + match actual Stream Access Points). + + Authentication and Authorization: Publishers and subscribers + SHOULD be authenticated before being allowed to announce or + consume CMSF content. Authorization policies SHOULD restrict + which paths a client may publish to or subscribe from. + Credentials SHOULD have bounded lifetimes, and implementations + SHOULD support revocation mechanisms. + + Replay and Impersonation: Implementations MUST rely on the + transport's replay protection (e.g., QUIC's nonce-based + encryption). CMSF itself does not introduce additional replay + vectors beyond those of the underlying MoQ Transport. + + Availability: Receivers MUST impose limits on the rate and size + of CMSF Objects and Groups to mitigate resource exhaustion + attacks. Relays SHOULD apply backpressure or rate limiting + to protect downstream consumers. + + Privacy: CMSF track names and catalog metadata may reveal + information about the media content or its structure. + Implementations SHOULD consider the privacy implications of + exposing track names, codec parameters, and timing metadata + to untrusted intermediaries. + + Operational Guidance: Implementations SHOULD log security- + relevant events (authentication failures, authorization + denials, malformed messages) for monitoring and incident + response. Error messages SHOULD NOT leak internal state or + configuration details to unauthenticated clients. + +7. IANA Considerations + + This document has no IANA actions. + +8. Normative References + + [BASE64] Josefsson, S., "The Base16, Base32, and Base64 Data + Encodings", RFC 4648, DOI 10.17487/RFC4648, October 2006, + . + + [CMAF] Standardization, I. O. for., "Information technology — + Multimedia application format (MPEG-A) — Part 19: Common + media application format (CMAF) for segmented media", + October 2021. + + + + + +Law Expires 4 June 2026 [Page 8] + +Internet-Draft CMSF- a CMAF compliant implementation of December 2025 + + + [MoQTransport] + Curley, L., Pugin, K., Nandakumar, S., Vasiliev, V., and + I. Swett, "Media over QUIC Transport", Work in Progress, + Internet-Draft, draft-ietf-moq-transport-10, 3 March 2025, + . + + [MSF] Law, W., Curley, L., Vasiliev, V., Nandakumar, S., and K. + Pugin, "WARP Streaming Format", Work in Progress, + Internet-Draft, draft-ietf-moq-warp-01, 22 July 2025, + . + + [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate + Requirement Levels", BCP 14, RFC 2119, + DOI 10.17487/RFC2119, March 1997, + . + + [RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC + 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, + May 2017, . + +Acknowledgments + + TODO acknowledge. + +Author's Address + + Will Law + Akamai + Email: wilaw@akamai.com + + + + + + + + + + + + + + + + + + + + +Law Expires 4 June 2026 [Page 9] diff --git a/doc/spec/draft-ietf-moq-msf-00.txt b/doc/spec/draft-ietf-moq-msf-00.txt new file mode 100644 index 000000000..dafe4e045 --- /dev/null +++ b/doc/spec/draft-ietf-moq-msf-00.txt @@ -0,0 +1,1964 @@ + + + + +Media Over QUIC W. Law +Internet-Draft Akamai +Intended status: Informational 19 January 2026 +Expires: 23 July 2026 + + + MOQT Streaming Format + draft-ietf-moq-msf-00 + +Abstract + + This document specifies the MOQT Streaming Format, designed to + operate on Media Over QUIC Transport. + +About This Document + + This note is to be removed before publishing as an RFC. + + The latest revision of this draft can be found at https://moq- + wg.github.io/msf/draft-ietf-moq-msf.html. Status information for + this document may be found at https://datatracker.ietf.org/doc/draft- + ietf-moq-msf/. + + Discussion of this document takes place on the Media Over QUIC + Working Group mailing list (mailto:moq@ietf.org), which is archived + at https://mailarchive.ietf.org/arch/browse/moq/. Subscribe at + https://www.ietf.org/mailman/listinfo/moq/. + + Source for this draft and an issue tracker can be found at + https://github.com/moq-wg/msf. + +Status of This Memo + + This Internet-Draft is submitted in full conformance with the + provisions of BCP 78 and BCP 79. + + Internet-Drafts are working documents of the Internet Engineering + Task Force (IETF). Note that other groups may also distribute + working documents as Internet-Drafts. The list of current Internet- + Drafts is at https://datatracker.ietf.org/drafts/current/. + + Internet-Drafts are draft documents valid for a maximum of six months + and may be updated, replaced, or obsoleted by other documents at any + time. It is inappropriate to use Internet-Drafts as reference + material or to cite them other than as "work in progress." + + This Internet-Draft will expire on 23 July 2026. + + + + +Law Expires 23 July 2026 [Page 1] + +Internet-Draft MOQT Streaming Format January 2026 + + +Copyright Notice + + Copyright (c) 2026 IETF Trust and the persons identified as the + document authors. All rights reserved. + + This document is subject to BCP 78 and the IETF Trust's Legal + Provisions Relating to IETF Documents (https://trustee.ietf.org/ + license-info) in effect on the date of publication of this document. + Please review these documents carefully, as they describe your rights + and restrictions with respect to this document. Code Components + extracted from this document must include Revised BSD License text as + described in Section 4.e of the Trust Legal Provisions and are + provided without warranty as described in the Revised BSD License. + +Table of Contents + + 1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . 4 + 2. Conventions and Definitions . . . . . . . . . . . . . . . . . 4 + 3. Scope . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 + 4. Media packaging . . . . . . . . . . . . . . . . . . . . . . . 6 + 4.1. LOC packaging . . . . . . . . . . . . . . . . . . . . . . 6 + 4.2. Time-alignment . . . . . . . . . . . . . . . . . . . . . 6 + 4.3. Content protection and encryption . . . . . . . . . . . . 6 + 5. Catalog . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 + 5.1. Catalog Fields . . . . . . . . . . . . . . . . . . . . . 7 + 5.1.1. MSF version . . . . . . . . . . . . . . . . . . . . . 9 + 5.1.2. Delta update . . . . . . . . . . . . . . . . . . . . 9 + 5.1.3. Add tracks . . . . . . . . . . . . . . . . . . . . . 9 + 5.1.4. Remove tracks . . . . . . . . . . . . . . . . . . . . 9 + 5.1.5. Clone tracks . . . . . . . . . . . . . . . . . . . . 10 + 5.1.6. Generated at . . . . . . . . . . . . . . . . . . . . 10 + 5.1.7. Is Complete . . . . . . . . . . . . . . . . . . . . . 10 + 5.1.8. Tracks . . . . . . . . . . . . . . . . . . . . . . . 10 + 5.1.9. Tracks object . . . . . . . . . . . . . . . . . . . . 10 + 5.1.10. Track namespace . . . . . . . . . . . . . . . . . . . 11 + 5.1.11. Track name . . . . . . . . . . . . . . . . . . . . . 11 + 5.1.12. Packaging . . . . . . . . . . . . . . . . . . . . . . 11 + 5.1.13. Event timeline type . . . . . . . . . . . . . . . . . 11 + 5.1.14. Track role . . . . . . . . . . . . . . . . . . . . . 12 + 5.1.15. Is Live . . . . . . . . . . . . . . . . . . . . . . . 13 + 5.1.16. Target latency . . . . . . . . . . . . . . . . . . . 13 + 5.1.17. Track label . . . . . . . . . . . . . . . . . . . . . 13 + 5.1.18. Render group . . . . . . . . . . . . . . . . . . . . 13 + 5.1.19. Alternate group . . . . . . . . . . . . . . . . . . . 14 + 5.1.20. Initialization data . . . . . . . . . . . . . . . . . 14 + 5.1.21. Dependencies . . . . . . . . . . . . . . . . . . . . 14 + 5.1.22. Temporal ID . . . . . . . . . . . . . . . . . . . . . 14 + 5.1.23. Spatial ID . . . . . . . . . . . . . . . . . . . . . 14 + + + +Law Expires 23 July 2026 [Page 2] + +Internet-Draft MOQT Streaming Format January 2026 + + + 5.1.24. Codec . . . . . . . . . . . . . . . . . . . . . . . . 15 + 5.1.25. Mimetype . . . . . . . . . . . . . . . . . . . . . . 15 + 5.1.26. Framerate . . . . . . . . . . . . . . . . . . . . . . 15 + 5.1.27. Timescale . . . . . . . . . . . . . . . . . . . . . . 15 + 5.1.28. Bitrate . . . . . . . . . . . . . . . . . . . . . . . 15 + 5.1.29. Width . . . . . . . . . . . . . . . . . . . . . . . . 15 + 5.1.30. Height . . . . . . . . . . . . . . . . . . . . . . . 15 + 5.1.31. Audio sample rate . . . . . . . . . . . . . . . . . . 16 + 5.1.32. Channel configuration . . . . . . . . . . . . . . . . 16 + 5.1.33. Display width . . . . . . . . . . . . . . . . . . . . 16 + 5.1.34. Display height . . . . . . . . . . . . . . . . . . . 16 + 5.1.35. Language . . . . . . . . . . . . . . . . . . . . . . 16 + 5.1.36. Parent name . . . . . . . . . . . . . . . . . . . . . 16 + 5.1.37. Track duration . . . . . . . . . . . . . . . . . . . 17 + 5.2. Delta updates . . . . . . . . . . . . . . . . . . . . . . 17 + 5.3. Catalog Examples . . . . . . . . . . . . . . . . . . . . 18 + 5.3.1. Time-aligned Audio/Video Tracks with single + quality . . . . . . . . . . . . . . . . . . . . . . . 18 + 5.3.2. Simulcast video tracks - 3 alternate qualities along + with audio . . . . . . . . . . . . . . . . . . . . . 19 + 5.3.3. SVC video tracks with 2 spatial and 2 temporal + qualities . . . . . . . . . . . . . . . . . . . . . . 21 + 5.3.4. Delta update - adding two tracks . . . . . . . . . . 23 + 5.3.5. Delta update removing tracks . . . . . . . . . . . . 24 + 5.3.6. Time-aligned Audio/Video Tracks with custom field + values . . . . . . . . . . . . . . . . . . . . . . . 24 + 5.3.7. Time-aligned VOD Audio/Video Tracks . . . . . . . . . 25 + 5.3.8. Media timeline and Event timeline . . . . . . . . . . 26 + 5.3.9. Terminating a live broadcast . . . . . . . . . . . . 27 + 6. Media transmission . . . . . . . . . . . . . . . . . . . . . 28 + 6.1. Group numbering . . . . . . . . . . . . . . . . . . . . . 28 + 7. Media Timeline track . . . . . . . . . . . . . . . . . . . . 28 + 7.1. Media Timeline track payload . . . . . . . . . . . . . . 28 + 7.2. Media Timeline Catalog requirements . . . . . . . . . . . 29 + 7.3. Media Timeline track updating . . . . . . . . . . . . . . 29 + 8. Event Timeline track . . . . . . . . . . . . . . . . . . . . 29 + 8.1. Event Timeline data format . . . . . . . . . . . . . . . 30 + 8.2. Event Timeline Catalog requirements . . . . . . . . . . . 30 + 8.3. Event Timeline track updating . . . . . . . . . . . . . . 31 + 8.4. Event timeline track examples . . . . . . . . . . . . . . 31 + 8.4.1. Event timeline track with wallclock time indexing . . 31 + 8.4.2. Event timeline track with MOQT Location indexing . . 31 + 9. Workflow . . . . . . . . . . . . . . . . . . . . . . . . . . 32 + 9.1. Initiating a broadcast . . . . . . . . . . . . . . . . . 32 + 9.2. Ending a live broadcast . . . . . . . . . . . . . . . . . 32 + 10. Security Considerations . . . . . . . . . . . . . . . . . . . 32 + 11. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 32 + 12. Normative References . . . . . . . . . . . . . . . . . . . . 33 + + + +Law Expires 23 July 2026 [Page 3] + +Internet-Draft MOQT Streaming Format January 2026 + + + Acknowledgments . . . . . . . . . . . . . . . . . . . . . . . . . 34 + Contributors . . . . . . . . . . . . . . . . . . . . . . . . . . 34 + Author's Address . . . . . . . . . . . . . . . . . . . . . . . . 34 + +1. Introduction + + MOQT Streaming Format (MSF) is a media format designed to deliver LOC + [LOC] compliant media content over Media Over QUIC Transport (MOQT) + [MoQTransport]. MSF works by fragmenting the bitstream into objects + that can be independently transmitted. MSF leverages a catalog + format to describe the output of the original publisher. MSF + specifies how content should be packaged and signaled, defines how + the catalog communicates the content, specifies prioritization + strategies for real-time and workflows for beginning and terminating + broadcasts. MSF also details how end-subscribers may perform + adaptive bitrate switching. MSF is targeted at real-time and + interactive levels of live latency, as well as VOD content. + + This document describes version 1 of the streaming format. + +2. Conventions and Definitions + + The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and + "OPTIONAL" in this document are to be interpreted as described in + BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all + capitals, as shown here. + + This document uses the conventions detailed in Section 1.3 of + [RFC9000] when describing the binary encoding. + +3. Scope + + The purpose of MSF is to provide an interoperable media streaming + format operating over [MoQTransport]. Interoperability implies that: + + * An original publisher can package incoming media content into + tracks, prepare a catalog and announce the availability of the + content to an MOQT relay. Media content refers to audio and video + data, as well as ancillary data such as captions, subtitles, + accessibility and other timed-text data. + + * An MOQT relay can process the announcement as well as cache and + propagate the tracks, both to other relays or to the final + subscriber. + + * A final subscriber can parse the catalog, request tracks, decode + and render the received media data. + + + +Law Expires 23 July 2026 [Page 4] + +Internet-Draft MOQT Streaming Format January 2026 + + + MSF is intended to provide a format for delivering commercial media + content. To that end, the following features are within scope: + + * Video codecs - all codecs supported by [LOC] + + * Audio codecs - all audio codecs supported by [LOC] + + * Catalog track - describes the availability and characteristics of + content produced by the original publisher. + + * Timeline track - describes the relationship between MOQT Group and + Object IDs to media time. + + * Token-based authorization and access control + + * Captions + Subtitles - support for [WEBVTT] and [IMSC1] + transmission + + * Latency support across multiple regimes (thresholds are + informative only and describe the delay between the original + publisher placing the content on the wire and the final subscriber + rendering it) + + * Real-time - less than 500ms + + * Interactive - between 500ms and 2500ms + + * Standard - above 2500ms + + * VOD latency - content that was previously produced, is no longer + live and is available indefinitely. + + * Content encryption + + * ABR between time-synced tracks - subscribers may switch between + tracks at different quality levels in order to maximize visual or + audio quality under conditions of throughput variability. + + * Capable of delivering interstitial advertising. + + * Logs and analytics management - support for the reporting of + client-side QoE and relay delivery actions. + + Initial versions of MSF will prioritize basic features necessary to + exercise interoperability across delivery systems. Later versions + will add commercially necessary features. + + + + + +Law Expires 23 July 2026 [Page 5] + +Internet-Draft MOQT Streaming Format January 2026 + + +4. Media packaging + + MSF delivers LOC [LOC] packaged media bitstreams. + +4.1. LOC packaging + + This specification references Low Overhead Container (LOC) [LOC] to + define how audio and video content is packaged. With this packaging + mode, each EncodedAudioChunk or EncodedVideoChunk sample is placed in + a separate MOQT Object. Samples that belong to the same Group of + Pictures (GOP) MUST be placed within the same MOQT Group. + + When LOC packaging is used for a track, the catalog packaging + attribute (Section 5.1.12) MUST be present and it MUST be populated + with a value of "loc". + +4.2. Time-alignment + + MSF Tracks MAY be time-aligned. Those that are, are subject to the + following requirements: + + * Tracks advertised in the catalog as belonging to a common render + group MUST be time-aligned. + + * The render duration of the first media object of each equally + numbered MOQT Group, after decoding, MUST have overlapping + presentation time. + + A consequence of this restriction is that an MSF receiver SHOULD be + able to cleanly switch between time-aligned media tracks at group + boundaries. + +4.3. Content protection and encryption + + ToDo - content protection for LOC-packaged content. + +5. Catalog + + A Catalog is an MOQT Track that provides information about the other + tracks being produced by a MSF publisher. A Catalog is used by MSF + publishers for advertising their output and for subscribers in + consuming that output. The payload of the Catalog object is opaque + to Relays and can be end-to-end encrypted. The Catalog provides the + names and namespaces of the tracks being produced, along with the + relationship between tracks, properties of the tracks that consumers + may use for selection and any relevant initialization data. + + The catalog track MUST have a case-sensitive Track Name of "catalog". + + + +Law Expires 23 July 2026 [Page 6] + +Internet-Draft MOQT Streaming Format January 2026 + + + A catalog object MAY be independent of other catalog objects or it + MAY represent a delta update of a prior catalog object. The first + catalog object published within a new group MUST be independent. A + catalog object SHOULD be published only when the availability of + tracks changes. + + Each catalog update MUST be mapped to an MOQT Object. + +5.1. Catalog Fields + + A catalog is a JSON [JSON] document, comprised of a series of + mandatory and optional fields. At a minimum, a catalog MUST provide + all mandatory fields. A producer MAY add additional fields to the + ones described in this draft. Custom field names MUST NOT collide + with field names described in this draft. The order of field names + within the JSON document is not important. + + A parser MUST ignore fields it does not understand. + + Table 1 provides an overview of all fields defined by this document. + + +=======================+===============+================+ + | Field | Name | Definition | + +=======================+===============+================+ + | MSF version | version | Section 5.1.1 | + +-----------------------+---------------+----------------+ + | Delta update | deltaUpdate | Section 5.1.2 | + +-----------------------+---------------+----------------+ + | Add tracks | addTracks | Section 5.1.3 | + +-----------------------+---------------+----------------+ + | Remove tracks | removeTracks | Section 5.1.4 | + +-----------------------+---------------+----------------+ + | Clone tracks | cloneTracks | Section 5.1.5 | + +-----------------------+---------------+----------------+ + | Generated at | generatedAt | Section 5.1.6 | + +-----------------------+---------------+----------------+ + | Is Complete | isComplete | Section 5.1.7 | + +-----------------------+---------------+----------------+ + | Tracks | tracks | Section 5.1.8 | + +-----------------------+---------------+----------------+ + | Track namespace | namespace | Section 5.1.10 | + +-----------------------+---------------+----------------+ + | Track name | name | Section 5.1.11 | + +-----------------------+---------------+----------------+ + | Packaging | packaging | Section 5.1.12 | + +-----------------------+---------------+----------------+ + | Event timeline type | eventType | Section 5.1.13 | + +-----------------------+---------------+----------------+ + + + +Law Expires 23 July 2026 [Page 7] + +Internet-Draft MOQT Streaming Format January 2026 + + + | Is Live | isLive | Section 5.1.15 | + +-----------------------+---------------+----------------+ + | Target latency | targetLatency | Section 5.1.16 | + +-----------------------+---------------+----------------+ + | Track role | role | Section 5.1.14 | + +-----------------------+---------------+----------------+ + | Track label | label | Section 5.1.17 | + +-----------------------+---------------+----------------+ + | Render group | renderGroup | Section 5.1.18 | + +-----------------------+---------------+----------------+ + | Alternate group | altGroup | Section 5.1.19 | + +-----------------------+---------------+----------------+ + | Initialization data | initData | Section 5.1.20 | + +-----------------------+---------------+----------------+ + | Dependencies | depends | Section 5.1.21 | + +-----------------------+---------------+----------------+ + | Temporal ID | temporalId | Section 5.1.22 | + +-----------------------+---------------+----------------+ + | Spatial ID | spatialId | Section 5.1.23 | + +-----------------------+---------------+----------------+ + | Codec | codec | Section 5.1.24 | + +-----------------------+---------------+----------------+ + | Mime type | mimeType | Section 5.1.25 | + +-----------------------+---------------+----------------+ + | Framerate | framerate | Section 5.1.26 | + +-----------------------+---------------+----------------+ + | Timescale | timescale | Section 5.1.27 | + +-----------------------+---------------+----------------+ + | Bitrate | bitrate | Section 5.1.28 | + +-----------------------+---------------+----------------+ + | Width | width | Section 5.1.29 | + +-----------------------+---------------+----------------+ + | Height | height | Section 5.1.30 | + +-----------------------+---------------+----------------+ + | Audio sample rate | samplerate | Section 5.1.31 | + +-----------------------+---------------+----------------+ + | Channel configuration | channelConfig | Section 5.1.32 | + +-----------------------+---------------+----------------+ + | Display width | displayWidth | Section 5.1.33 | + +-----------------------+---------------+----------------+ + | Display height | displayHeight | Section 5.1.34 | + +-----------------------+---------------+----------------+ + | Language | lang | Section 5.1.35 | + +-----------------------+---------------+----------------+ + | Parent name | parentName | Section 5.1.36 | + +-----------------------+---------------+----------------+ + | Track duration | trackDuration | Section 5.1.37 | + +-----------------------+---------------+----------------+ + + + +Law Expires 23 July 2026 [Page 8] + +Internet-Draft MOQT Streaming Format January 2026 + + + Table 1 + + Table 2 defines the allowed locations for these fields within the + document + + +==========+=================================+ + | Location | Allowed locations for the field | + +==========+=================================+ + | R | The Root of the JSON object | + +----------+---------------------------------+ + | T | Track object | + +----------+---------------------------------+ + + Table 2 + +5.1.1. MSF version + + Location: R Required: Yes JSON Type: Number + + Specifies the version of MSF referenced by this catalog. There is no + guarantee that future catalog versions are backwards compatible and + field definitions and interpretation may change between versions. A + subscriber MUST NOT attempt to parse a catalog version which it does + not understand. + +5.1.2. Delta update + + Location: R Required: Optional JSON Type: Boolean + + A Boolean that if true indicates that this catalog object represents + a delta (or partial) update. A delta update has a restricted set of + fields and special processing rules - see Section 5.2. This value + SHOULD NOT be added to a catalog if it is false. + +5.1.3. Add tracks + + Location: R Required: Optional JSON Type: Array + + Indicates a delta processing instruction to add new tracks. The + value of this field is an Array of track objects Section 5.1.9. + +5.1.4. Remove tracks + + Location: R Required: Optional JSON Type: Array + + + + + + + +Law Expires 23 July 2026 [Page 9] + +Internet-Draft MOQT Streaming Format January 2026 + + + Indicates a delta processing instruction to remove new tracks. The + value of this field is an Array of track objects Section 5.1.9. Each + track object MUST include a Track Name Section 5.1.11 field, MAY + include a Track Namespace Section 5.1.10 field and MUST NOT hold any + other fields. + +5.1.5. Clone tracks + + Location: R Required: Optional JSON Type: Array + + Indicates a delta processing instruction to clone new tracks from + previously declared tracks. The value of this field is an Array of + track objects Section 5.1.9. Each track object MUST include a Parent + Name Section 5.1.36 field. + +5.1.6. Generated at + + Location: R Required: Optional JSON Type: Number + + The wallclock time at which this catalog instance was generated, + expressed as the number of milliseconds that have elapsed since + January 1, 1970 (midnight UTC/GMT). This field SHOULD NOT be + included if the isLive field is false. + +5.1.7. Is Complete + + Location: R Required: Optional JSON Type: Boolean + + Issued once a previously live broadcast is complete. This is a + commitment that all tracks are complete, no new tracks will be added + and no new content will be published. This field MUST NOT be + included if it is FALSE. This field MUST NOT be removed from a + catalog once it has been added. + +5.1.8. Tracks + + Location: R Required: Yes JSON Type: Array + + An array of track objects Section 5.1.9. + +5.1.9. Tracks object + + A track object is JSON Object containing a collection of fields whose + location is specified 'T' in Table 2. + + + + + + + +Law Expires 23 July 2026 [Page 10] + +Internet-Draft MOQT Streaming Format January 2026 + + +5.1.10. Track namespace + + Location: T Required: Optional JSON Type: String + + The name space under which the track name is defined. See section + 2.3 of [MoQTransport]. The track namespace is optional. If it is + not declared within a track, then each track MUST inherit the + namespace of the catalog track. A namespace declared in a track + object overwrites any inherited name space. + +5.1.11. Track name + + Location: T Required: Yes JSON Type: String + + A string defining the name of the track. See section 2.3 of + [MoQTransport]. Within the catalog, track names MUST be unique per + namespace. + +5.1.12. Packaging + + Location: T Required: Yes JSON Type: String + + A string defining the type of payload encapsulation. Allowed values + are strings as defined in Table 3. + + Table 3: Allowed packaging values + + +================+===============+===============+ + | Name | Value | Reference | + +================+===============+===============+ + | LOC | loc | See RFC XXXX | + +----------------+---------------+---------------+ + | Media Timeline | mediatimeline | See Section 7 | + +----------------+---------------+---------------+ + | Event Timeline | eventtimeline | See Section 8 | + +----------------+---------------+---------------+ + + Table 3 + +5.1.13. Event timeline type + + Location: T Required: Optional JSON Type: String + + + + + + + + + +Law Expires 23 July 2026 [Page 11] + +Internet-Draft MOQT Streaming Format January 2026 + + + A String defining the type & structure of the data contained within + the data field of the Event timeline track. Types are defined by the + application provider and are not centrally registered. Implementers + are encouraged to use a unique naming scheme, such as Reverse Domain + Name Notation, to avoid naming collisions. This field is required if + the Section 5.1.12 value is "eventtimeline". This field MUST NOT be + used if the packaging value is not "eventtimeline". + +5.1.14. Track role + + Location: T Required: Optional JSON Type: String + + A string defining the role of content carried by the track. + Specified roles are described in Table 4. These role values are + case-sensitive. + + This role field MAY be used in conjunction with the Mimetype + Section 5.1.25 to fully describe the content of the track. + + Table 4: Reserved track roles + + +==================+==========================+ + | Role | Description | + +==================+==========================+ + | audiodescription | An audio description for | + | | visually impaired users | + +------------------+--------------------------+ + | video | Visual content | + +------------------+--------------------------+ + | audio | Audio content | + +------------------+--------------------------+ + | mediatimeline | An MSF media timeline | + | | Section 7 | + +------------------+--------------------------+ + | eventtimeline | An MSF event timeline | + | | Section 8 | + +------------------+--------------------------+ + | caption | A textual representation | + | | of the audio track | + +------------------+--------------------------+ + | subtitle | A transcription of the | + | | spoken dialogue | + +------------------+--------------------------+ + | signlanguage | A visual track for | + | | hearing impaired users. | + +------------------+--------------------------+ + + Table 4 + + + +Law Expires 23 July 2026 [Page 12] + +Internet-Draft MOQT Streaming Format January 2026 + + + Custom roles MAY be used as long as they do not collide with the + specified roles. + +5.1.15. Is Live + + Location: T Required: Yes JSON Type: Boolean + + True if new Objects will be added to the track. False if no new + Objects will be added to the track. This is sent under two possible + conditions: * the publisher of a previously live track has ended the + track. * the track is Video-On-Demand (VOD) and was never live. + +5.1.16. Target latency + + Location: T Required: Optional JSON Type: Number + + The target latency in milliseconds. Target latency is defined as the + offset in wallclock time between when content was encoded and when it + is displayed to the end user. For example, if a frame of video is + encoded at 10:08:32.638 UTC and the target latency is 5000, then that + frame should be rendered to the end-user at 10:08:37.638 UTC. This + field MUST NOT be included if isLive is FALSE. All tracks belonging + to the same render group MUST have identical target latencies. All + tracks belonging to the same alternate group MUST have identical + target latencies. If this field is absent from the track definition, + then the player MAY choose the latency with which it renders the + content. + +5.1.17. Track label + + Location: T Required: Optional JSON Type: String + + A string defining a human-readable label for the track. Examples + might be "Overhead camera view" or "Deutscher Kommentar". Note that + the [JSON] spec requires UTF-8 support by decoders. + +5.1.18. Render group + + Location: T Required: Optional JSON Type: Number + + An integer specifying a group of tracks which are designed to be + rendered together. Tracks with the same group number SHOULD be + rendered simultaneously, are time-aligned and are designed to + accompany one another. A common example would be tying together + audio and video tracks. + + + + + + +Law Expires 23 July 2026 [Page 13] + +Internet-Draft MOQT Streaming Format January 2026 + + +5.1.19. Alternate group + + Location: T Required: Optional JSON Type: Number + + An integer specifying a group of tracks which are alternate versions + of one-another. Alternate tracks represent the same media content, + but differ in their selection properties. Alternate tracks MUST have + matching media time sequences. A subscriber typically subscribes to + one track from a set of tracks specifying the same alternate group + number. A common example would be a set video tracks of the same + content offered in alternate bitrates. + +5.1.20. Initialization data + + Location: T Required: Optional JSON Type: String + + A string holding Base64 [BASE64] encoded initialization data for the + track. + +5.1.21. Dependencies + + Location: T Required: Optional JSON Type: Array + + Certain tracks may depend on other tracks for decoding. Dependencies + holds an array of track names Section 5.1.11 on which the current + track is dependent. Since only the track name is signaled, the + namespace of the dependencies is assumed to match that of the track + declaring the dependencies. + +5.1.22. Temporal ID + + Location: T Required: Optional JSON Type: Number + + A number identifying the temporal layer/sub-layer encoding of the + track, starting with 0 for the base layer, and increasing by 1 for + the next higher temporal fidelity. + +5.1.23. Spatial ID + + Location: T Required: Optional JSON Type: Number + + A number identifying the spatial layer encoding of the track, + starting with 0 for the base layer, and increasing by 1 for the next + higher fidelity. + + + + + + + +Law Expires 23 July 2026 [Page 14] + +Internet-Draft MOQT Streaming Format January 2026 + + +5.1.24. Codec + + Location: T Required: Optional JSON Type: String + + A string defining the codec used to encode the track. For LOC + packaged content, the string codec registrations are defined in Sect + 3 and Section 4 of [WEBCODECS-CODEC-REGISTRY]. + +5.1.25. Mimetype + + Location: T Required: Optional JSON Type: String + + A string defining the mime type [MIME] of the track. + +5.1.26. Framerate + + Location: T Required: Optional JSON Type: Number + + A number defining the video framerate of the track, expressed as + frames per second. + +5.1.27. Timescale + + Location: T Required: Optional JSON Type: Number + + The number of time units that pass per second. + +5.1.28. Bitrate + + Location: T Required: Optional JSON Type: Number + + A number defining the bitrate of track, expressed in bits per second. + +5.1.29. Width + + Location: T Required: Optional JSON Type: Number + + A number expressing the encoded width of the video frames in pixels. + +5.1.30. Height + + Location: T Required: Optional JSON Type: Number + + A number expressing the encoded height of the video frames in pixels. + + + + + + + +Law Expires 23 July 2026 [Page 15] + +Internet-Draft MOQT Streaming Format January 2026 + + +5.1.31. Audio sample rate + + Location: T Required: Optional JSON Type: Number + + The number of audio frame samples per second. This property SHOULD + only accompany audio codecs. + +5.1.32. Channel configuration + + Location: T Required: Optional JSON Type: String + + A string specifying the audio channel configuration. This property + SHOULD only accompany audio codecs. A string is used in order to + provide the flexibility to describe complex channel configurations + for multi-channel and Next Generation Audio schemas. + +5.1.33. Display width + + Location: T Required: Optional JSON Type: Number + + A number expressing the intended display width of the track content + in pixels. + +5.1.34. Display height + + Location: T Required: Optional JSON Type: Number + + A number expressing the intended display height of the track content + in pixels. + +5.1.35. Language + + Location: T Required: Optional JSON Type: String + + A string defining the dominant language of the track. The string + MUST be one of the standard Tags for Identifying Languages as defined + by [LANG]. + +5.1.36. Parent name + + Location: T Required: Optional JSON Type: String + + A string defining the parent track name Section 5.1.11 to be cloned. + This field MUST only be included inside a Clone tracks Section 5.1.5 + object. + + + + + + +Law Expires 23 July 2026 [Page 16] + +Internet-Draft MOQT Streaming Format January 2026 + + +5.1.37. Track duration + + Location: T Required: Optional JSON Type: Number + + The duration of the track expressed in integer milliseconds. This + field MUST NOT be included if the isLive Section 5.1.15 field value + is true. + +5.2. Delta updates + + A catalog update might contain incremental changes. This is a useful + property if many tracks may be initially declared but then there are + small changes to a subset of tracks. The producer can issue a delta + update to describe these changes. Changes are described + incrementally, meaning that a delta update can itself modify a prior + delta update. + + A restricted set of operations are allowed with each delta update: * + Add a new track that has not previously been declared. * Add a new + track by cloning a previously declared track. * Remove a track that + has been previously declared. + + The following rules are to be followed in constructing and processing + delta updates: + + * A delta update MUST include the Delta Update Section 5.1.2 field + set to true. + + * A delta update catalog MUST contain at least one instance of Add + tracks Section 5.1.3, Remove tracks Section 5.1.4 or Clone Tracks + Section 5.1.5 fields and MAY contain more. It MUST NOT contain an + instance of a Tracks Section 5.1.8 field or an MSF version + Section 5.1.1 field. + + * The Add, Delete and Clone operations are applied sequentially in + the order they are declared in the document. Each operation in + the sequence is applied to the target document; the resulting + document becomes the target of the next operation. Evaluation + continues until all operations are successfully applied. + + * A Cloned track inherits all the attributes of the track defined by + the Parent Name Section 5.1.36, except the Track Name which MUST + be new. Attributes redefined in the cloning Object overwrite + inherited values. + + + + + + + +Law Expires 23 July 2026 [Page 17] + +Internet-Draft MOQT Streaming Format January 2026 + + + * The tuple of Track Namespace and Track Name defines a fixed set of + Track attributes which MUST NOT be modified after being declared. + To modify any attribute, a new track with a different + Namespace|Name tuple is created by Adding or Cloning and then the + old track is removed. + +5.3. Catalog Examples + + The following section provides non-normative JSON examples of various + catalogs compliant with this draft. + +5.3.1. Time-aligned Audio/Video Tracks with single quality + + This example shows a catalog for a media producer capable of sending + LOC packaged, time-aligned audio and video tracks. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Law Expires 23 July 2026 [Page 18] + +Internet-Draft MOQT Streaming Format January 2026 + + + { + "version": 1, + "generatedAt": 1746104606044, + "tracks": [ + { + "name": "1080p-video", + "namespace": "conference.example.com/conference123/alice", + "packaging": "loc", + "isLive": true, + "targetLatency": 2000, + "role": "video", + "renderGroup": 1, + "codec":"av01.0.08M.10.0.110.09", + "width":1920, + "height":1080, + "framerate":30, + "bitrate":1500000 + }, + { + "name": "audio", + "namespace": "conference.example.com/conference123/alice", + "packaging": "loc", + "isLive": true, + "targetLatency": 2000, + "role": "audio", + "renderGroup": 1, + "codec":"opus", + "samplerate":48000, + "channelConfig":"2", + "bitrate":32000 + } + ] + } + +5.3.2. Simulcast video tracks - 3 alternate qualities along with audio + + This example shows catalog for a media producer capable of sending 3 + time-aligned video tracks for high definition, low definition and + medium definition video qualities, along with an audio track. In + this example the namespace is absent, which infers that each track + must inherit the namespace of the catalog. + + { + "version": 1, + "generatedAt": 1746104606044, + "tracks":[ + { + "name": "hd", + + + +Law Expires 23 July 2026 [Page 19] + +Internet-Draft MOQT Streaming Format January 2026 + + + "renderGroup": 1, + "packaging": "loc", + "isLive": true, + "targetLatency": 1500, + "role": "video", + "codec":"av01", + "width":1920, + "height":1080, + "bitrate":5000000, + "framerate":30, + "altGroup":1 + }, + { + "name": "md", + "renderGroup": 1, + "packaging": "loc", + "isLive": true, + "targetLatency": 1500, + "role": "video", + "codec":"av01", + "width":720, + "height":640, + "bitrate":3000000, + "framerate":30, + "altGroup":1 + }, + { + "name": "sd", + "renderGroup": 1, + "packaging": "loc", + "isLive": true, + "targetLatency": 1500, + "role": "video", + "codec":"av01", + "width":192, + "height":144, + "bitrate":500000, + "framerate":30, + "altGroup":1 + }, + { + "name": "audio", + "renderGroup": 1, + "packaging": "loc", + "isLive": true, + "targetLatency": 1500, + "role": "audio", + "codec":"opus", + + + +Law Expires 23 July 2026 [Page 20] + +Internet-Draft MOQT Streaming Format January 2026 + + + "samplerate":48000, + "channelConfig":"2", + "bitrate":32000 + } + ] + } + +5.3.3. SVC video tracks with 2 spatial and 2 temporal qualities + + This example shows catalog for a media producer capable of sending + scalable video codec with 2 spatial and 2 temporal layers with a + dependency relation as shown below: + + +----------+ + +----------->| S1T1 | + | | 1080p30 | + | +----------+ + | ^ + | | + +----------+ | + | S1TO | | + | 1080p15 | | + +----------+ +-----+----+ + ^ | SOT1 | + | | 480p30 | + | +----------+ + | ^ + +----------+ | + | SOTO | | + | 480p15 |---------+ + +----------+ + + The corresponding catalog uses "depends" attribute to express the + track relationships. + + { + "version": 1, + "generatedAt": 1746104606044, + "tracks":[ + { + "name": "480p15", + "namespace": "conference.example.com/conference123/alice", + "renderGroup": 1, + "packaging": "loc", + "isLive": true, + "role": "video", + "codec":"av01.0.01M.10.0.110.09", + "width":640, + + + +Law Expires 23 July 2026 [Page 21] + +Internet-Draft MOQT Streaming Format January 2026 + + + "height":480, + "bitrate":3000000, + "framerate":15 + }, + { + "name": "480p30", + "namespace": "conference.example.com/conference123/alice", + "renderGroup": 1, + "packaging": "loc", + "isLive": true, + "role": "video", + "codec":"av01.0.04M.10.0.110.09", + "width":640, + "height":480, + "bitrate":3000000, + "framerate":30, + "depends": ["480p15"] + }, + { + "name": "1080p15", + "namespace": "conference.example.com/conference123/alice", + "renderGroup": 1, + "packaging": "loc", + "isLive": true, + "role": "video", + "codec":"av01.0.05M.10.0.110.09", + "width":1920, + "height":1080, + "bitrate":3000000, + "framerate":15, + "depends":["480p15"] + }, + + { + "name": "1080p30", + "namespace": "conference.example.com/conference123/alice", + "renderGroup": 1, + "packaging": "loc", + "isLive": true, + "role": "video", + "codec":"av01.0.08M.10.0.110.09", + "width":1920, + "height":1080, + "bitrate":5000000, + "framerate":30, + "depends": ["480p30", "1080p15"] + }, + { + + + +Law Expires 23 July 2026 [Page 22] + +Internet-Draft MOQT Streaming Format January 2026 + + + "name": "audio", + "namespace": "conference.example.com/conference123/alice", + "renderGroup": 1, + "packaging": "loc", + "isLive": true, + "role": "audio", + "codec":"opus", + "samplerate":48000, + "channelConfig":"2", + "bitrate":32000 + } + ] + } + +5.3.4. Delta update - adding two tracks + + This example shows the catalog delta update for a media producer + adding two tracks to an established video conference. One track is + newly declared, the other is cloned from a previous track. + + { + "deltaUpdate": true, + "generatedAt": 1746104606044, + "addTracks": [ + { + "name": "slides", + "isLive": true, + "role": "video", + "codec": "av01.0.08M.10.0.110.09", + "width": 1920, + "height": 1080, + "framerate": 15, + "bitrate": 750000, + "renderGroup": 1 + } + ], + "cloneTracks": [ + { + "parentName": "video-1080", + "name": "video-720", + "width":1280, + "height":720, + "bitrate":600000 + } + ] + } + + + + + +Law Expires 23 July 2026 [Page 23] + +Internet-Draft MOQT Streaming Format January 2026 + + +5.3.5. Delta update removing tracks + + This example shows a delta update for a media producer removing two + tracks from an established video conference. + + { + "deltaUpdate": true, + "generatedAt": 1746104606044, + "removeTracks": [{"name": "video"},{"name": "slides"}] + } + +5.3.6. Time-aligned Audio/Video Tracks with custom field values + + This example shows catalog for a media producer capable of sending + LOC packaged, time-aligned audio and video tracks along with custom + fields in each track description. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Law Expires 23 July 2026 [Page 24] + +Internet-Draft MOQT Streaming Format January 2026 + + + { + "version": 1, + "generatedAt": 1746104606044, + "tracks": [ + { + "name": "1080p-video", + "namespace": "conference.example.com/conference123/alice", + "packaging": "loc", + "isLive": true, + "role": "video", + "renderGroup": 1, + "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", + "namespace": "conference.example.com/conference123/alice", + "packaging": "loc", + "isLive": true, + "role": "audio", + "renderGroup": 1, + "codec":"opus", + "samplerate":48000, + "channelConfig":"2", + "bitrate":32000 + } + ] + } + +5.3.7. Time-aligned VOD Audio/Video Tracks + + This example shows catalog for a media producer offering VOD (video + on-demand) non-live content. The content is LOC packaged, and + includes time-aligned audio and video tracks. + + + + + + + + + + + +Law Expires 23 July 2026 [Page 25] + +Internet-Draft MOQT Streaming Format January 2026 + + + { + "version": 1, + "tracks": [ + { + "name": "video", + "namespace": "movies.example.com/assets/boy-meets-girl-season3/episode5", + "packaging": "loc", + "isLive": false, + "trackDuration": 8072340, + "renderGroup": 1, + "codec":"av01.0.08M.10.0.110.09", + "width":1920, + "height":1080, + "framerate":30, + "bitrate":1500000 + }, + { + "name": "audio", + "namespace": "movies.example.com/assets/boy-meets-girl-season3/episode5", + "packaging": "loc", + "isLive": false, + "trackDuration": 8072340, + "renderGroup": 1, + "codec":"opus", + "samplerate":48000, + "channelConfig":"2", + "bitrate":32000 + } + ] + } + +5.3.8. Media timeline and Event timeline + + This example shows a catalog for a media producer capable of sending + LOC packaged, time-aligned audio and video tracks, along with a Media + Timeline which describes the history of those tracks and an Event + Timeline providing synchronized data. + + { + "version": 1, + "generatedAt": 1746104606044, + "tracks": [ + { + "name": "history", + "namespace": "conference.example.com/conference123/alice", + "packaging": "mediatimeline", + "mimetype": "application/json", + "depends": ["1080p-video","audio"] + + + +Law Expires 23 July 2026 [Page 26] + +Internet-Draft MOQT Streaming Format January 2026 + + + }, + { + "name": "identified-objects", + "namespace": "another-provider/time-synchronized-data", + "packaging": "eventtimeline", + "eventType": "com.ai-extraction/appID/v3", + "mimetype": "application/json", + "depends": ["1080p-video"] + }, + { + "name": "1080p-video", + "namespace": "conference.example.com/conference123/alice", + "packaging": "loc", + "isLive": true, + "targetLatency": 2000, + "role": "video", + "renderGroup": 1, + "codec":"av01.0.08M.10.0.110.09", + "width":1920, + "height":1080, + "framerate":30, + "bitrate":1500000 + }, + { + "name": "audio", + "namespace": "conference.example.com/conference123/alice", + "packaging": "loc", + "isLive": true, + "targetLatency": 2000, + "role": "audio", + "renderGroup": 1, + "codec":"opus", + "samplerate":48000, + "channelConfig":"2", + "bitrate":32000 + } + ] + } + +5.3.9. Terminating a live broadcast + + This example shows catalog for a media producer terminating a + previously live broadcast containing a video and an audio track. + + + + + + + + +Law Expires 23 July 2026 [Page 27] + +Internet-Draft MOQT Streaming Format January 2026 + + + { + "version": 1, + "generatedAt": 1746104606044, + "isComplete": true, + "tracks": [] + } + +6. Media transmission + + The MOQT Groups and MOQT Objects need to be mapped to MOQT Streams. + Irrespective of the Section 4 in place, each MOQT Object MUST be + mapped to a new MOQT Stream. + +6.1. Group numbering + + The Group ID of the first Group published in a track at application + startup MUST be a unique integer that will not repeat in the future. + One approach to achieve this is to set the initial Group ID to the + creation time of the first Object in the group, represented as the + number of milliseconds since the Unix epoch, rounded to the nearest + millisecond. This ensures that republishing the same track in the + future, such as after a loss of connectivity or an encoder restart, + will not result in smaller or duplicate Group IDs for the same track + name. Note that this method does not prevent duplication if more + than 1000 groups are published per second. + + Each subsequent Group ID MUST increase by 1. + + If a publisher is able to maintain state across a republish, it MUST + signal the gap in Group IDs using the MOQT Prior Group ID Gap + Extension header. + +7. Media Timeline track + + The media timeline track provides data about the previously published + groups and their relationship to wallclock time and media time. + Media timeline tracks allow players to seek to precise points behind + the live head in a live broadcast, or for random access in a VOD + asset. Media timeline tracks are optional. Multiple media timeline + tracks can exist inside a catalog. + +7.1. Media Timeline track payload + + A media timeline track is a JSON [JSON] document. This document MAY + be compressed using GZIP [GZIP]. The document contains an array of + records. Each record consists of an array of three required items, + whose ordinal position defines their type: + + + + +Law Expires 23 July 2026 [Page 28] + +Internet-Draft MOQT Streaming Format January 2026 + + + * The first item holds the media presentation timestamp, expressed + as a JSON Number. This value MUST match the media presentation + timestamp, rounded to the nearest millisecond, of the first media + sample in the referenced Object + + * The second item holds the MOQT Location of the entry, defined as a + tuple of the MOQT Group ID and MOQT Object ID, and expressed as a + JSON Array of Numbers, where the first number is the Group ID and + the second number is the Object ID. + + * The third time holds the wallclock time at which the media was + encoded, defined as the number of milliseconds that have elapsed + since January 1, 1970 (midnight UTC/GMT) and expressed as a JSON + Number. For VOD assets, or if the wallclock time is not known, + the value SHOULD be 0. + + An example media timeline is shown below: + + [ + [0, [0,0], 1759924158381], + [2002, [1,0], 1759924160383], + [4004, [2,0], 1759924162385], + [6006, [3,0], 1759924164387], + [8008, [4,0], 1759924166389] + ] + +7.2. Media Timeline Catalog requirements + + A media timeline track MUST carry a 'packaging' identifier in the + Catalog with a value of "mediatimeline". A media timeline track MUST + carry a 'depends' attribute which contains an array of all track + names to which the media timeline track applies. The mime-type of a media + timeline track MUST be specified as "application/json". + +7.3. Media Timeline track updating + + The publisher MUST publish an independent media timeline in the first + MOQT Object of each MOQT Group of a media timeline track. The + publisher MAY publish incremental updates in the second and + subsequent Objects within each Group. Incremental updates only + contain media timeline records since the last media timeline Object. + +8. Event Timeline track + + The event timeline track provides a mechanism to associate ad-hoc + event metadata with the broadcast. Use-case examples include live + sports score data, GPS coordinates of race cars, SAP-types for media + segments or active speaker notifications in web conferences. + + + +Law Expires 23 July 2026 [Page 29] + +Internet-Draft MOQT Streaming Format January 2026 + + + To allow the client to bind this event metadata with the broadcast + content described by the media timeline track, each event record MUST + contain a reference to one of Media PTS, wallclock time or MOQT + Location. + + Event timeline tracks are optional. Multiple event timeline tracks + can exist inside a catalog. The type & structure of the data + contained within each event timeline track is declared in the + catalog, to facilitate client selection and parsing. + +8.1. Event Timeline data format + + An event timeline track is a JSON [JSON] document. This document MAY + be compressed using GZIP [GZIP]. The document contains an array of + records. Each record consists of a JSON Object containing the + following required fields: + + * An index reference, which MUST be either 't' for wallclock time, + 'l' for Location or 'm' for Media PTS. Only one of these index + values may be used within each record. Event timelines SHOULD use + the same index reference type for each record. The definitions + for wallclock time, Location and Media PTS are identical to those + defined for media timeline payload Section 7.1. Wallclock time + and media PTS values are JSON Number, while Location value is an + Array of Numbers, where the first item represents the MOQT GroupID + and the second item the MOQT Object ID. + + * A 'data' Object, whose structure is defined by the Section 5.1.13 + value declared for this track in the Catalog. + +8.2. Event Timeline Catalog requirements + + An event timeline track MUST carry: + + * a Section 5.1.12 attribute with a value of "eventtimeline". + + * a Section 5.1.21 attribute which contains an array of all track + names to which the event timeline track applies. + + * a Section 5.1.25 attribute with a value of "application/json". + + * an Section 5.1.13 attribute declaring the type & structure of data + contained in the event timeline track. + + + + + + + + +Law Expires 23 July 2026 [Page 30] + +Internet-Draft MOQT Streaming Format January 2026 + + +8.3. Event Timeline track updating + + The publisher MUST publish an independent event timeline in the first + MOQT Object of each MOQT Group of an event timeline track. The + publisher MAY publish incremental updates in the second and + subsequent Objects within each Group. Incremental updates only + contain event timeline records since the last event timeline Object. + +8.4. Event timeline track examples + +8.4.1. Event timeline track with wallclock time indexing + + This example shows how sports scores and game information might be + defined in a live sports broadcast. + + [ + { + "t": 1756885678361, + "data": { + "status": "in_progress", + "period": 1, + "clock": "12:00", + "homeScore": 0, + "awayScore": 0, + "lastPlay": "Game Start" + } + }, + { + "t": 1756885981542, + "data": { + "status": "in_progress", + "period": 1, + "clock": "09:25", + "homeScore": 2, + "awayScore": 0, + "lastPlay": "Team A: #23 makes 2-pt jump shot" + } + } + ] + +8.4.2. Event timeline track with MOQT Location indexing + + This example shows drone GPS coordinates synched with the start of + each Group. + + + + + + + +Law Expires 23 July 2026 [Page 31] + +Internet-Draft MOQT Streaming Format January 2026 + + + [ + { + "l": [0,0], + "data": [47.1812,8.4592] + }, + { + "l": [1,0], + "data": [47.1662,8.5155] + } + ] + +9. Workflow + +9.1. Initiating a broadcast + + An MSF publisher MUST publish a catalog track object before + publishing any media track objects. + +9.2. Ending a live broadcast + + After publishing a catalog and defining tracks carrying live content, + an original publisher can deliver a deterministic signal to all + subscribers that the broadcast is complete by taking the following + steps: + + * Send a SUBSCRIBE_DONE (See MOQT Sect 8.1.2) message for all active + tracks using status code 0x2 Track Ended. + + * If the live stream is being converted instantly to a VOD asset, + then publish an independent (non-delta) catalog update which, for + each track, sets isLive Section 5.1.15 to FALSE and adds a track + duration Section 5.1.37 field. + + * If the live stream is being terminated permanently without + conversion to VOD, then publish an independent catalog update + which signals isComplete Section 5.1.7 as TRUE and which contains + an empty Tracks Section 5.1.8 field. + +10. Security Considerations + + MSF inherits the security properties of the underlying MoQ + Transport protocol [MoQTransport] and its associated transport + security mechanisms (TLS 1.3 over QUIC or WebTransport). + + Threat Model: Attackers may be on-path (e.g., compromised relays) + or off-path. They may attempt to eavesdrop on media content, + inject or modify track data, replay previously captured Groups + or Objects, or disrupt service availability through resource + exhaustion or malformed messages. + + Confidentiality and Integrity: Media content and catalog metadata + are protected in transit by the underlying QUIC transport + encryption. However, relay nodes can observe and modify + content. Applications requiring end-to-end confidentiality + SHOULD apply payload-level encryption (e.g., Secure Frame + [SFrame]) above the MSF layer. + + Authentication and Authorization: Implementations MUST + authenticate publishers and subscribers before allowing them + to announce or consume MSF tracks. Authorization SHOULD be + enforced per-track or per-broadcast path. Credentials MUST + have bounded lifetimes and implementations SHOULD support + revocation. + + Replay and Denial of Service: The underlying QUIC transport + provides replay protection through its nonce-based encryption. + Implementations MUST impose limits on catalog size, track + count, Group rate, and Object sizes to prevent resource + exhaustion. Relays SHOULD apply rate limiting and + backpressure to protect downstream consumers from floods of + data or announcements. + + Catalog Integrity: The MSF catalog (Section 5) describes the + structure of a broadcast and is critical to correct operation. + A malicious or compromised publisher could advertise misleading + codec parameters, invalid track dependencies, or excessive + track counts. Receivers MUST validate catalog contents and + SHOULD reject catalogs that exceed implementation-defined + complexity limits. + + Privacy and Metadata: MSF track names, catalog fields (codec, + bitrate, resolution, temporal properties), and media timeline + metadata may reveal information about content and users. + Implementations SHOULD minimize metadata exposure to untrusted + intermediaries. The 'renderGroup' and temporal ordering + metadata in particular may allow traffic analysis of media + structure even when payloads are encrypted. + + Interoperability: Implementations that do not fully validate + MSF catalog fields or track packaging types may be vulnerable + to unexpected behavior when interoperating with other + implementations. Receivers MUST gracefully handle unknown + catalog fields and SHOULD ignore unrecognized packaging types + rather than crashing. + + Operational Guidance: Deployments SHOULD log authentication + failures, authorization denials, and malformed catalog or + track data for monitoring. Error responses MUST NOT leak + internal state or configuration to unauthenticated peers. + Implementations SHOULD follow the guidance in [RFC3552] for + security considerations in protocol design. + +11. IANA Considerations + + This document creates a new entry in the "MoQ Streaming Format" + Registry (see [MoQTransport] Sect 8). The type value is 0x001, the + name is "MOQT Streaming Format" and the RFC is XXX. + + + + +Law Expires 23 July 2026 [Page 32] + +Internet-Draft MOQT Streaming Format January 2026 + + +12. Normative References + + [BASE64] Josefsson, S., "The Base16, Base32, and Base64 Data + Encodings", RFC 4648, DOI 10.17487/RFC4648, October 2006, + . + + [GZIP] Deutsch, P., "GZIP file format specification version 4.3", + RFC 1952, DOI 10.17487/RFC1952, May 1996, + . + + [IMSC1] "W3C, TTML Profiles for Internet Media Subtitles and + Captions 1.0 (IMSC1)", April 2016, + . + + [JSON] Bray, T., Ed., "The JavaScript Object Notation (JSON) Data + Interchange Format", STD 90, RFC 8259, + DOI 10.17487/RFC8259, December 2017, + . + + [LANG] Phillips, A., Ed. and M. Davis, Ed., "Tags for Identifying + Languages", BCP 47, RFC 5646, DOI 10.17487/RFC5646, + September 2009, . + + [LOC] Zanaty, M., Nandakumar, S., and P. Thatcher, "Low Overhead + Media Container", Work in Progress, Internet-Draft, draft- + mzanaty-moq-loc-05, 3 March 2025, + . + + [MIME] Freed, N., Klensin, J., and T. Hansen, "Media Type + Specifications and Registration Procedures", BCP 13, + RFC 6838, DOI 10.17487/RFC6838, January 2013, + . + + [MoQTransport] + Nandakumar, S., Vasiliev, V., Swett, I., and A. Frindell, + "Media over QUIC Transport", Work in Progress, Internet- + Draft, draft-ietf-moq-transport-11, 28 April 2025, + . + + [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate + Requirement Levels", BCP 14, RFC 2119, + DOI 10.17487/RFC2119, March 1997, + . + + + + + + +Law Expires 23 July 2026 [Page 33] + +Internet-Draft MOQT Streaming Format January 2026 + + + [RFC4180] Shafranovich, Y., "Common Format and MIME Type for Comma- + Separated Values (CSV) Files", RFC 4180, + DOI 10.17487/RFC4180, October 2005, + . + + [RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC + 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, + May 2017, . + + [RFC9000] Iyengar, J., Ed. and M. Thomson, Ed., "QUIC: A UDP-Based + Multiplexed and Secure Transport", RFC 9000, + DOI 10.17487/RFC9000, May 2021, + . + + [WEBCODECS-CODEC-REGISTRY] + "WebCodecs Codec Registry", September 2024, + . + + [WEBVTT] "World Wide Web Consortium (W3C), WebVTT: The Web Video + Text Tracks Format", April 2019, + . + +Acknowledgments + + * the MoQ Workgroup and mailing lists. + +Contributors + + The following persons where the co-authors of the individual draft + (draft-law-moq-warpstreamingformat) this document is based on: + + * Luke Curley + + * Victor Vasiliev + + * Suhas Nandakumar + + * Kirill Pugin + + * Will Law + +Author's Address + + Will Law + Akamai + Email: wilaw@akamai.com + + + + + +Law Expires 23 July 2026 [Page 34] diff --git a/js/clock/src/main.ts b/js/clock/src/main.ts index 873e764e8..651646630 100755 --- a/js/clock/src/main.ts +++ b/js/clock/src/main.ts @@ -147,7 +147,7 @@ async function subscribe(config: Config) { // Handle groups and frames like the Rust implementation for (;;) { - const group = await track.nextGroup(); + const group = await track.recvGroup(); if (!group) { console.log("❌ Connection ended"); break; diff --git a/js/hang/src/catalog/audio.ts b/js/hang/src/catalog/audio.ts index 47178addc..2403aa545 100644 --- a/js/hang/src/catalog/audio.ts +++ b/js/hang/src/catalog/audio.ts @@ -1,6 +1,7 @@ import * as z from "zod/mini"; import { ContainerSchema } from "./container"; import { u53Schema } from "./integers"; +import { Section } from "./section"; // Backwards compatibility: old track schema const TrackSchema = z.object({ @@ -60,3 +61,6 @@ export const AudioSchema = z.union([ export type Audio = z.infer; export type AudioConfig = z.infer; + +/// Predefined section for audio catalog data. +export const AUDIO = new Section("audio", AudioSchema); diff --git a/js/hang/src/catalog/capabilities.ts b/js/hang/src/catalog/capabilities.ts deleted file mode 100644 index b1f4df8ce..000000000 --- a/js/hang/src/catalog/capabilities.ts +++ /dev/null @@ -1,22 +0,0 @@ -import * as z from "zod/mini"; - -export const VideoCapabilitiesSchema = z.object({ - hardware: z.optional(z.array(z.string())), - software: z.optional(z.array(z.string())), - unsupported: z.optional(z.array(z.string())), -}); - -export const AudioCapabilitiesSchema = z.object({ - hardware: z.optional(z.array(z.string())), - software: z.optional(z.array(z.string())), - unsupported: z.optional(z.array(z.string())), -}); - -export const CapabilitiesSchema = z.object({ - video: z.optional(VideoCapabilitiesSchema), - audio: z.optional(AudioCapabilitiesSchema), -}); - -export type Capabilities = z.infer; -export type VideoCapabilities = z.infer; -export type AudioCapabilities = z.infer; diff --git a/js/hang/src/catalog/chat.ts b/js/hang/src/catalog/chat.ts deleted file mode 100644 index d012b96fe..000000000 --- a/js/hang/src/catalog/chat.ts +++ /dev/null @@ -1,9 +0,0 @@ -import * as z from "zod/mini"; -import { TrackSchema } from "./track"; - -export const ChatSchema = z.object({ - message: z.optional(TrackSchema), - typing: z.optional(TrackSchema), -}); - -export type Chat = z.infer; diff --git a/js/hang/src/catalog/container.ts b/js/hang/src/catalog/container.ts index c5c14ff32..ee077ba8d 100644 --- a/js/hang/src/catalog/container.ts +++ b/js/hang/src/catalog/container.ts @@ -1,5 +1,4 @@ import * as z from "zod/mini"; -import { u53Schema } from "./integers"; /** * Container format for frame timestamp encoding and frame payload structure. @@ -7,19 +6,17 @@ import { u53Schema } from "./integers"; * - "legacy": Uses QUIC VarInt encoding (1-8 bytes, variable length), raw frame payloads. * Timestamps are in microseconds. * - "cmaf": Fragmented MP4 container - frames contain complete moof+mdat fragments. - * Timestamps are in timescale units. + * The init segment (ftyp+moov) is base64-encoded in the catalog. */ export const ContainerSchema = z._default( z.discriminatedUnion("kind", [ // The default hang container z.object({ kind: z.literal("legacy") }), - // CMAF container with timescale for timestamp conversion + // CMAF container with base64-encoded init segment (ftyp+moov) z.object({ kind: z.literal("cmaf"), - // Time units per second - timescale: u53Schema, - // Track ID used in the moof/mdat fragments - trackId: u53Schema, + // Base64-encoded init segment (ftyp+moov) + initData: z.base64(), }), ]), { kind: "legacy" }, diff --git a/js/hang/src/catalog/index.ts b/js/hang/src/catalog/index.ts index ecf4e9daf..86aee07be 100644 --- a/js/hang/src/catalog/index.ts +++ b/js/hang/src/catalog/index.ts @@ -1,12 +1,9 @@ export * from "./audio"; -export * from "./capabilities"; -export * from "./chat"; export * from "./container"; export * from "./integers"; -export * from "./location"; -export * from "./preview"; export * from "./priority"; -export * from "./root"; +export * from "./reader"; +export * from "./section"; export * from "./track"; -export * from "./user"; export * from "./video"; +export * from "./writer"; diff --git a/js/hang/src/catalog/location.ts b/js/hang/src/catalog/location.ts deleted file mode 100644 index 52055e511..000000000 --- a/js/hang/src/catalog/location.ts +++ /dev/null @@ -1,45 +0,0 @@ -import * as z from "zod/mini"; -import { TrackSchema } from "./track"; - -export const PositionSchema = z.object({ - // The relative X position of the broadcast, from -1 to +1. - // This should be used for audio panning but can also be used for video positioning. - x: z.optional(z.number()), - - // The relative Y position of the broadcast, from -1 to +1. - // This can be used for video positioning, and maybe audio panning. - y: z.optional(z.number()), - - // The relative Z index of the broadcast, where larger values are closer to the viewer. - // This is used to break ties when there are multiple broadcasts at the same position. - z: z.optional(z.number()), - - // The scale of the broadcast, where 1 is 100% - s: z.optional(z.number()), -}); - -export const LocationSchema = z.object({ - // The initial position of the broadcaster, from -1 to +1 in both dimensions. - // If not provided, then the broadcaster is assumed to be at (0,0) - // This should be used for audio panning but can also be used for video positioning. - initial: z.optional(PositionSchema), - - // If provided, then updates to the position are done via a separate Moq track. - // This is used to avoid a full catalog update every time we want to update a few bytes. - // TODO: These updates currently use JSON for simplicity, but we should use a binary format. - track: z.optional(TrackSchema), - - // If set, then this broadcaster allows other peers to request position updates via this handle. - // We will have to discover and subscribe to their position updates. - handle: z.optional(z.string()), - - // If provided, this broadcaster is signaling the location of other peers. - // The payload is a JSON blob keyed by handle for each peer. - peers: z.optional(TrackSchema), -}); - -export type Location = z.infer; -export type Position = z.infer; - -export const PeersSchema = z.record(z.string(), PositionSchema); -export type Peers = z.infer; diff --git a/js/hang/src/catalog/preview.ts b/js/hang/src/catalog/preview.ts deleted file mode 100644 index ae8183b07..000000000 --- a/js/hang/src/catalog/preview.ts +++ /dev/null @@ -1,15 +0,0 @@ -import * as z from "zod/mini"; - -export const PreviewSchema = z.object({ - name: z.optional(z.string()), // name - avatar: z.optional(z.string()), // avatar - - audio: z.optional(z.boolean()), // audio enabled - video: z.optional(z.boolean()), // video enabled - - typing: z.optional(z.boolean()), // actively typing - chat: z.optional(z.boolean()), // chatted recently - screen: z.optional(z.boolean()), // screen sharing -}); - -export type Preview = z.infer; diff --git a/js/hang/src/catalog/reader.ts b/js/hang/src/catalog/reader.ts new file mode 100644 index 000000000..a08bf6a0a --- /dev/null +++ b/js/hang/src/catalog/reader.ts @@ -0,0 +1,68 @@ +import type * as Moq from "@moq/lite"; +import { type Effect, type Getter, Signal } from "@moq/signals"; +import type { z } from "zod/mini"; +import type { Section } from "./section"; + +/// A catalog reader that provides per-section change notifications. +/// +/// Sections are registered with a name and Zod schema. When the catalog track +/// receives a new frame, JSON is parsed and each registered section is validated +/// and updated. Signal equality checking ensures subscribers only fire when +/// their specific section's value actually changed. +export class CatalogReader { + // biome-ignore lint/suspicious/noExplicitAny: we store heterogeneous section types + #sections = new Map; signal: Signal }>(); + + /// Register interest in a section. Returns a Getter. + /// + /// The getter updates when the catalog is re-fetched and this section's value differs. + section(def: Section): Getter { + const existing = this.#sections.get(def.name); + if (existing) return existing.signal as Getter; + + const signal = new Signal(undefined); + this.#sections.set(def.name, { schema: def.schema, signal }); + return signal; + } + + /// Start consuming from a MoQ track. + /// + /// Spawns an async loop that reads frames, parses JSON, and updates + /// per-section signals. Unregistered keys in the JSON are ignored. + consume(track: Moq.Track, effect: Effect): void { + effect.spawn(async () => { + try { + for (;;) { + const frame = await Promise.race([effect.cancel, track.readFrame()]); + if (!frame) break; + + const decoder = new TextDecoder(); + const str = decoder.decode(frame); + const json = JSON.parse(str); + + for (const [name, { schema, signal }] of this.#sections) { + const raw = json[name]; + if (raw !== undefined) { + try { + const parsed = schema.parse(raw); + signal.set(parsed); + } catch (err) { + console.warn(`invalid catalog section "${name}"`, err); + signal.set(undefined); + } + } else { + signal.set(undefined); + } + } + } + } catch (err) { + console.warn("error reading catalog", err); + } finally { + // Clear all sections when the track ends + for (const { signal } of this.#sections.values()) { + signal.set(undefined); + } + } + }); + } +} diff --git a/js/hang/src/catalog/root.ts b/js/hang/src/catalog/root.ts deleted file mode 100644 index a025c6160..000000000 --- a/js/hang/src/catalog/root.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type * as Moq from "@moq/lite"; -import * as z from "zod/mini"; - -import { AudioSchema } from "./audio"; -import { CapabilitiesSchema } from "./capabilities"; -import { ChatSchema } from "./chat"; -import { LocationSchema } from "./location"; -import { TrackSchema } from "./track"; -import { UserSchema } from "./user"; -import { VideoSchema } from "./video"; - -export const RootSchema = z.object({ - video: z.optional(VideoSchema), - audio: z.optional(AudioSchema), - location: z.optional(LocationSchema), - user: z.optional(UserSchema), - chat: z.optional(ChatSchema), - capabilities: z.optional(CapabilitiesSchema), - preview: z.optional(TrackSchema), -}); - -export type Root = z.infer; - -export function encode(root: Root): Uint8Array { - const encoder = new TextEncoder(); - return encoder.encode(JSON.stringify(root)); -} - -export function decode(raw: Uint8Array): Root { - const decoder = new TextDecoder(); - const str = decoder.decode(raw); - try { - const json = JSON.parse(str); - return RootSchema.parse(json); - } catch (error) { - console.warn("invalid catalog", str); - throw error; - } -} - -export async function fetch(track: Moq.Track): Promise { - const frame = await track.readFrame(); - if (!frame) return undefined; - return decode(frame); -} diff --git a/js/hang/src/catalog/section.ts b/js/hang/src/catalog/section.ts new file mode 100644 index 000000000..5ed7a6c64 --- /dev/null +++ b/js/hang/src/catalog/section.ts @@ -0,0 +1,15 @@ +import type { z } from "zod/mini"; + +/// A section definition that pairs a JSON key name with a Zod schema. +/// +/// Used to register interest in specific catalog sections for reading or writing. +/// Audio and video sections are predefined but not registered by default. +export class Section { + readonly name: string; + readonly schema: z.ZodMiniType; + + constructor(name: string, schema: z.ZodMiniType) { + this.name = name; + this.schema = schema; + } +} diff --git a/js/hang/src/catalog/user.ts b/js/hang/src/catalog/user.ts deleted file mode 100644 index 7f4a699c2..000000000 --- a/js/hang/src/catalog/user.ts +++ /dev/null @@ -1,10 +0,0 @@ -import * as z from "zod/mini"; - -export const UserSchema = z.object({ - id: z.optional(z.string()), - name: z.optional(z.string()), - avatar: z.optional(z.string()), // TODO allow using a track instead of a URL? - color: z.optional(z.string()), -}); - -export type User = z.infer; diff --git a/js/hang/src/catalog/video.ts b/js/hang/src/catalog/video.ts index dffb5e7cb..cb44e8c24 100644 --- a/js/hang/src/catalog/video.ts +++ b/js/hang/src/catalog/video.ts @@ -1,6 +1,7 @@ import * as z from "zod/mini"; import { ContainerSchema } from "./container"; import { u53Schema } from "./integers"; +import { Section } from "./section"; // Backwards compatibility: old track schema const TrackSchema = z.object({ @@ -103,3 +104,6 @@ export const VideoSchema = z.union([ export type Video = z.infer; export type VideoConfig = z.infer; + +/// Predefined section for video catalog data. +export const VIDEO = new Section("video", VideoSchema); diff --git a/js/hang/src/catalog/writer.ts b/js/hang/src/catalog/writer.ts new file mode 100644 index 000000000..b6863bc54 --- /dev/null +++ b/js/hang/src/catalog/writer.ts @@ -0,0 +1,44 @@ +import type * as Moq from "@moq/lite"; +import { type Effect, Signal } from "@moq/signals"; +import type { Section } from "./section"; + +/// A catalog writer that manages typed sections and serializes them to a MoQ track. +/// +/// Each section is a Signal that can be set independently. When served on a track, +/// changes are reactively detected and the full catalog JSON is re-serialized. +/// Microtask coalescing in Signal means multiple set() calls in the same tick +/// produce a single write. +export class CatalogWriter { + // biome-ignore lint/suspicious/noExplicitAny: we store heterogeneous section types + #sections = new Map }>(); + + /// Register a section for writing. Returns a Signal for read+write. + section(def: Section): Signal { + const existing = this.#sections.get(def.name); + if (existing) return existing.signal as Signal; + + const signal = new Signal(undefined); + this.#sections.set(def.name, { signal }); + return signal; + } + + /// Serve the catalog on a MoQ track. + /// + /// Uses Effect to reactively subscribe to all registered section signals. + /// When any signal changes, re-serializes and writes a new frame. + serve(track: Moq.Track, effect: Effect): void { + effect.run((inner) => { + const obj: Record = {}; + + for (const [name, { signal }] of this.#sections) { + const value = inner.get(signal); + if (value !== undefined) { + obj[name] = value; + } + } + + const encoder = new TextEncoder(); + track.writeFrame(encoder.encode(JSON.stringify(obj))); + }); + } +} diff --git a/js/hang/src/container/cmaf/decode.ts b/js/hang/src/container/cmaf/decode.ts index b34e0f54e..9efba5d9e 100644 --- a/js/hang/src/container/cmaf/decode.ts +++ b/js/hang/src/container/cmaf/decode.ts @@ -278,11 +278,6 @@ export function decodeDataSegment(segment: Uint8Array, timescale: number): Sampl throw new Error(`Invalid sample size ${sampleSize} for sample ${i} in trun`); } - // Validate sample duration - must be positive for proper timing - if (sampleDuration <= 0) { - throw new Error(`Invalid sample duration ${sampleDuration} for sample ${i} in trun`); - } - // Bounds check before slicing to prevent reading past mdat data if (dataOffset + sampleSize > mdatData.length) { throw new Error( diff --git a/js/hang/src/container/cmaf/encode.ts b/js/hang/src/container/cmaf/encode.ts index 104cb4b98..923b6bae3 100644 --- a/js/hang/src/container/cmaf/encode.ts +++ b/js/hang/src/container/cmaf/encode.ts @@ -232,17 +232,14 @@ function createAvc1Box(width: number, height: number, avcC: Uint8Array): Uint8Ar * ``` */ export function createVideoInitSegment(config: Catalog.VideoConfig): Uint8Array { - const { codedWidth, codedHeight, description, container } = config; + const { codedWidth, codedHeight, description } = config; if (!codedWidth || !codedHeight || !description) { - // TODO: We could throw new Error("Missing required fields to create video init segment"); } - // Use timescale from CMAF container, or microseconds for legacy - const timescale = container.kind === "cmaf" ? container.timescale : 1_000_000; - - // Use track_id from CMAF container, or default to 1 for legacy - const trackId = container.kind === "cmaf" ? container.trackId : 1; + // Legacy container always uses microsecond timescale and track ID 1 + const timescale = 1_000_000; + const trackId = 1; // ftyp - File Type Box const ftyp: FileTypeBox = { @@ -442,13 +439,11 @@ export function createVideoInitSegment(config: Catalog.VideoConfig): Uint8Array * Supports AAC (mp4a) and Opus codecs. */ export function createAudioInitSegment(config: Catalog.AudioConfig): Uint8Array { - const { sampleRate, numberOfChannels, description, codec, container } = config; - - // Use timescale from CMAF container, or microseconds for legacy - const timescale = container.kind === "cmaf" ? container.timescale : 1_000_000; + const { sampleRate, numberOfChannels, description, codec } = config; - // Use track_id from CMAF container, or default to 1 for legacy - const trackId = container.kind === "cmaf" ? container.trackId : 1; + // Legacy container always uses microsecond timescale and track ID 1 + const timescale = 1_000_000; + const trackId = 1; // ftyp - File Type Box const ftyp: FileTypeBox = { diff --git a/js/hang/src/container/legacy.ts b/js/hang/src/container/legacy.ts index 70e265894..68498f88e 100644 --- a/js/hang/src/container/legacy.ts +++ b/js/hang/src/container/legacy.ts @@ -109,7 +109,7 @@ export class Consumer { async #run() { // Start fetching groups in the background for (;;) { - const consumer = await this.#track.nextGroup(); + const consumer = await this.#track.recvGroup(); if (!consumer) break; // To improve TTV, we always start with the first group. diff --git a/js/lite/examples/subscribe.ts b/js/lite/examples/subscribe.ts index e294fdd6a..d2b4ca913 100644 --- a/js/lite/examples/subscribe.ts +++ b/js/lite/examples/subscribe.ts @@ -12,7 +12,7 @@ async function main() { // Read data as it arrives for (;;) { - const group = await track.nextGroup(); + const group = await track.recvGroup(); if (!group) break; for (;;) { diff --git a/js/lite/src/connection/accept.ts b/js/lite/src/connection/accept.ts index 45eb53d61..0a29938b5 100644 --- a/js/lite/src/connection/accept.ts +++ b/js/lite/src/connection/accept.ts @@ -26,6 +26,8 @@ export async function accept(transport: WebTransport, url: URL, props?: AcceptPr return acceptAlpnVersion(transport, url, Ietf.Version.DRAFT_16); } else if (protocol === Ietf.ALPN.DRAFT_15) { return acceptAlpnVersion(transport, url, Ietf.Version.DRAFT_15); + } else if (protocol === Lite.ALPN_04) { + return new Lite.Connection(url, transport, Lite.Version.DRAFT_04, undefined); } else if (protocol === Lite.ALPN_03) { return new Lite.Connection(url, transport, Lite.Version.DRAFT_03, undefined); } else if (protocol === Lite.ALPN || protocol === "" || protocol === undefined) { diff --git a/js/lite/src/connection/connect.ts b/js/lite/src/connection/connect.ts index 6a9026fa1..3620a724a 100644 --- a/js/lite/src/connection/connect.ts +++ b/js/lite/src/connection/connect.ts @@ -152,6 +152,9 @@ export async function connect(url: URL, props?: ConnectProps): Promise { for (;;) { - const group = await track.nextGroup(); + const group = await track.recvGroup(); if (!group) return; void this.#runGroup(msg.requestId, group); } diff --git a/js/lite/src/lite/announce.ts b/js/lite/src/lite/announce.ts index 32da48ba4..be7a46ef4 100644 --- a/js/lite/src/lite/announce.ts +++ b/js/lite/src/lite/announce.ts @@ -1,18 +1,21 @@ import * as Path from "../path.ts"; import type { Reader, Writer } from "../stream.ts"; -import { unreachable } from "../util/error.ts"; import * as Message from "./message.ts"; import { Version } from "./version.ts"; +const MAX_HOPS = 32; + export class Announce { suffix: Path.Valid; active: boolean; - hops: number; - constructor(props: { suffix: Path.Valid; active: boolean; hops?: number }) { + /// Ordered origin path. Draft03 populates with 0n (UNKNOWN) entries; Draft04+ uses real IDs. + hops: bigint[]; + + constructor(props: { suffix: Path.Valid; active: boolean; hops?: bigint[] }) { this.suffix = props.suffix; this.active = props.active; - this.hops = props.hops ?? 0; + this.hops = props.hops ?? []; } async #encode(w: Writer, version: Version) { @@ -21,13 +24,23 @@ export class Announce { switch (version) { case Version.DRAFT_03: - await w.u53(this.hops); + if (this.hops.length > MAX_HOPS) { + throw new Error(`hop count ${this.hops.length} exceeds maximum of ${MAX_HOPS}`); + } + await w.u53(this.hops.length); break; case Version.DRAFT_01: case Version.DRAFT_02: break; default: - unreachable(version); + if (this.hops.length > MAX_HOPS) { + throw new Error(`hop count ${this.hops.length} exceeds maximum of ${MAX_HOPS}`); + } + await w.u53(this.hops.length); + for (const hop of this.hops) { + await w.u62(hop); + } + break; } } @@ -35,16 +48,32 @@ export class Announce { const active = await r.bool(); const suffix = Path.from(await r.string()); - let hops = 0; + const hops: bigint[] = []; switch (version) { - case Version.DRAFT_03: - hops = await r.u53(); + case Version.DRAFT_03: { + // Read count but don't know actual IDs; use 0 as unknown placeholder. + const count = await r.u53(); + if (count > MAX_HOPS) { + throw new Error(`hop count ${count} exceeds maximum of ${MAX_HOPS}`); + } + for (let i = 0; i < count; i++) { + hops.push(0n); + } break; + } case Version.DRAFT_01: case Version.DRAFT_02: break; - default: - unreachable(version); + default: { + const count = await r.u53(); + if (count > MAX_HOPS) { + throw new Error(`hop count ${count} exceeds maximum of ${MAX_HOPS}`); + } + for (let i = 0; i < count; i++) { + hops.push(await r.u62()); + } + break; + } } return new Announce({ suffix, active, hops }); @@ -66,31 +95,57 @@ export class Announce { export class AnnounceInterest { prefix: Path.Valid; - constructor(prefix: Path.Valid) { - this.prefix = prefix; + /// Filter out announces whose hops contain this hop ID. 0n means no filtering. + excludeHop: bigint; + + constructor(props: { prefix: Path.Valid; excludeHop?: bigint }) { + this.prefix = props.prefix; + this.excludeHop = props.excludeHop ?? 0n; } - async #encode(w: Writer) { + async #encode(w: Writer, version: Version) { await w.string(this.prefix); + + switch (version) { + case Version.DRAFT_01: + case Version.DRAFT_02: + case Version.DRAFT_03: + break; + default: + await w.u62(this.excludeHop); + break; + } } - static async #decode(r: Reader): Promise { + static async #decode(r: Reader, version: Version): Promise { const prefix = Path.from(await r.string()); - return new AnnounceInterest(prefix); + + let excludeHop = 0n; + switch (version) { + case Version.DRAFT_01: + case Version.DRAFT_02: + case Version.DRAFT_03: + break; + default: + excludeHop = await r.u62(); + break; + } + + return new AnnounceInterest({ prefix, excludeHop }); } - async encode(w: Writer): Promise { - return Message.encode(w, this.#encode.bind(this)); + async encode(w: Writer, version: Version): Promise { + return Message.encode(w, (w) => this.#encode(w, version)); } - static async decode(r: Reader): Promise { - return Message.decode(r, AnnounceInterest.#decode); + static async decode(r: Reader, version: Version): Promise { + return Message.decode(r, (r) => AnnounceInterest.#decode(r, version)); } } /// Sent after setup to communicate the initially announced paths. /// -/// Used by Draft01/Draft02 only. Draft03 uses individual Announce messages instead. +/// Used by Draft01/Draft02 only. Draft03+ uses individual Announce messages instead. export class AnnounceInit { suffixes: Path.Valid[]; @@ -103,10 +158,8 @@ export class AnnounceInit { case Version.DRAFT_01: case Version.DRAFT_02: break; - case Version.DRAFT_03: - throw new Error("announce init not supported for Draft03"); default: - unreachable(version); + throw new Error("announce init not supported for this version"); } } diff --git a/js/lite/src/lite/connection.ts b/js/lite/src/lite/connection.ts index b7684dd54..768dc1cd3 100644 --- a/js/lite/src/lite/connection.ts +++ b/js/lite/src/lite/connection.ts @@ -167,7 +167,7 @@ export class Connection implements Established { if (typ === StreamId.Session) { throw new Error("duplicate session stream"); } else if (typ === StreamId.Announce) { - const msg = await AnnounceInterest.decode(stream.reader); + const msg = await AnnounceInterest.decode(stream.reader, this.#version); await this.#publisher.runAnnounce(msg, stream); return; } else if (typ === StreamId.Subscribe) { diff --git a/js/lite/src/lite/fetch.ts b/js/lite/src/lite/fetch.ts index 0035d018d..ba907d6fe 100644 --- a/js/lite/src/lite/fetch.ts +++ b/js/lite/src/lite/fetch.ts @@ -1,18 +1,16 @@ import * as Path from "../path.ts"; import type { Reader, Writer } from "../stream.ts"; -import { unreachable } from "../util/error.ts"; import * as Message from "./message.ts"; import { Version } from "./version.ts"; function guardDraft03(version: Version) { switch (version) { - case Version.DRAFT_03: - break; case Version.DRAFT_01: case Version.DRAFT_02: throw new Error("fetch not supported for this version"); default: - unreachable(version); + // DRAFT_03+ + break; } } diff --git a/js/lite/src/lite/probe.ts b/js/lite/src/lite/probe.ts index 986c2a4c6..6244449f5 100644 --- a/js/lite/src/lite/probe.ts +++ b/js/lite/src/lite/probe.ts @@ -1,17 +1,15 @@ import type { Reader, Writer } from "../stream.ts"; -import { unreachable } from "../util/error.ts"; import * as Message from "./message.ts"; import { Version } from "./version.ts"; function guardDraft03(version: Version) { switch (version) { - case Version.DRAFT_03: - break; case Version.DRAFT_01: case Version.DRAFT_02: throw new Error("probe not supported for this version"); default: - unreachable(version); + // DRAFT_03+ + break; } } diff --git a/js/lite/src/lite/publisher.ts b/js/lite/src/lite/publisher.ts index e50d40a93..1a8ab62f9 100644 --- a/js/lite/src/lite/publisher.ts +++ b/js/lite/src/lite/publisher.ts @@ -83,19 +83,19 @@ export class Publisher { } switch (this.version) { - case Version.DRAFT_03: - // Draft03: send individual Announce messages for initial state. - for (const suffix of active) { - const wire = new Announce({ suffix, active: true }); - await wire.encode(stream.writer, this.version); - } - break; case Version.DRAFT_01: case Version.DRAFT_02: { const init = new AnnounceInit([...active]); await init.encode(stream.writer, this.version); break; } + default: + // Draft03+: send individual Announce messages for initial state. + for (const suffix of active) { + const wire = new Announce({ suffix, active: true }); + await wire.encode(stream.writer, this.version); + } + break; } // Wait for updates to the broadcasts. @@ -201,7 +201,7 @@ export class Publisher { async #runTrack(sub: bigint, broadcast: Path.Valid, track: Track, stream: Writer) { try { for (;;) { - const next = track.nextGroup(); + const next = track.recvGroup(); const group = await Promise.race([next, stream.closed]); if (!group) { next.then((group) => group?.close()).catch(() => {}); diff --git a/js/lite/src/lite/session.ts b/js/lite/src/lite/session.ts index 63893faab..dc085c224 100644 --- a/js/lite/src/lite/session.ts +++ b/js/lite/src/lite/session.ts @@ -1,5 +1,4 @@ import type { Reader, Writer } from "../stream.ts"; -import { unreachable } from "../util/error.ts"; import * as Message from "./message.ts"; import { Version } from "./version.ts"; @@ -132,10 +131,9 @@ export class SessionInfo { case Version.DRAFT_01: case Version.DRAFT_02: break; - case Version.DRAFT_03: - throw new Error("session info not supported for Draft03"); default: - unreachable(version); + // DRAFT_03+: session info not supported + throw new Error("session info not supported for this version"); } } diff --git a/js/lite/src/lite/subscribe.ts b/js/lite/src/lite/subscribe.ts index 622e78799..e217b8850 100644 --- a/js/lite/src/lite/subscribe.ts +++ b/js/lite/src/lite/subscribe.ts @@ -1,6 +1,5 @@ import * as Path from "../path.ts"; import type { Reader, Writer } from "../stream.ts"; -import { unreachable } from "../util/error.ts"; import * as Message from "./message.ts"; import { Version } from "./version.ts"; @@ -27,25 +26,28 @@ export class SubscribeUpdate { async #encode(w: Writer, version: Version) { switch (version) { - case Version.DRAFT_03: + case Version.DRAFT_01: + case Version.DRAFT_02: + await w.u8(this.priority); + break; + default: + // DRAFT_03+ await w.u8(this.priority); await w.bool(this.ordered); await w.u53(this.maxLatency); await w.u53(this.startGroup !== undefined ? this.startGroup + 1 : 0); await w.u53(this.endGroup !== undefined ? this.endGroup + 1 : 0); break; - case Version.DRAFT_01: - case Version.DRAFT_02: - await w.u8(this.priority); - break; - default: - unreachable(version); } } static async #decode(r: Reader, version: Version): Promise { switch (version) { - case Version.DRAFT_03: { + case Version.DRAFT_01: + case Version.DRAFT_02: + return new SubscribeUpdate({ priority: await r.u8() }); + default: { + // DRAFT_03+ const priority = await r.u8(); const ordered = await r.bool(); const maxLatency = await r.u53(); @@ -59,11 +61,6 @@ export class SubscribeUpdate { endGroup: endGroup > 0 ? endGroup - 1 : undefined, }); } - case Version.DRAFT_01: - case Version.DRAFT_02: - return new SubscribeUpdate({ priority: await r.u8() }); - default: - unreachable(version); } } @@ -118,17 +115,16 @@ export class Subscribe { await w.u8(this.priority); switch (version) { - case Version.DRAFT_03: + case Version.DRAFT_01: + case Version.DRAFT_02: + break; + default: + // DRAFT_03+ await w.bool(this.ordered); await w.u53(this.maxLatency); await w.u53(this.startGroup !== undefined ? this.startGroup + 1 : 0); await w.u53(this.endGroup !== undefined ? this.endGroup + 1 : 0); break; - case Version.DRAFT_01: - case Version.DRAFT_02: - break; - default: - unreachable(version); } } @@ -139,7 +135,11 @@ export class Subscribe { const priority = await r.u8(); switch (version) { - case Version.DRAFT_03: { + case Version.DRAFT_01: + case Version.DRAFT_02: + return new Subscribe({ id, broadcast, track, priority }); + default: { + // DRAFT_03+ const ordered = await r.bool(); const maxLatency = await r.u53(); const startGroup = await r.u53(); @@ -155,11 +155,6 @@ export class Subscribe { endGroup: endGroup > 0 ? endGroup - 1 : undefined, }); } - case Version.DRAFT_01: - case Version.DRAFT_02: - return new Subscribe({ id, broadcast, track, priority }); - default: - unreachable(version); } } @@ -201,13 +196,6 @@ export class SubscribeOk { async #encode(w: Writer, version: Version) { switch (version) { - case Version.DRAFT_03: - await w.u8(this.priority); - await w.bool(this.ordered); - await w.u53(this.maxLatency); - await w.u53(this.startGroup !== undefined ? this.startGroup + 1 : 0); - await w.u53(this.endGroup !== undefined ? this.endGroup + 1 : 0); - break; case Version.DRAFT_02: // noop break; @@ -215,7 +203,13 @@ export class SubscribeOk { await w.u8(this.priority ?? 0); break; default: - unreachable(version); + // DRAFT_03+ + await w.u8(this.priority); + await w.bool(this.ordered); + await w.u53(this.maxLatency); + await w.u53(this.startGroup !== undefined ? this.startGroup + 1 : 0); + await w.u53(this.endGroup !== undefined ? this.endGroup + 1 : 0); + break; } } @@ -227,13 +221,6 @@ export class SubscribeOk { let endGroup: number | undefined; switch (version) { - case Version.DRAFT_03: - priority = await r.u8(); - ordered = await r.bool(); - maxLatency = await r.u53(); - startGroup = await r.u53(); - endGroup = await r.u53(); - break; case Version.DRAFT_02: // noop break; @@ -241,7 +228,13 @@ export class SubscribeOk { priority = await r.u8(); break; default: - unreachable(version); + // DRAFT_03+ + priority = await r.u8(); + ordered = await r.bool(); + maxLatency = await r.u53(); + startGroup = await r.u53(); + endGroup = await r.u53(); + break; } return new SubscribeOk({ @@ -308,31 +301,34 @@ export type SubscribeResponse = { ok: SubscribeOk } | { drop: SubscribeDrop }; export async function encodeSubscribeResponse(w: Writer, resp: SubscribeResponse, version: Version): Promise { switch (version) { - case Version.DRAFT_03: + case Version.DRAFT_01: + case Version.DRAFT_02: if ("ok" in resp) { - await w.u53(0x0); await resp.ok.encode(w, version); } else { - await w.u53(0x1); - await resp.drop.encode(w); + throw new Error("subscribe drop not supported for this version"); } break; - case Version.DRAFT_01: - case Version.DRAFT_02: + default: + // DRAFT_03+ if ("ok" in resp) { + await w.u53(0x0); await resp.ok.encode(w, version); } else { - throw new Error("subscribe drop not supported for this version"); + await w.u53(0x1); + await resp.drop.encode(w); } break; - default: - unreachable(version); } } export async function decodeSubscribeResponse(r: Reader, version: Version): Promise { switch (version) { - case Version.DRAFT_03: { + case Version.DRAFT_01: + case Version.DRAFT_02: + return { ok: await SubscribeOk.decode(r, version) }; + default: { + // DRAFT_03+ const typ = await r.u53(); switch (typ) { case 0x0: @@ -343,10 +339,5 @@ export async function decodeSubscribeResponse(r: Reader, version: Version): Prom throw new Error(`unknown subscribe response type: ${typ}`); } } - case Version.DRAFT_01: - case Version.DRAFT_02: - return { ok: await SubscribeOk.decode(r, version) }; - default: - unreachable(version); } } diff --git a/js/lite/src/lite/subscriber.ts b/js/lite/src/lite/subscriber.ts index 7f0fc0728..fd88df564 100644 --- a/js/lite/src/lite/subscriber.ts +++ b/js/lite/src/lite/subscriber.ts @@ -47,13 +47,13 @@ export class Subscriber { async #runAnnounced(announced: Announced, prefix: Path.Valid): Promise { console.debug(`announced: prefix=${prefix}`); - const msg = new AnnounceInterest(prefix); + const msg = new AnnounceInterest({ prefix }); try { // Open a stream and send the announce interest. const stream = await Stream.open(this.#quic); await stream.writer.u53(StreamId.Announce); - await msg.encode(stream.writer); + await msg.encode(stream.writer, this.version); switch (this.version) { case Version.DRAFT_01: @@ -70,7 +70,8 @@ export class Subscriber { break; } case Version.DRAFT_03: - // Draft03: no AnnounceInit, initial state comes via Announce messages. + case Version.DRAFT_04: + // Draft03+: no AnnounceInit, initial state comes via Announce messages. break; } diff --git a/js/lite/src/lite/version.ts b/js/lite/src/lite/version.ts index 18ce7ca3b..c66e0a697 100644 --- a/js/lite/src/lite/version.ts +++ b/js/lite/src/lite/version.ts @@ -2,6 +2,7 @@ export const Version = { DRAFT_01: 0xff0dad01, DRAFT_02: 0xff0dad02, DRAFT_03: 0xff0dad03, + DRAFT_04: 0xff0dad04, } as const; export type Version = (typeof Version)[keyof typeof Version]; @@ -13,10 +14,14 @@ export const ALPN = "moql"; /// The ALPN string for Draft03, which uses ALPN-based version negotiation. export const ALPN_03 = "moq-lite-03"; +/// The ALPN string for Draft04, which uses ALPN-based version negotiation. +export const ALPN_04 = "moq-lite-04"; + const VERSION_NAMES: Record = { [Version.DRAFT_01]: "moq-lite-01", [Version.DRAFT_02]: "moq-lite-02", [Version.DRAFT_03]: "moq-lite-03", + [Version.DRAFT_04]: "moq-lite-04", }; export function versionName(v: Version): string { diff --git a/js/lite/src/track.ts b/js/lite/src/track.ts index 16dfe17fe..0d4b0242f 100644 --- a/js/lite/src/track.ts +++ b/js/lite/src/track.ts @@ -91,7 +91,13 @@ export class Track { group.close(); } - async nextGroup(): Promise { + /** + * Receive the next group available on this track. + * + * Groups may arrive out of order or with gaps due to network conditions. + * Use `OrderedConsumer` (in `@moq/hang`) if you need groups in sequence order. + */ + async recvGroup(): Promise { for (;;) { const groups = this.state.groups.peek(); if (groups.length > 0) { @@ -106,6 +112,11 @@ export class Track { } } + /** @deprecated Use {@link recvGroup} instead. */ + async nextGroup(): Promise { + return this.recvGroup(); + } + async readFrame(): Promise { return (await this.readFrameSequence())?.data; } diff --git a/js/publish/src/audio/encoder.ts b/js/publish/src/audio/encoder.ts index 3266d2bd9..8ba0f256a 100644 --- a/js/publish/src/audio/encoder.ts +++ b/js/publish/src/audio/encoder.ts @@ -1,5 +1,5 @@ -import * as Catalog from "@moq/hang/catalog"; -import * as Container from "@moq/hang/container"; +import { type Audio, type AudioConfig, type Container, PRIORITY, u53 } from "@moq/hang/catalog"; +import * as ContainerMod from "@moq/hang/container"; import * as Util from "@moq/hang/util"; import type * as Moq from "@moq/lite"; import { Time } from "@moq/lite"; @@ -25,12 +25,12 @@ export type EncoderProps = { // NOTE: Each frame is always flushed to the network immediately. groupDuration?: Time.Milli; - container?: Catalog.Container; + container?: Container; }; export class Encoder { static readonly TRACK = "audio/data"; - static readonly PRIORITY = Catalog.PRIORITY.audio; + static readonly PRIORITY = PRIORITY.audio; enabled: Signal; @@ -40,11 +40,11 @@ export class Encoder { source: Signal; - #catalog = new Signal(undefined); - readonly catalog: Getter = this.#catalog; + #catalog = new Signal