From 97f4a58eec94ef7be3ed7bb24bc33c017ddf2532 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Wed, 4 Nov 2026 04:39:16 -0800 Subject: [PATCH 1/2] chore(quest): claim quest/m0/pin-presentation-timeline.md Quest-Claim: 091f690756bcfa20 From b2924c28d0868fabb14a441c8fd9bf3c10f5c28d Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Wed, 4 Nov 2026 04:43:59 -0800 Subject: [PATCH 2/2] chore(moq): pin moq ffcb7cd for the presentation timeline Bump the moq gitlink to ffcb7cd, which carries moq-mux's shared presentation timeline, and port the studio to the API it breaks: with_hls_hint is with_interval, HLS records are gone, and the clock moved under the catalog's media section. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 14 -------------- moq | 2 +- quest/m0/README.md | 1 - quest/m0/broadcast-sync.md | 15 ++++----------- quest/m0/pin-presentation-timeline.md | 11 ----------- rs/moq-scene/src/publish.rs | 12 ++++++------ rs/moq-scene/src/replay.rs | 6 ++---- rs/moq-scene/src/source.rs | 2 +- rs/moq-studio/src/engine.rs | 2 +- rs/moq-studio/src/record.rs | 2 +- rs/moq-studio/src/replay.rs | 2 +- 11 files changed, 17 insertions(+), 52 deletions(-) delete mode 100644 quest/m0/pin-presentation-timeline.md diff --git a/Cargo.lock b/Cargo.lock index f61e069..f8f2544 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3712,7 +3712,6 @@ name = "moq-relay" version = "0.17.2" dependencies = [ "anyhow", - "async-trait", "aws-lc-rs", "axum", "axum-server", @@ -3722,7 +3721,6 @@ dependencies = [ "hex", "http-body", "http-cache-reqwest", - "humantime", "hyper-util", "kio", "moq-auth", @@ -3748,8 +3746,6 @@ dependencies = [ "tracing", "url", "usage-rs", - "web-transport-trait", - "x509-parser", ] [[package]] @@ -3896,7 +3892,6 @@ dependencies = [ "url", "usage-rs", "web-transport-proto", - "web-transport-trait", "webpki-roots", "x509-parser", ] @@ -7509,15 +7504,6 @@ dependencies = [ "url", ] -[[package]] -name = "web-transport-trait" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60221f03172c9e8e8d19d1c038c87ef2804d05f8a71264817f60a28bbd03edab" -dependencies = [ - "bytes", -] - [[package]] name = "webm-iterable" version = "0.7.1" diff --git a/moq b/moq index fd07f5b..ffcb7cd 160000 --- a/moq +++ b/moq @@ -1 +1 @@ -Subproject commit fd07f5b7c93f23b7ed1c4faddbbf4274a7d72e59 +Subproject commit ffcb7cd9748422daa30e935ec464108b5a8ebb71 diff --git a/quest/m0/README.md b/quest/m0/README.md index 8a2c12a..4c0c3a9 100644 --- a/quest/m0/README.md +++ b/quest/m0/README.md @@ -112,7 +112,6 @@ repository. - [Virtual camera](/quest/m0/vcam/README.md) - a canvas's program exposed as a system camera on Linux, Windows, and macOS - [Signing identities are available](/quest/m0/signing.md) - the Apple and Windows code-signing credentials packaging and the macOS camera need - [Packaging](/quest/m0/packaging.md) - signed installers for macOS and Windows and a Flatpak for Linux -- [moq-mux's presentation timeline is pinned](/quest/m0/pin-presentation-timeline.md) - shared playout timing, for broadcast sync - [The persistable portal restore token is pinned](/quest/m0/pin-portal-token.md) - moq-video's restore token, for Wayland sources - [Platform AAC encoders are pinned](/quest/m0/pin-aac-encode.md) - moq-audio's AAC encoders, for destinations - [moq-audio's filter chain is pinned](/quest/m0/pin-audio-filters.md) - the chain audio filters configure diff --git a/quest/m0/broadcast-sync.md b/quest/m0/broadcast-sync.md index 437d640..5f74d9f 100644 --- a/quest/m0/broadcast-sync.md +++ b/quest/m0/broadcast-sync.md @@ -21,20 +21,13 @@ Decided 2026-10-26 (maintainer): whole broadcast. A broadcast with no audio keeps video's own short delay. Show each source's delay in the UI. -Lift the shared part out of `moq play` into a library both binaries use, -rather than copying it. Decided 2026-11-02 (maintainer): that library is -moq-mux, which already owns the broadcast clock and the `Pacer` the timing -is built on, and it lands there first; this quest waits on its -[pin](/quest/m0/pin-presentation-timeline.md). The video side holds frames until their audio-aligned -time, and a source still repeats its last frame rather than stalling the -canvas. This replaces the live-edge behavior the studio app PR chose, where +Use the timing `moq play` shares rather than copying it: moq-mux's +`timeline` module, pinned here (decided 2026-11-02, maintainer). The video +side holds frames until their audio-aligned time, and a source still repeats +its last frame rather than stalling the canvas. This replaces the live-edge behavior the studio app PR chose, where replaying a publisher's timestamp gaps pushed audio behind its own video. Here video waits for audio instead. Update `README.md`. -## Required - -- [moq-mux's presentation timeline is pinned](/quest/m0/pin-presentation-timeline.md) - the shared audio-anchored timing - ## Related - [Audio jitter target](https://github.com/moq-dev/moq/blob/main/quest/m1/audio-jitter-target/README.md) - the delay estimate this reuses diff --git a/quest/m0/pin-presentation-timeline.md b/quest/m0/pin-presentation-timeline.md deleted file mode 100644 index d6039c4..0000000 --- a/quest/m0/pin-presentation-timeline.md +++ /dev/null @@ -1,11 +0,0 @@ -# [XS] moq-mux's presentation timeline is pinned - -## Goal - -The `moq` submodule includes moq-mux's audio-anchored playout timing, lifted -out of `moq play`, from -[moq-mux presents a broadcast on the publisher's timestamps](https://github.com/moq-dev/moq/blob/main/quest/m1/presentation-timeline.md), -which broadcast sources play on. - -Check: that quest file is gone from moq's `main`. Advance it with -`just upgrade-moq` in its own PR, fixing whatever the new API breaks here. diff --git a/rs/moq-scene/src/publish.rs b/rs/moq-scene/src/publish.rs index 55935f4..2de68f1 100644 --- a/rs/moq-scene/src/publish.rs +++ b/rs/moq-scene/src/publish.rs @@ -177,8 +177,7 @@ impl<'a> Listing<'a> { // The studio's clock, so source and program timestamps share one timeline. let config = moq_mux::catalog::Config::default() .with_clock(publish.catalog.clock()) - .with_bandwidth(publish.bandwidth.clone()) - .with_hls(None); + .with_bandwidth(publish.bandwidth.clone()); let catalog = moq_mux::catalog::Producer::new(&mut scratch, config)?; claim.owners.lock().unwrap().catalogs.insert(claim.id, catalog.clone()); Ok(Self { @@ -198,8 +197,9 @@ impl<'a> Listing<'a> { let mut updates = self.catalog.consume()?; let mut studio = self.studio.clone(); while let Some(mut catalog) = updates.next().await? { - // Already at the studio catalog's root. - catalog.clock = None; + // Media timing is the root's: the one clock, and the join interval an + // HLS export of the program numbers its segments on. + catalog.media = Default::default(); // The claim is checked inside the mutation, so a newer source's // writes, made under the same lock, are never overwritten. studio.mutate(|root| { @@ -411,7 +411,7 @@ impl Video { let reserved = listing .catalog .reserve() - .with_hls_hint(config.gop.duration(config.framerate)); + .with_interval(config.gop.duration(config.framerate)); let producer = moq_video::encode::Producer::new(listing.broadcast.clone(), reserved, rendition)?; Ok(Self { track: Encoding::new(producer), @@ -609,7 +609,7 @@ mod tests { let snapshot = listed(&catalog, |sources| sources.len() == 2).await; assert!(snapshot.video.renditions.is_empty(), "no source is a program rendition"); let published = &snapshot.ext.studio.sources; - assert!(published["red"].clock.is_none(), "the root carries the clock"); + assert!(published["red"].media.is_empty(), "the root carries the media timing"); assert_eq!(sources.get("red").unwrap().track(), Some(Status::Live)); assert_eq!(sources.get("guest").unwrap().track(), None); assert!(sources.get("red copy").is_none(), "a clone opens no feed, so no track"); diff --git a/rs/moq-scene/src/replay.rs b/rs/moq-scene/src/replay.rs index 1341dd9..dffd666 100644 --- a/rs/moq-scene/src/replay.rs +++ b/rs/moq-scene/src/replay.rs @@ -136,10 +136,8 @@ impl Take { "the broadcast has described none of its tracks yet".into(), )); } - // Listed so an export of the take finds its renditions; HLS is for live - // viewers, which a take has none of. - let config = moq_mux::catalog::Config::default().with_hls(None); - let mut listing = moq_mux::catalog::Producer::new(&mut broadcast, config)?; + // Listed so an export of the take finds its renditions. + let mut listing = moq_mux::catalog::Producer::new(&mut broadcast, Default::default())?; listing.mutate(|listed| { listed.video.renditions = video.clone().into_iter().collect(); listed.audio.renditions = audio.clone().into_iter().collect(); diff --git a/rs/moq-scene/src/source.rs b/rs/moq-scene/src/source.rs index c131239..787bb03 100644 --- a/rs/moq-scene/src/source.rs +++ b/rs/moq-scene/src/source.rs @@ -1085,7 +1085,7 @@ pub(crate) mod test { let mut config = moq_video::encode::Config::new(64, 64, moq_video::FrameRate::new(30, 1).unwrap()); config.kind = moq_video::encode::Kind::Software; let rendition = config.probe().await.unwrap(); - let reserved = catalog.reserve().with_hls_hint(config.gop.duration(config.framerate)); + let reserved = catalog.reserve().with_interval(config.gop.duration(config.framerate)); let mut producer = moq_video::encode::Producer::new(broadcast.clone(), reserved, rendition).unwrap(); let mut encoder = moq_video::encode::Encoder::new(&config).unwrap(); let gray = vec![0x80; 64 * 64 * 4]; diff --git a/rs/moq-studio/src/engine.rs b/rs/moq-studio/src/engine.rs index 7dba92f..786a400 100644 --- a/rs/moq-studio/src/engine.rs +++ b/rs/moq-studio/src/engine.rs @@ -1088,7 +1088,7 @@ impl Renditions { let reserved = self .timebase .reserve() - .with_hls_hint(config.gop.duration(config.framerate)); + .with_interval(config.gop.duration(config.framerate)); let producer = moq_video::encode::Producer::new(self.broadcast.clone(), reserved, video)?; let watched = Arc::new(AtomicBool::new(false)); self.runtime.spawn(watch(producer.demand(), watched.clone())); diff --git a/rs/moq-studio/src/record.rs b/rs/moq-studio/src/record.rs index 035b1c7..ae55388 100644 --- a/rs/moq-studio/src/record.rs +++ b/rs/moq-studio/src/record.rs @@ -856,7 +856,7 @@ mod tests { .iter() .map(|(name, _, _, listing)| { let mut listing = listing.snapshot(); - listing.clock = None; + listing.media = Default::default(); (name.clone(), listing) }) .collect(); diff --git a/rs/moq-studio/src/replay.rs b/rs/moq-studio/src/replay.rs index c87dbe8..8eeaf0d 100644 --- a/rs/moq-studio/src/replay.rs +++ b/rs/moq-studio/src/replay.rs @@ -160,7 +160,7 @@ mod tests { let mut config = moq_video::encode::Config::new(64, 64, moq_video::FrameRate::new(30, 1).unwrap()); config.kind = moq_video::encode::Kind::Software; let rendition = config.probe().await.unwrap(); - let reserved = catalog.reserve().with_hls_hint(config.gop.duration(config.framerate)); + let reserved = catalog.reserve().with_interval(config.gop.duration(config.framerate)); let mut producer = moq_video::encode::Producer::new(broadcast.clone(), reserved, rendition).unwrap(); let mut encoder = moq_video::encode::Encoder::new(&config).unwrap(); let gray = vec![0x80; 64 * 64 * 4];