diff --git a/quest/m0/README.md b/quest/m0/README.md index 302d5fc..8782273 100644 --- a/quest/m0/README.md +++ b/quest/m0/README.md @@ -101,7 +101,6 @@ repository. - [Virtual camera](/quest/m0/vcam/README.md) - the program or a published scene exposed as a system camera on Linux, Windows, and macOS - [Browser sources on the GPU under Mesa](/quest/m0/cef-headless.md) - shared-texture pages paint on Intel and AMD under Wayland, and a page that cannot paint fails loudly - [Browser sources make no outside requests](/quest/m0/browser-offline.md) - no component updates, spellcheck downloads, or network time from the helper -- [Unpublishing a scene ends its broadcast cleanly](/quest/m0/unpublish-clean.md) - viewers get a clean finish, not `Unroutable`, when a scene stops publishing - [A media file's sound plays with its pictures](/quest/m0/media-av-sync.md) - media sources write a bus latency ahead, as broadcast sources do - [The Apple signing identity is available](/quest/m0/signing.md) - the Apple credentials packaging and the macOS camera need; Windows ships unsigned - [Packaging](/quest/m0/packaging.md) - a signed macOS app, a Windows installer, and a Flatpak for Linux diff --git a/quest/m0/unpublish-clean.md b/quest/m0/unpublish-clean.md deleted file mode 100644 index bbac118..0000000 --- a/quest/m0/unpublish-clean.md +++ /dev/null @@ -1,24 +0,0 @@ -# [S] Unpublishing a scene ends its broadcast cleanly - -## Goal - -When a scene is unpublished, renamed, or removed, its viewers receive the -end of each track as a clean finish, never an `Unroutable` error, and -`a_published_scene_has_its_own_broadcast` passes every time. - -## Plan - -From [moq-dev/studio#81](https://github.com/moq-dev/studio/pull/81), picked -2026-11-07 (maintainer away under /afk, recommended option), m0 by the rule -that fixes join m0. - -The studio closes a scene's broadcast as soon as its encoders finish their -tracks locally, so the relay can unroute it before a track's end reaches the -viewer. The test above failed 3 times in about 33 runs on a loaded host from -this race. Quitting already waits until the relay has what it was sent -(`Output::close` in `rs/moq-studio/src/engine.rs`). - -- ✅ **Unpublishing waits the same way** before it closes the broadcast. - Rejected for now: changing moq-net to deliver a finished track's end after - its broadcast closes, which would land upstream first and is only worth it - if the studio's wait cannot cover every case. diff --git a/rs/moq-studio/src/engine.rs b/rs/moq-studio/src/engine.rs index 5ae01b2..ea0813b 100644 --- a/rs/moq-studio/src/engine.rs +++ b/rs/moq-studio/src/engine.rs @@ -3463,11 +3463,34 @@ mod tests { (broadcast, catalogs, catalog) } - /// Read a published scene's `video` and `catalogs` to a clean end. - async fn ended(mut catalogs: moq_mux::catalog::Consumer<()>, mut video: moq_net::track::Subscriber) { + /// A viewer of `broadcast`'s `video`, and the first group the relay + /// handed it once it answered the subscription. The relay refuses one + /// still unanswered when the scene's broadcast goes, since the program's + /// broadcast still covers its path there. + async fn served( + broadcast: &moq_net::broadcast::Consumer, + video: &str, + ) -> (moq_net::track::Subscriber, moq_net::group::Consumer) { + let mut track = broadcast.track(video).unwrap().subscribe(None).await.unwrap(); + let first = kio::clock::timeout(TIMEOUT, track.recv_group()) + .await + .expect("the relay serves the video") + .expect("the video") + .expect("a group"); + (track, first) + } + + /// Read a published scene's `video`, from the first group it was served, + /// and its `catalogs` to a clean end. + async fn ended( + mut catalogs: moq_mux::catalog::Consumer<()>, + (mut video, first): (moq_net::track::Subscriber, moq_net::group::Consumer), + ) { kio::clock::timeout(TIMEOUT, async { - while let Some(mut group) = video.recv_group().await.expect("the video finishes") { - while group.read_frame().await.expect("the video finishes").is_some() {} + let mut group = Some(first); + while let Some(mut current) = group { + while current.read_frame().await.expect("the video finishes").is_some() {} + group = video.recv_group().await.expect("the video finishes"); } while catalogs.next().await.expect("the catalog finishes").is_some() {} }) @@ -3480,8 +3503,9 @@ mod tests { /// more encode session. /// What only it shows is shown but not on program. Health grades its /// video beside the program, with its own viewers. Unpublishing - /// it, or quitting, ends its video and then its catalog cleanly, then its - /// broadcast; unpublishing gives its session back. + /// it, or quitting, ends its video and then its catalog cleanly for a + /// viewer the relay serves, then its broadcast; unpublishing gives its + /// session back. #[tokio::test(flavor = "multi_thread")] #[ignore = "needs a Vulkan device and an H.264 encoder; run by just rs render-sw"] async fn a_published_scene_has_its_own_broadcast() { @@ -3601,7 +3625,7 @@ mod tests { .await .expect("health grades the published scene"); - let track = broadcast.track(&video).unwrap().subscribe(None).await.unwrap(); + let track = served(&broadcast, &video).await; { let mut state = engine.state.lock().unwrap(); let scene = state.collection().scenes[1].clone(); @@ -3631,7 +3655,7 @@ mod tests { engine.state.lock().unwrap().undo().unwrap(); let (broadcast, catalogs, catalog) = routed(&relay, "studio/tall").await; let (video, _) = canvas_size(&catalog); - let track = broadcast.track(&video).unwrap().subscribe(None).await.unwrap(); + let track = served(&broadcast, &video).await; let stopping = engine.clone(); tokio::task::spawn_blocking(move || stopping.stop()).await.unwrap(); kio::clock::timeout(TIMEOUT, engine.close())