From 448417b723d096d697eb892c05bba0c9456f2191 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 9 Nov 2026 15:46:42 -0800 Subject: [PATCH 01/10] wip(json): finishing a producer returns a Finished handle --- rs/moq-json/examples/telemetry.rs | 6 +-- rs/moq-json/examples/teleop.rs | 4 +- rs/moq-json/src/snapshot/consumer.rs | 4 +- rs/moq-json/src/snapshot/mod.rs | 65 ++++++++++++++-------------- rs/moq-json/src/snapshot/producer.rs | 48 ++++++++++++++++++-- rs/moq-json/src/stream/consumer.rs | 2 +- rs/moq-json/src/stream/mod.rs | 20 ++++----- rs/moq-json/src/stream/producer.rs | 46 ++++++++++++++++++-- rs/moq-json/src/window/mod.rs | 47 ++++++++++---------- rs/moq-json/src/window/producer.rs | 55 ++++++++++++++++++++--- 10 files changed, 212 insertions(+), 85 deletions(-) diff --git a/rs/moq-json/examples/telemetry.rs b/rs/moq-json/examples/telemetry.rs index 8047000f47a..2c72b7853fe 100644 --- a/rs/moq-json/examples/telemetry.rs +++ b/rs/moq-json/examples/telemetry.rs @@ -100,7 +100,7 @@ fn frame_bytes(config: snapshot::Config, ticks: u64) -> usize { .update(moq_net::Timed::from(&telemetry(tick)).at(moq_net::Timestamp::now())) .unwrap(); } - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); // Drain the frames and sum their sizes. let waiter = kio::Waiter::noop(); @@ -134,7 +134,7 @@ fn verify(producer_config: snapshot::Config, compress: bool, ticks: u64) { other => panic!("tick {tick}: expected a value, got {other:?}"), } } - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); // Drain: nothing left and the stream ends cleanly. assert!( @@ -158,7 +158,7 @@ fn verify_late_joiner(producer_config: snapshot::Config, compress: bool, ticks: .update(moq_net::Timed::from(&telemetry(tick)).at(moq_net::Timestamp::now())) .unwrap(); } - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let waiter = kio::Waiter::noop(); let mut last = None; diff --git a/rs/moq-json/examples/teleop.rs b/rs/moq-json/examples/teleop.rs index 6f2e8f4b8a1..a04b86145c2 100644 --- a/rs/moq-json/examples/teleop.rs +++ b/rs/moq-json/examples/teleop.rs @@ -42,14 +42,14 @@ fn main() -> Result<(), Box> { { state.update(Timed::from(control).at(Timestamp::from_millis(tick as u64)?))?; } - state.finish()?; + let _state = state.finish()?; // Every discrete command is meaningful, including Stop between Arm and Disarm. let expected = [Command::Arm, Command::Stop, Command::Disarm]; for (tick, command) in expected.iter().enumerate() { commands.append(Timed::from(command).at(Timestamp::from_millis(tick as u64)?))?; } - commands.finish()?; + let _commands = commands.finish()?; let waiter = kio::Waiter::noop(); // `latest` skips the stale steering values; `next` would replay them in order. diff --git a/rs/moq-json/src/snapshot/consumer.rs b/rs/moq-json/src/snapshot/consumer.rs index 777bdd83660..1b9618683d7 100644 --- a/rs/moq-json/src/snapshot/consumer.rs +++ b/rs/moq-json/src/snapshot/consumer.rs @@ -258,7 +258,7 @@ mod timestamps { assert_eq!(at.value(), ticks); assert_eq!(at.scale(), scale); } - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); assert!(matches!( consumer.poll_next(&kio::Waiter::noop()), Poll::Ready(Ok(None)) @@ -310,7 +310,7 @@ mod timestamps { panic!("missing new state") }; assert_eq!(value.value["n"].as_u64().unwrap(), 3); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); assert!(matches!(consumer.poll_latest(&waiter), Poll::Ready(Ok(None)))); } } diff --git a/rs/moq-json/src/snapshot/mod.rs b/rs/moq-json/src/snapshot/mod.rs index e25c6c2e0ad..35267cee6e1 100644 --- a/rs/moq-json/src/snapshot/mod.rs +++ b/rs/moq-json/src/snapshot/mod.rs @@ -53,7 +53,7 @@ pub mod producer; pub use consumer::Consumer; pub use decoder::Decoder; pub use encoder::{Config, Encoded, Encoder, Pending}; -pub use producer::{Guard, Producer}; +pub use producer::{Finished, Guard, Producer}; #[cfg(test)] mod test { @@ -122,7 +122,7 @@ mod test { producer.update(&json!({ "n": 2 })), Err(crate::Error::Net(moq_net::Error::TimestampMismatch)) )); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); assert_eq!(drain(track), vec![json!({ "n": 1 })], "deltas={deltas}"); } } @@ -142,7 +142,7 @@ mod test { producer.update(moq_net::Timed::from(&json!({ "n": 2 })).at(huge)), Err(crate::Error::Net(moq_net::Error::TimestampMismatch)) )); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); assert_eq!(drain(track), vec![json!({ "n": 1 })], "deltas={deltas}"); } } @@ -228,7 +228,7 @@ mod test { .update(&json!({ "padding": "same-long-value-so-deltas-stay-in-one-group", "n": n })) .unwrap(); } - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let mut consumer = Consumer::::new(track, consumer::Config::default()); let mut values = Vec::new(); while let Poll::Ready(Ok(Some(value))) = consumer.poll_next(&kio::Waiter::noop()) { @@ -307,7 +307,7 @@ mod test { producer.update(&json!({ "a": 1, "b": 2 })).unwrap(); producer.cut().unwrap(); producer.update(&json!({ "a": 1, "b": 3 })).unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); // The ratio would have kept every update in one group; the cut rolled it anyway. A consumer // joining at the new group reads the whole value from its first frame, with none of the deltas @@ -328,7 +328,7 @@ mod test { // An unchanged value normally writes nothing. After a cut it must still open the replacement // group, or the value would only exist in a group no new consumer reads. producer.update(&json!({ "a": 1 })).unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); assert_eq!( track.largest().now_or_never().flatten().map(|position| position.group), @@ -342,7 +342,7 @@ mod test { let (mut producer, mut track) = producer(cfg(true)); producer.update(&json!({ "a": 1 })).unwrap(); producer.cut().unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); // Cutting closes the open group and stops there: no empty group for a consumer to advance into // and wait on. @@ -370,7 +370,7 @@ mod test { producer.cut().unwrap(); producer.cut().unwrap(); producer.update(&json!({ "a": 2 })).unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); assert_eq!( track.largest().now_or_never().flatten().map(|position| position.group), @@ -387,7 +387,7 @@ mod test { // With deltas off every frame already closes its own group, so there is never one to cut. producer.cut().unwrap(); producer.update(&json!({ "a": 2 })).unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); assert_eq!( track.largest().now_or_never().flatten().map(|position| position.group), @@ -401,7 +401,7 @@ mod test { let (mut producer, mut track) = producer(cfg(false)); producer.update(&json!({ "a": 1 })).unwrap(); producer.update(&json!({ "a": 2 })).unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); // Two updates => two groups, each a full snapshot. A consumer that joins after both // exist only sees the latest, like the existing catalog consumer. @@ -439,7 +439,7 @@ mod test { // Serializes past the group cache limit, so the frame cannot be published. let oversized = json!({ "big": "x".repeat(crate::MAX_GROUP_BYTES as usize + 1) }); assert!(producer.update(&oversized).is_err()); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); // A reader arriving now still finds the last good value, not an empty superseding group. assert_eq!(drain(track), vec![json!({ "keep": true })]); @@ -454,7 +454,7 @@ mod test { None, "an unchanged value reports that nothing was written" ); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); assert_eq!( track.largest().now_or_never().flatten().map(|position| position.group), @@ -497,7 +497,7 @@ mod test { producer.update(&json!({ "n": n })).unwrap(); } assert!(producer.poll_refresh(&kio::Waiter::noop()).is_pending()); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); assert_eq!( track.largest().now_or_never().flatten().map(|position| position.group), @@ -630,7 +630,7 @@ mod test { producer .update(moq_net::Timed::from(&json!({ "n": 4 })).at(ms(104))) .unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); assert_eq!(drain(track).last(), Some(&json!({ "n": 4 }))); } @@ -658,7 +658,8 @@ mod test { let _late = producer.consume(); assert!(matches!(refresh.now_or_never(), Some(Ok(())))); - producer.finish().unwrap(); + // A clone finishes, so the shared state refuses this handle's refresh. + let _finished = producer.clone().finish().unwrap(); assert!(matches!(producer.refresh().now_or_never(), Some(Err(_)))); } @@ -667,7 +668,7 @@ mod test { let (mut producer, mut track) = producer(cfg(true)); producer.update(&json!({ "list": [1, 2] })).unwrap(); producer.update(&json!({ "list": [1, 2, 3] })).unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); // The array is replaced wholesale in a delta, so it stays in the same group. assert_eq!( @@ -684,7 +685,7 @@ mod test { for i in 0..=MAX_DELTA_FRAMES { producer.update(&json!({ "n": i })).unwrap(); } - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); // The frame cap forced exactly one extra snapshot group despite the huge ratio. assert_eq!( @@ -721,7 +722,7 @@ mod test { assert!(matches!(producer.modify(None), Err(crate::Error::Json(_)))); // The refused edit published nothing after the original value. - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); assert_eq!(drain_with(consumer), vec![json!({ "count": "1", "other": 1 })]); } @@ -755,7 +756,7 @@ mod test { let mut producer = Producer::::new(track.clone(), cfg(false)); producer.update(&json!({ "a": 1 })).unwrap(); track.finish_at(moq_net::track::Position::group(10), None).unwrap(); - producer.finish().unwrap(); + let _finished = producer.clone().finish().unwrap(); assert!(matches!( producer.update(&json!({ "a": 2 })), @@ -794,7 +795,7 @@ mod test { // Locking without mutating publishes nothing (the guard stays clean). let _ = producer.modify(None).unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let waiter = kio::Waiter::noop(); let mut last = None; @@ -842,7 +843,7 @@ mod test { Some(1) ); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let waiter = kio::Waiter::noop(); let mut last = None; @@ -881,7 +882,7 @@ mod test { *producer.modify(None).unwrap() = json!({ "a": 1, "b": 1 }); *producer.modify(None).unwrap() = json!({ "a": 1, "b": 2 }); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); // The ratio keeps both edits in one group: a snapshot plus a merge-patch delta. assert_eq!( @@ -895,7 +896,7 @@ mod test { fn modify_refuses_a_finished_track() { let (mut producer, track) = producer(cfg(false)); producer.update(&json!({ "a": 1 })).unwrap(); - producer.finish().unwrap(); + let _finished = producer.clone().finish().unwrap(); assert!(matches!( producer.modify(None), @@ -942,7 +943,7 @@ mod test { // The caller got the error, so the track stays usable for a smaller value. *producer.modify(None).unwrap() = json!({ "keep": false }); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); assert_eq!(drain(track).last().unwrap(), &json!({ "keep": false })); } @@ -960,7 +961,7 @@ mod test { // A discarded edit is not a publish failure, so the track is still open for the next one. *producer.modify(None).unwrap() = json!({ "a": 3 }); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); assert_eq!( track.largest().now_or_never().flatten().map(|position| position.group), Some(1), @@ -998,7 +999,7 @@ mod test { let (mut producer, _track) = producer(cfg(true)); producer.update(&json!({ "a": 1 })).unwrap(); producer.update(&json!({ "a": 2 })).unwrap(); // a delta, so a group is open - producer.finish().unwrap(); + let _finished = producer.clone().finish().unwrap(); assert!(matches!(producer.update(&json!({ "a": 3 })), Err(crate::Error::Net(_)))); } @@ -1071,7 +1072,7 @@ mod test { guard.commit().unwrap(); // The drop that follows `commit` must not publish a second group. - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); assert_eq!( consumer .largest() @@ -1100,7 +1101,7 @@ mod test { producer.update(&json!({ "a": 3 })).unwrap(); // delta in group 0 producer.cut().unwrap(); producer.update(&json!({ "a": 4 })).unwrap(); // snapshot, group 1 - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); assert_eq!( observer .largest() @@ -1156,7 +1157,7 @@ mod test { for n in 0..=20 { producer.update(&json!({ "n": n })).unwrap(); } - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); // One group (ratio is generous), so a single poll drains the backlog into one yield. assert_eq!( @@ -1178,7 +1179,7 @@ mod test { for n in 0..=20 { producer.update(&json!({ "n": n })).unwrap(); } - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); assert_eq!( track.largest().now_or_never().flatten().map(|position| position.group), @@ -1197,7 +1198,7 @@ mod test { let (mut producer, mut track) = producer_on(cfg(false), compressed()); producer.update(&json!({ "a": 1 })).unwrap(); producer.update(&json!({ "a": 2 })).unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); // Deltas disabled: one compressed snapshot per group, latest reconstructs identically. assert_eq!( @@ -1222,7 +1223,7 @@ mod test { Poll::Ready(Ok(())) )); producer.update(&json!({ "n": 41 })).unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); assert_eq!( track.largest().now_or_never().flatten().map(|position| position.group), diff --git a/rs/moq-json/src/snapshot/producer.rs b/rs/moq-json/src/snapshot/producer.rs index c1326a9991f..84c9249ba82 100644 --- a/rs/moq-json/src/snapshot/producer.rs +++ b/rs/moq-json/src/snapshot/producer.rs @@ -199,9 +199,17 @@ impl Producer { take(&self.inner).cut() } - /// Finish the track, closing any open group. - pub fn finish(&mut self) -> Result<()> { - take(&self.inner).finish() + /// Finish the track, closing any open group, and return the [`Finished`] value. + /// + /// Clones sharing the track see it ended: their next [`modify`](Self::modify) or + /// [`update`](Self::update) fails. A finish that fails aborts the track with the cause, which + /// those clones then report. + pub fn finish(self) -> Result> { + take(&self.inner).finish()?; + Ok(Finished { + inner: self.inner, + _marker: PhantomData, + }) } /// Abort the track with the given error, which consumers see in place of any further value. @@ -215,6 +223,40 @@ impl Producer { } } +/// A value whose producer [finished](Producer::finish) it: it can still be read or aborted, but +/// not published to. +/// +/// Dropping the last handle sharing the track releases it, so new readers can no longer find it. +/// +/// ```compile_fail,E0599 +/// # fn cannot_update(value: moq_json::snapshot::Finished) { +/// value.update(&1).unwrap(); +/// # } +/// ``` +#[must_use = "dropping the last handle releases the track, which new readers can then no longer find"] +pub struct Finished { + inner: Arc>>, + _marker: PhantomData, +} + +impl Finished { + /// Create a subscriber for the underlying track. + pub fn consume(&self) -> moq_net::track::Subscriber { + take(&self.inner).track.inner.subscribe(None) + } + + /// A watch-only handle to the underlying track's subscriber demand. + pub fn demand(&self) -> moq_net::track::Demand { + take(&self.inner).track.inner.demand() + } + + /// Release the track's cache, reporting `err` to readers that have not drained it. + pub fn abort(self, err: moq_net::Error) -> Result<()> { + take(&self.inner).abort(err.into()); + Ok(()) + } +} + /// An RAII editing guard returned by [`Producer::modify`]. /// /// Holds the producer's lock for its lifetime and derefs to the current value. Mutating it through diff --git a/rs/moq-json/src/stream/consumer.rs b/rs/moq-json/src/stream/consumer.rs index b1e7c5e6935..5aa22682b69 100644 --- a/rs/moq-json/src/stream/consumer.rs +++ b/rs/moq-json/src/stream/consumer.rs @@ -130,7 +130,7 @@ mod timestamps { }) .unwrap(); } - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); for (n, ticks) in times.into_iter().enumerate() { let Poll::Ready(Ok(Some(value))) = consumer.poll_next(&kio::Waiter::noop()) else { panic!("missing state {n}") diff --git a/rs/moq-json/src/stream/mod.rs b/rs/moq-json/src/stream/mod.rs index afcc9602781..a8b242edb15 100644 --- a/rs/moq-json/src/stream/mod.rs +++ b/rs/moq-json/src/stream/mod.rs @@ -37,7 +37,7 @@ pub mod producer; pub use consumer::Consumer; pub use decoder::Decoder; pub use encoder::{Config, Encoder, Pending}; -pub use producer::Producer; +pub use producer::{Finished, Producer}; #[cfg(test)] use futures::FutureExt; @@ -69,7 +69,7 @@ mod test { producer .append(moq_net::Timed::from(&json!({ "n": 3 })).at(at)) .unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); assert_eq!(drain(consume(track)), vec![json!({ "n": 1 }), json!({ "n": 3 })]); } @@ -90,7 +90,7 @@ mod test { producer .append(moq_net::Timed::from(&json!({ "n": 3 })).at(at)) .unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); assert_eq!(drain(consume(track)), vec![json!({ "n": 1 }), json!({ "n": 3 })]); } @@ -170,7 +170,7 @@ mod test { for n in 0..5 { producer.append(&json!({ "n": n })).unwrap(); } - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let records = drain(consume(track)); assert_eq!(records, (0..5).map(|n| json!({ "n": n })).collect::>()); @@ -205,7 +205,7 @@ mod test { for n in 0..50 { producer.append(&json!({ "n": n })).unwrap(); } - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); // Never rolled: a single group holds the whole log. assert_eq!( @@ -229,7 +229,7 @@ mod test { } } assert!(matches!(consumer.poll_next(&waiter), Poll::Pending)); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); } /// A record the encoder rejects must not have published a group first: a live consumer would @@ -359,7 +359,7 @@ mod test { fn appending_after_finish_fails_without_aborting() { let (mut producer, _track) = producer_on(Config::default(), compressed()); producer.append(&json!({ "n": 0 })).unwrap(); - producer.finish().unwrap(); + let _finished = producer.clone().finish().unwrap(); assert!(producer.append(&json!({ "n": 1 })).is_err()); assert_eq!(drain(consume(producer.consume())), vec![json!({ "n": 0 })]); @@ -439,7 +439,7 @@ mod test { assert!(refused(producer.append(&oversized)), "compression={compression:?}"); producer.append(&json!({ "n": 1 })).unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); assert_eq!( drain(consume(track)), @@ -457,7 +457,7 @@ mod test { for n in 0..10_000 { producer.append(&json!({ "n": n })).unwrap(); } - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let records = drain(consume(track)); assert_eq!(records.len(), 10_000, "compression={compression:?}"); @@ -474,7 +474,7 @@ mod test { for _ in 0..4 { producer.append(&value).unwrap(); } - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let records = drain(consume(track)); assert_eq!(records, vec![value.clone(), value.clone(), value.clone(), value]); diff --git a/rs/moq-json/src/stream/producer.rs b/rs/moq-json/src/stream/producer.rs index 9e2e1cea049..d5e351a279b 100644 --- a/rs/moq-json/src/stream/producer.rs +++ b/rs/moq-json/src/stream/producer.rs @@ -87,9 +87,15 @@ impl Producer { self.inner.lock().unwrap().append(value.into()) } - /// Finish the track, closing the group. - pub fn finish(&mut self) -> Result<()> { - self.inner.lock().unwrap().finish() + /// Finish the track, closing the group, and return the [`Finished`] log. + /// + /// Clones sharing the track see it ended: their next [`append`](Self::append) fails. + pub fn finish(self) -> Result> { + self.inner.lock().unwrap().finish()?; + Ok(Finished { + inner: self.inner, + _marker: PhantomData, + }) } /// Abort the track with the given error, which consumers see in place of a clean end. @@ -102,6 +108,40 @@ impl Producer { } } +/// A log whose producer [finished](Producer::finish) it: it can still be read or aborted, but +/// not appended to. +/// +/// Dropping the last handle sharing the track releases it, so new readers can no longer find it. +/// +/// ```compile_fail,E0599 +/// # fn cannot_append(log: moq_json::stream::Finished) { +/// log.append(&1).unwrap(); +/// # } +/// ``` +#[must_use = "dropping the last handle releases the track, which new readers can then no longer find"] +pub struct Finished { + inner: Arc>>, + _marker: PhantomData, +} + +impl Finished { + /// Create a subscriber for the underlying track. + pub fn consume(&self) -> moq_net::track::Subscriber { + self.inner.lock().unwrap().track.inner.subscribe(None) + } + + /// A watch-only handle to the underlying track's subscriber demand. + pub fn demand(&self) -> moq_net::track::Demand { + self.inner.lock().unwrap().track.inner.demand() + } + + /// Release the track's cache, reporting `err` to readers that have not drained it. + pub fn abort(self, err: moq_net::Error) -> Result<()> { + self.inner.lock().unwrap().track.abort(err); + Ok(()) + } +} + #[cfg(test)] impl Producer { /// Shrink the frame limit to a size a test can reach. diff --git a/rs/moq-json/src/window/mod.rs b/rs/moq-json/src/window/mod.rs index 7c80de6d50c..8433ffc68d5 100644 --- a/rs/moq-json/src/window/mod.rs +++ b/rs/moq-json/src/window/mod.rs @@ -61,7 +61,7 @@ mod properties; pub use consumer::Consumer; pub use decoder::{ConsumerConfig, Decoder, Event, Group}; pub use encoder::{Encoded, Encoder, Pending, ProducerConfig}; -pub use producer::Producer; +pub use producer::{Finished, Producer}; #[cfg(test)] use futures::FutureExt; @@ -111,7 +111,7 @@ mod test { let at = moq_net::Timestamp::from_millis(100).unwrap(); producer.push(moq_net::Timed::from(&rec(1)).at(at)).unwrap(); producer.push(moq_net::Timed::from(&rec(2)).at(at)).unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let pushed: Vec<_> = drain(&mut consumer(track)) .into_iter() .filter_map(|event| match event { @@ -148,7 +148,7 @@ mod test { producer.push(moq_net::Timed::from(&rec(2)).at(at)).unwrap(); assert_eq!(largest(&mut track), Some(0), "a refused op rolled the group"); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let pushed: Vec<_> = drain(&mut consumer(track)) .into_iter() .filter_map(|event| match event { @@ -372,11 +372,11 @@ mod test { fn finish(self) -> Vec> { let Live { - mut producer, + producer, mut consumer, mut events, } = self; - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); events.extend(drain(&mut consumer)); events } @@ -551,7 +551,7 @@ mod test { .unwrap(); subscriber = subscriber.with_range(largest..); let mut fresh = consumer(subscriber); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); // Joining at offset 3 must not report 3 skips for records that were never this reader's to // miss: it simply starts where the window starts. @@ -798,7 +798,8 @@ mod test { let _late = producer.consume(); assert!(producer.poll_refresh(&kio::Waiter::noop()).is_pending()); - producer.finish().unwrap(); + // A clone finishes, so the shared state refuses this handle's refresh. + let _finished = producer.clone().finish().unwrap(); assert!(matches!(producer.refresh().now_or_never(), Some(Err(_)))); } @@ -826,7 +827,7 @@ mod test { fn an_empty_pop_writes_nothing() { let (mut producer, mut track) = producer(ProducerConfig::default()); producer.pop(5).unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); // Nothing was ever pushed, so there is nothing to drop and no group to publish. assert_eq!( @@ -854,23 +855,25 @@ mod test { } #[test] - fn the_handle_remains_usable_after_finish() { - let (mut producer, mut track) = producer(ProducerConfig::default()); - producer.push(&rec(1)).unwrap(); - producer.finish().unwrap(); - - assert_eq!(producer.window(), vec![rec(1)]); - assert_eq!(producer.range(), 0..1); - let mut mirror = producer.consume(); + fn the_window_stays_readable_after_finish() { + let (producer, mut track) = producer(ProducerConfig::default()); + let mut clone = producer.clone(); + clone.push(&rec(1)).unwrap(); + let finished = producer.finish().unwrap(); + + assert_eq!(finished.window(), vec![rec(1)]); + assert_eq!(finished.range(), 0..1); + let mut mirror = finished.consume(); assert_eq!( mirror.largest().now_or_never().flatten().map(|position| position.group), track.largest().now_or_never().flatten().map(|position| position.group) ); - producer.finish().unwrap(); + // A clone sharing the finished state refuses edits, and finishing it again does nothing. assert!(matches!( - producer.push(&rec(2)), + clone.push(&rec(2)), Err(crate::Error::Net(moq_net::Error::Closed)) )); + let _finished = clone.finish().unwrap(); } /// A finish that fails is final: the track aborts, and a retry reports the failure rather than @@ -886,8 +889,8 @@ mod test { // Another handle ends the track, so finishing it fails. track.abort(moq_net::Error::Cancel).unwrap(); - let err = producer.finish().unwrap_err().to_string(); - assert_eq!(producer.finish().unwrap_err().to_string(), err); + let err = producer.clone().finish().err().unwrap().to_string(); + assert_eq!(producer.clone().finish().err().unwrap().to_string(), err); assert_eq!(producer.push(&rec(2)).unwrap_err().to_string(), err); } @@ -903,7 +906,7 @@ mod test { let mut producer = Producer::::new(track.clone(), ProducerConfig::default()); producer.push(&rec(1)).unwrap(); let _track = track.finish().unwrap(); - producer.finish().unwrap(); + let _finished = producer.clone().finish().unwrap(); let waiter = kio::Waiter::noop(); let Poll::Ready(Ok(Some(mut group))) = subscriber.poll_next_group(&waiter) else { @@ -941,7 +944,7 @@ mod test { fn writes_after_another_clone_finishes_are_rejected() { let (mut producer, _track) = producer(ProducerConfig::default()); producer.push(&rec(1)).unwrap(); - producer.clone().finish().unwrap(); + let _finished = producer.clone().finish().unwrap(); assert!(matches!( producer.push(&rec(2)), diff --git a/rs/moq-json/src/window/producer.rs b/rs/moq-json/src/window/producer.rs index 37d84b9854e..2f3827541f6 100644 --- a/rs/moq-json/src/window/producer.rs +++ b/rs/moq-json/src/window/producer.rs @@ -102,14 +102,16 @@ impl Producer { self.inner.lock().unwrap().poll_refresh(waiter) } - /// Finish the track, closing any open group. + /// Finish the track, closing any open group, and return the [`Finished`] window. /// - /// Borrows rather than consumes, matching snapshot and stream, so the handle stays - /// usable afterwards (reads, a second finish). Writes after this fail with - /// [`moq_net::Error::Closed`]. A finish that fails aborts the track instead, and every later - /// call returns its cause. - pub fn finish(&mut self) -> Result<()> { - self.inner.lock().unwrap().finish() + /// Clones sharing the track see it ended: their edits fail with [`moq_net::Error::Closed`]. A + /// finish that fails aborts the track instead, and those clones then report its cause. + pub fn finish(self) -> Result> { + self.inner.lock().unwrap().finish()?; + Ok(Finished { + inner: self.inner, + _marker: PhantomData, + }) } /// Abort the track with the given error, which consumers see in place of a clean end. @@ -132,6 +134,45 @@ impl Producer { } } +/// A window whose producer [finished](Producer::finish) it: it can still be read or aborted, but +/// not edited. +/// +/// Dropping the last handle sharing the track releases it, so new readers can no longer find it. +/// +/// ```compile_fail,E0599 +/// # fn cannot_push(window: moq_json::window::Finished) { +/// window.push(&1).unwrap(); +/// # } +/// ``` +#[must_use = "dropping the last handle releases the track, which new readers can then no longer find"] +pub struct Finished { + inner: Arc>>, + _marker: PhantomData, +} + +impl Finished { + /// Create a subscriber for the underlying track. + pub fn consume(&self) -> moq_net::track::Subscriber { + self.inner.lock().unwrap().track.inner.subscribe(None) + } + + /// The retained checkpoint suffix, oldest first; see [`Producer::window`]. + pub fn window(&self) -> Vec { + self.inner.lock().unwrap().encoder.window() + } + + /// Absolute index of the oldest retained record, and of the next one; see [`Producer::range`]. + pub fn range(&self) -> std::ops::Range { + self.inner.lock().unwrap().encoder.range() + } + + /// Release the track's cache, reporting `err` to readers that have not drained it. + pub fn abort(self, err: moq_net::Error) -> Result<()> { + self.inner.lock().unwrap().abort(err.into()); + Ok(()) + } +} + /// Shared publishing state behind [`Producer`]'s `Arc`. /// /// The track and the encoder are separate fields so a [`Pending`](super::Pending) frame (which From 21d964016c821f63a821cc7968720a4e8e89cfbd Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 9 Nov 2026 15:58:55 -0800 Subject: [PATCH 02/10] wip(mux): importers and the container producer finish into a Finished handle --- rs/moq-mux/src/catalog/producer.rs | 4 +- rs/moq-mux/src/codec/aac/import.rs | 2 +- rs/moq-mux/src/codec/av1/import.rs | 2 +- rs/moq-mux/src/codec/flac/import.rs | 2 +- rs/moq-mux/src/codec/h264/import.rs | 2 +- rs/moq-mux/src/codec/h265/import.rs | 2 +- rs/moq-mux/src/codec/legacy.rs | 2 +- rs/moq-mux/src/codec/mp3.rs | 2 +- rs/moq-mux/src/codec/opus/import.rs | 2 +- rs/moq-mux/src/codec/vp8/import.rs | 2 +- rs/moq-mux/src/codec/vp9/import.rs | 2 +- rs/moq-mux/src/container/flv/import.rs | 36 ++++++++- rs/moq-mux/src/container/fmp4/import.rs | 38 ++++++++-- rs/moq-mux/src/container/mkv/import.rs | 37 +++++++-- rs/moq-mux/src/container/mod.rs | 2 +- rs/moq-mux/src/container/producer.rs | 64 ++++++++++++++-- rs/moq-mux/src/container/ts/import.rs | 49 ++++++++++-- rs/moq-mux/src/container/ts/mod.rs | 4 +- rs/moq-mux/src/container/ts/passthrough.rs | 67 ++++++++++------- rs/moq-mux/src/container/ts/programs.rs | 87 ++++++++++++---------- rs/moq-mux/src/import/container.rs | 62 ++++++++++++--- rs/moq-mux/src/import/mod.rs | 4 +- rs/moq-mux/src/json.rs | 12 ++- 23 files changed, 360 insertions(+), 126 deletions(-) diff --git a/rs/moq-mux/src/catalog/producer.rs b/rs/moq-mux/src/catalog/producer.rs index a40ab4fd911..316e7e28dcd 100644 --- a/rs/moq-mux/src/catalog/producer.rs +++ b/rs/moq-mux/src/catalog/producer.rs @@ -1088,8 +1088,8 @@ impl Producer { if let Some(err) = &state.closed { return Err(err.clone()); } - let result = self.outputs.hang.finish().map_err(crate::Error::from).and_then(|_| { - // The shared outputs keep their own clone, whose appends the end refuses. + // The shared outputs keep their own clones, whose appends the end refuses. + let result = self.outputs.hang.clone().finish().map_err(crate::Error::from).and_then(|_| { let msf = self.outputs.msf_track.clone().finish(); msf.map(drop).map_err(crate::Error::from) }); diff --git a/rs/moq-mux/src/codec/aac/import.rs b/rs/moq-mux/src/codec/aac/import.rs index 831e8a4408e..a0500fa4e98 100644 --- a/rs/moq-mux/src/codec/aac/import.rs +++ b/rs/moq-mux/src/codec/aac/import.rs @@ -56,7 +56,7 @@ impl Import { /// Finish the track, flushing the current group. pub fn finish(&mut self) -> crate::Result<()> { - self.track.finish()?; + self.track.end_with(None)?; Ok(()) } diff --git a/rs/moq-mux/src/codec/av1/import.rs b/rs/moq-mux/src/codec/av1/import.rs index aad3d1d6e31..6e887823619 100644 --- a/rs/moq-mux/src/codec/av1/import.rs +++ b/rs/moq-mux/src/codec/av1/import.rs @@ -199,7 +199,7 @@ impl Import { /// Finish the track, flushing the current group. pub fn finish(&mut self) -> Result<()> { - self.track.finish()?; + self.track.end_with(None)?; Ok(()) } diff --git a/rs/moq-mux/src/codec/flac/import.rs b/rs/moq-mux/src/codec/flac/import.rs index 2bf10ec2f58..d5b540c35b3 100644 --- a/rs/moq-mux/src/codec/flac/import.rs +++ b/rs/moq-mux/src/codec/flac/import.rs @@ -47,7 +47,7 @@ impl Import { /// Finish the track, flushing the current group. pub fn finish(&mut self) -> crate::Result<()> { - self.track.finish()?; + self.track.end_with(None)?; Ok(()) } diff --git a/rs/moq-mux/src/codec/h264/import.rs b/rs/moq-mux/src/codec/h264/import.rs index de1418010c7..331988bea31 100644 --- a/rs/moq-mux/src/codec/h264/import.rs +++ b/rs/moq-mux/src/codec/h264/import.rs @@ -133,7 +133,7 @@ impl Import { /// Finish the track, flushing any buffered data. pub fn finish(&mut self) -> Result<()> { - self.track.finish()?; + self.track.end_with(None)?; Ok(()) } diff --git a/rs/moq-mux/src/codec/h265/import.rs b/rs/moq-mux/src/codec/h265/import.rs index 2db0d58542f..cc491e88c8a 100644 --- a/rs/moq-mux/src/codec/h265/import.rs +++ b/rs/moq-mux/src/codec/h265/import.rs @@ -136,7 +136,7 @@ impl Import { /// Finish the track, flushing the current group. pub fn finish(&mut self) -> Result<()> { - self.track.finish()?; + self.track.end_with(None)?; Ok(()) } diff --git a/rs/moq-mux/src/codec/legacy.rs b/rs/moq-mux/src/codec/legacy.rs index f6ccd5b96a9..538e9ddc414 100644 --- a/rs/moq-mux/src/codec/legacy.rs +++ b/rs/moq-mux/src/codec/legacy.rs @@ -159,7 +159,7 @@ impl Import { /// Finish the track, flushing the current group. pub fn finish(&mut self) -> crate::Result<()> { - self.track.finish()?; + self.track.end_with(None)?; Ok(()) } diff --git a/rs/moq-mux/src/codec/mp3.rs b/rs/moq-mux/src/codec/mp3.rs index 1a86ae4ca9b..40f22612ea0 100644 --- a/rs/moq-mux/src/codec/mp3.rs +++ b/rs/moq-mux/src/codec/mp3.rs @@ -136,7 +136,7 @@ impl Import { /// Finish the track, flushing the current group. pub fn finish(&mut self) -> crate::Result<()> { - self.track.finish()?; + self.track.end_with(None)?; Ok(()) } diff --git a/rs/moq-mux/src/codec/opus/import.rs b/rs/moq-mux/src/codec/opus/import.rs index d235f586a36..6e58476edf0 100644 --- a/rs/moq-mux/src/codec/opus/import.rs +++ b/rs/moq-mux/src/codec/opus/import.rs @@ -49,7 +49,7 @@ impl Import { /// Finish the track, flushing the current group. pub fn finish(&mut self) -> crate::Result<()> { - self.track.finish()?; + self.track.end_with(None)?; Ok(()) } diff --git a/rs/moq-mux/src/codec/vp8/import.rs b/rs/moq-mux/src/codec/vp8/import.rs index df6afb36b8a..bfc2222ef63 100644 --- a/rs/moq-mux/src/codec/vp8/import.rs +++ b/rs/moq-mux/src/codec/vp8/import.rs @@ -97,7 +97,7 @@ impl Import { /// Finish the track, flushing the current group. pub fn finish(&mut self) -> crate::Result<()> { - self.track.finish()?; + self.track.end_with(None)?; Ok(()) } diff --git a/rs/moq-mux/src/codec/vp9/import.rs b/rs/moq-mux/src/codec/vp9/import.rs index 33cf292665c..cb17472208b 100644 --- a/rs/moq-mux/src/codec/vp9/import.rs +++ b/rs/moq-mux/src/codec/vp9/import.rs @@ -98,7 +98,7 @@ impl Import { /// Finish the track, flushing the current group. pub fn finish(&mut self) -> crate::Result<()> { - self.track.finish()?; + self.track.end_with(None)?; Ok(()) } diff --git a/rs/moq-mux/src/container/flv/import.rs b/rs/moq-mux/src/container/flv/import.rs index 893746c3eaa..5cb8cc2fb4a 100644 --- a/rs/moq-mux/src/container/flv/import.rs +++ b/rs/moq-mux/src/container/flv/import.rs @@ -616,11 +616,16 @@ impl Import { Ok(()) } - /// Finish every track, flushing the current group. + /// Finish every track, flushing the current group, and return the [`Finished`] import. /// - /// Every track ends even when one fails to, aborting with the cause, and a finish that failed - /// returns the same error to every later call. Calling it again after a success does nothing. - pub fn finish(&mut self) -> crate::Result<()> { + /// Every track ends even when one fails to, aborting with the cause, so a failed finish can't + /// be retried. + pub fn finish(mut self) -> crate::Result> { + self.finish_in_place()?; + Ok(Finished { import: self }) + } + + fn finish_in_place(&mut self) -> crate::Result<()> { match &self.input { crate::container::Input::Open => {} crate::container::Input::Finished => return Ok(()), @@ -661,6 +666,29 @@ impl Import { } } +/// An FLV import that [finished](Import::finish): every track has ended, and they can still be +/// read or aborted, but it takes no more input. +/// +/// Dropping it retires the tracks and their catalog renditions, as dropping the [`Import`] does. +/// +/// ```compile_fail,E0599 +/// # fn cannot_decode(mut import: moq_mux::container::flv::Finished) { +/// import.decode(b"").unwrap(); +/// # } +/// ``` +#[must_use = "dropping it retires the tracks and their catalog renditions"] +pub struct Finished { + import: Import, +} + +impl Finished { + /// Release every track's cache, reporting `err` to readers that have not drained it; see + /// [`Import::abort`]. + pub fn abort(self, err: moq_net::Error) { + self.import.abort(err); + } +} + impl Drop for Import { fn drop(&mut self) { let (video, audio) = (std::mem::take(&mut self.video), std::mem::take(&mut self.audio)); diff --git a/rs/moq-mux/src/container/fmp4/import.rs b/rs/moq-mux/src/container/fmp4/import.rs index 96df5021dda..5f8164a3d28 100644 --- a/rs/moq-mux/src/container/fmp4/import.rs +++ b/rs/moq-mux/src/container/fmp4/import.rs @@ -1688,16 +1688,19 @@ fn open_sample( } impl Import { - /// Finish all tracks, flushing current groups. + /// Finish all tracks, flushing current groups, and return the [`Finished`] import. /// /// A fragment whose samples are still arriving was truncated: its open frames abort, failing /// their groups, every track still finishes, and this returns /// [`UnexpectedEof`](mp4_atom::Error::UnexpectedEof). After a decode error, this ends the - /// accepted media without reading more input; it does not clear that error for later decodes. - /// Either way every track has ended. A finish that failed returns the same error to every - /// later call; one that succeeded refuses later input with - /// [`Finished`](crate::Error::Finished), or with the decode error it followed. - pub fn finish(&mut self) -> Result<()> { + /// accepted media without reading more input. Either way every track has ended, so a failed + /// finish can't be retried. + pub fn finish(mut self) -> Result> { + self.finish_in_place()?; + Ok(Finished { import: self }) + } + + fn finish_in_place(&mut self) -> Result<()> { match &self.input { crate::container::Input::Finished => return Ok(()), crate::container::Input::Failed(err) => return Err(err.clone()), @@ -1850,6 +1853,29 @@ struct Deferred { discontinuity: bool, } +/// An fMP4 import that [finished](Import::finish): every track has ended, and they can still be +/// read or aborted, but it takes no more input. +/// +/// Dropping it retires the tracks and their catalog renditions, as dropping the [`Import`] does. +/// +/// ```compile_fail,E0599 +/// # fn cannot_decode(mut import: moq_mux::container::fmp4::Finished) { +/// import.decode(b"").unwrap(); +/// # } +/// ``` +#[must_use = "dropping it retires the tracks and their catalog renditions"] +pub struct Finished { + import: Import, +} + +impl Finished { + /// Release every track's cache, reporting `err` to readers that have not drained it; see + /// [`Import::abort`]. + pub fn abort(self, err: moq_net::Error) { + self.import.abort(err); + } +} + impl Drop for Import { /// End each track, then retire its HLS records: every track takes a fresh name, so nothing /// publishes it again, as when an HLS import replaces this init segment's importer. diff --git a/rs/moq-mux/src/container/mkv/import.rs b/rs/moq-mux/src/container/mkv/import.rs index 091de0a9678..a4024c63e97 100644 --- a/rs/moq-mux/src/container/mkv/import.rs +++ b/rs/moq-mux/src/container/mkv/import.rs @@ -906,14 +906,18 @@ impl Import { Ok(()) } - /// Finish all tracks, flushing current groups. + /// Finish all tracks, flushing current groups, and return the [`Finished`] import. /// /// Input that ends inside an element other than an unknown-size Segment or Cluster is refused as /// truncated, but the tracks still end with what was accepted, as they do after a refused - /// [`decode`](Self::decode). Either way every track has ended. A finish that failed returns - /// the same error to every later call; one that succeeded refuses later input with - /// [`Finished`](crate::Error::Finished), or with the refusal it followed. - pub fn finish(&mut self) -> Result<()> { + /// [`decode`](Self::decode). Either way every track has ended, so a failed finish can't be + /// retried. + pub fn finish(mut self) -> Result> { + self.finish_in_place()?; + Ok(Finished { import: self }) + } + + fn finish_in_place(&mut self) -> Result<()> { let mut result = match &self.input { crate::container::Input::Finished => return Ok(()), crate::container::Input::Failed(err) => return Err(err.clone()), @@ -969,6 +973,29 @@ impl Import { } } +/// An MKV import that [finished](Import::finish): every track has ended, and they can still be +/// read or aborted, but it takes no more input. +/// +/// Dropping it retires the tracks and their catalog renditions, as dropping the [`Import`] does. +/// +/// ```compile_fail,E0599 +/// # fn cannot_decode(mut import: moq_mux::container::mkv::Finished) { +/// import.decode(b"").unwrap(); +/// # } +/// ``` +#[must_use = "dropping it retires the tracks and their catalog renditions"] +pub struct Finished { + import: Import, +} + +impl Finished { + /// Release every track's cache, reporting `err` to readers that have not drained it; see + /// [`Import::abort`]. + pub fn abort(self, err: moq_net::Error) { + self.import.abort(err); + } +} + impl Drop for Import { /// End each track, then retire its HLS records: every track takes a fresh name, so nothing /// publishes it again. diff --git a/rs/moq-mux/src/container/mod.rs b/rs/moq-mux/src/container/mod.rs index cb111e7e307..c903ee3b5ba 100644 --- a/rs/moq-mux/src/container/mod.rs +++ b/rs/moq-mux/src/container/mod.rs @@ -35,7 +35,7 @@ pub mod ts; pub use consumer::{Consumer, Playout}; pub use group::GroupConsumer; pub(crate) use input::Input; -pub use producer::{Guard, Producer}; +pub use producer::{Finished, Guard, Producer}; pub(crate) use source::{ExportSource, Resume}; /// The media role that determines how a container represents frame durations. diff --git a/rs/moq-mux/src/container/producer.rs b/rs/moq-mux/src/container/producer.rs index 9380948fa9b..cfee461effc 100644 --- a/rs/moq-mux/src/container/producer.rs +++ b/rs/moq-mux/src/container/producer.rs @@ -385,7 +385,7 @@ impl Producer { let mut next = rendition.fresh(container, self.inner.info().clone(), interval)?; next.buffer_duration = self.buffer_duration; next.set(config)?; - self.finish()?; + self.end_with(None)?; *self = next; Ok(()) } @@ -930,9 +930,13 @@ where Ok(()) } - /// Finish the track, flushing any buffered frames and closing any open group. - pub fn finish(&mut self) -> crate::Result<()> { - self.end_with(None) + /// Finish the track, flushing any buffered frames and closing any open group, and return + /// the [`Finished`] track. + /// + /// A finish that fails ends the track with the cause instead, so it can't be retried. + pub fn finish(mut self) -> crate::Result> { + self.end_with(None)?; + Ok(Finished { producer: self }) } /// [`finish`](Self::finish) the track with `err`, for a source that failed after @@ -941,11 +945,13 @@ where /// Unlike [`abort`](Self::abort), every group written stays readable and keeps being /// served; a reader reaching the end gets `err` instead of the end of stream. Over IETF /// moq-transport, a remote subscriber is told only of a generic internal error. - pub fn fail(&mut self, err: moq_net::Error) -> crate::Result<()> { - self.end_with(Some(err)) + pub fn fail(mut self, err: moq_net::Error) -> crate::Result> { + self.end_with(Some(err))?; + Ok(Finished { producer: self }) } - fn end_with(&mut self, err: Option) -> crate::Result<()> { + /// Flush and end the track, cleanly or with `err`, for an owner that keeps this producer. + pub(crate) fn end_with(&mut self, err: Option) -> crate::Result<()> { if let Err(cause) = self.cut(None) { // A track that cannot close its last group ends with the cause rather than staying open. let _ = self.inner.clone().abort(cause.reason()); @@ -978,7 +984,7 @@ where if self.inner.finished().is_some() || self.inner.is_closed() { return Ok(()); } - self.finish() + self.end_with(None) } /// Abort the track and any open group with the given error. @@ -1001,6 +1007,48 @@ where } } +/// A media track whose producer [finished](Producer::finish) it: every frame is written, and it +/// can still be read or aborted, but not written to. +/// +/// Dropping it releases the track like dropping a [`Producer`]: it leaves its broadcast's listing, +/// and a catalog rendition it owns leaves the catalog. +/// +/// ```compile_fail,E0599 +/// # fn cannot_write(track: moq_mux::container::Finished) { +/// track.discontinuity().unwrap(); +/// # } +/// ``` +#[must_use = "dropping it releases the track and its catalog rendition"] +pub struct Finished { + producer: Producer, +} + +impl Finished +where + crate::Error: From, +{ + /// The track's name, unique within its broadcast. + pub fn name(&self) -> &str { + self.producer.inner.name() + } + + /// The properties the track was created with. + pub fn info(&self) -> &moq_net::track::Info { + self.producer.inner.info() + } + + /// Create a consumer for this track. + pub fn consume(&self) -> moq_net::track::Subscriber { + self.producer.inner.subscribe(None) + } + + /// Release the track's cache, reporting `err` to readers that have not drained it; see + /// [`moq_net::track::Finished::abort`]. + pub fn abort(self, err: moq_net::Error) { + self.producer.abort(err); + } +} + impl std::ops::Deref for Producer { type Target = moq_net::track::Producer; diff --git a/rs/moq-mux/src/container/ts/import.rs b/rs/moq-mux/src/container/ts/import.rs index 022603f25d1..641b3e8599b 100644 --- a/rs/moq-mux/src/container/ts/import.rs +++ b/rs/moq-mux/src/container/ts/import.rs @@ -1905,11 +1905,18 @@ impl Import { } } - /// Flush any buffered PES and finish every track. + /// Flush any buffered PES, finish every track, and return the [`Finished`] import. /// - /// A finish that fails aborts every track with the cause, and later calls return it again. - /// A finish that succeeded is final: calling it again does nothing. - pub fn finish(&mut self) -> anyhow::Result<()> { + /// A finish that fails aborts every track with the cause, so it can't be retried. + pub fn finish(mut self) -> anyhow::Result> { + self.finish_in_place()?; + Ok(Finished { import: self }) + } + + /// [`finish`](Self::finish) for an owner that keeps the importer, as + /// [`Programs`](super::Programs) keeps each program's. A finish that fails returns the cause to + /// every later call; one that succeeded does nothing when called again. + pub(super) fn finish_in_place(&mut self) -> anyhow::Result<()> { match &self.input { crate::container::Input::Open => {} crate::container::Input::Finished => return Ok(()), @@ -2071,11 +2078,39 @@ impl Import { } } +/// A TS import that [finished](Import::finish): every track has ended, and they can still be +/// read or aborted, but it takes no more input. +/// +/// Dropping it releases the tracks and their catalog renditions, as dropping the [`Import`] does. +/// +/// ```compile_fail,E0599 +/// # fn cannot_decode(mut import: moq_mux::container::ts::Finished) { +/// import.decode(b"").unwrap(); +/// # } +/// ``` +#[must_use = "dropping it releases the tracks and their catalog renditions"] +pub struct Finished { + import: Import, +} + +impl Finished { + /// What every elementary stream delivered; see [`Import::stats`]. + pub fn stats(&self) -> stats::Snapshot { + self.import.stats() + } + + /// Release every track's cache, reporting `err` to readers that have not drained it; see + /// [`Import::abort`]. + pub fn abort(self, err: moq_net::Error) { + self.import.abort(err); + } +} + impl Drop for Import { /// End the input unless it already ended. fn drop(&mut self) { if matches!(self.input, crate::container::Input::Open) - && let Err(err) = self.finish() + && let Err(err) = self.finish_in_place() { tracing::warn!(%err, "failed to finish a dropped TS import"); } @@ -2461,7 +2496,7 @@ impl SectionStream { } fn finish(&mut self) -> anyhow::Result<()> { - self.track.finish()?; + self.track.end_with(None)?; Ok(()) } @@ -2713,7 +2748,7 @@ impl PesPacketStream { } fn finish(&mut self) -> anyhow::Result<()> { - self.track.finish()?; + self.track.end_with(None)?; Ok(()) } diff --git a/rs/moq-mux/src/container/ts/mod.rs b/rs/moq-mux/src/container/ts/mod.rs index 0b5c51fe5b4..68ffe216303 100644 --- a/rs/moq-mux/src/container/ts/mod.rs +++ b/rs/moq-mux/src/container/ts/mod.rs @@ -31,8 +31,8 @@ mod follower; mod health; mod import; mod mux_rate; -mod passthrough; -mod programs; +pub mod passthrough; +pub mod programs; mod psi; mod release; mod schedule; diff --git a/rs/moq-mux/src/container/ts/passthrough.rs b/rs/moq-mux/src/container/ts/passthrough.rs index fa28aae3869..9f4b182bfbc 100644 --- a/rs/moq-mux/src/container/ts/passthrough.rs +++ b/rs/moq-mux/src/container/ts/passthrough.rs @@ -156,9 +156,6 @@ pub struct Passthrough { all_random_access: bool, mux_rate: Meter, - - /// Whether input is still accepted: [`finish`](Self::finish) ends it. - input: crate::container::Input, } /// Owns the `m2ts` catalog section once written, removing it however the track ends, as the @@ -168,6 +165,34 @@ struct Section { written: hang::catalog::M2ts, } +/// A passthrough that [finished](Passthrough::finish): its track has ended, and it can still be +/// read or aborted, but it takes no more input. +/// +/// Dropping it releases the track and removes the catalog's `m2ts` section. +/// +/// ```compile_fail,E0599 +/// # fn cannot_decode(mut passthrough: moq_mux::container::ts::passthrough::Finished) { +/// passthrough.decode(b"").unwrap(); +/// # } +/// ``` +#[must_use = "dropping it releases the track and its catalog section"] +pub struct Finished { + passthrough: Passthrough, +} + +impl Finished { + /// The name of the track carrying the packets. + pub fn track(&self) -> &str { + self.passthrough.track() + } + + /// Release the track's cache, reporting `err` to readers that have not drained it; see + /// [`Passthrough::abort`]. + pub fn abort(self, err: moq_net::Error) { + self.passthrough.abort(err); + } +} + impl Drop for Section { fn drop(&mut self) { // A closed catalog has nothing left to remove it from. @@ -282,7 +307,6 @@ impl Passthrough { groups: Groups::Waiting(None), all_random_access: true, mux_rate: Meter::default(), - input: crate::container::Input::Open, }) } @@ -305,7 +329,6 @@ impl Passthrough { /// Append `data` and publish every whole packet it completes. A trailing partial packet is /// kept for the next call. pub fn decode(&mut self, data: &[u8]) -> crate::Result<()> { - self.input.check()?; self.scratch.extend_from_slice(data); if !self.sized { self.probe_packet_size(false)?; @@ -313,31 +336,21 @@ impl Passthrough { self.packets() } - /// Publish what is left at the end of the input and close the track. + /// Publish what is left at the end of the input, close the track, and return the + /// [`Finished`] passthrough. /// /// No later PCR will arrive, so the objects since the last one are timed at the rate of the - /// interval before it. The catalog keeps the `m2ts` section until the importer drops. + /// interval before it. The catalog keeps the `m2ts` section until the [`Finished`] passthrough + /// drops. /// - /// A finish that fails ends the track with an error, and every later call returns its cause. - /// Calling it again after a success does nothing, and later input is refused with - /// [`Finished`](crate::Error::Finished). - pub fn finish(&mut self) -> crate::Result<()> { - match &self.input { - crate::container::Input::Open => {} - crate::container::Input::Finished => return Ok(()), - crate::container::Input::Refused(err) | crate::container::Input::Failed(err) => return Err(err.clone()), - } - let result = self.end(); - match &result { - Ok(()) => self.input = crate::container::Input::Finished, - Err(err) => { - // The objects already written stay readable, then subscribers get an error. - let _ = self.media.fail(err.reason()); - self.reservation = None; - self.input = crate::container::Input::Failed(err.clone()); - } + /// A finish that fails ends the track with an error, so it can't be retried. + pub fn finish(mut self) -> crate::Result> { + if let Err(err) = self.end() { + // The objects already written stay readable, then subscribers get an error. + let _ = self.media.end_with(Some(err.reason())); + return Err(err); } - result + Ok(Finished { passthrough: self }) } /// Publish the rest of the input and finish the track, stopping at the first failure. @@ -349,7 +362,7 @@ impl Passthrough { self.close_object(); self.drain_queue()?; self.reservation = None; - self.media.finish()?; + self.media.end_with(None)?; Ok(()) } diff --git a/rs/moq-mux/src/container/ts/programs.rs b/rs/moq-mux/src/container/ts/programs.rs index cd7f931af00..14fbd40f6cd 100644 --- a/rs/moq-mux/src/container/ts/programs.rs +++ b/rs/moq-mux/src/container/ts/programs.rs @@ -80,8 +80,6 @@ pub struct Programs { due: health::Due, /// The packets on PIDs no program reads. unclaimed: Unclaimed, - /// Whether input is still accepted: [`finish`](Self::finish) ends it. - input: crate::container::Input, } /// `PMT_error_2`: a PMT recurs at least every 0.5 s of program clock. @@ -326,16 +324,11 @@ impl Programs { deadline: None, due: health::Due::default(), unclaimed: Unclaimed::default(), - input: crate::container::Input::Open, } } /// Demux a chunk of the multiplex. A trailing partial packet is retained for the next call. - /// - /// Input after [`finish`](Self::finish) is refused with [`Finished`](crate::Error::Finished), - /// or with the cause of a finish that failed. pub fn decode(&mut self, data: &[u8]) -> anyhow::Result<()> { - self.input.check()?; let mut demux = std::mem::take(&mut self.demux); let result = self.decode_with(&mut demux, data); self.demux = demux; @@ -562,7 +555,7 @@ impl Programs { program = program.number, "TS program left the PAT; retiring its broadcast" ); - program.import.finish()?; + program.import.finish_in_place()?; self.announced |= program.announce_if_listed()?; program.catalog.finish()?; } @@ -880,35 +873,22 @@ impl Programs { stats } - /// Finish each program's tracks, then its catalog while it still lists them. - /// - /// Returns each running program's broadcast; a retired one has already closed. The finished - /// tracks stay servable while this importer is held, so hand both to a - /// [`Linger`](crate::Linger) to keep them up for late subscribers. Fails when the input - /// ended before its PAT listed a program or any program declared a track. + /// Finish each program's tracks, then its catalog while it still lists them, and return the + /// [`Finished`] programs. /// - /// A finish that fails aborts every program with the cause, and later calls return it again. - /// A finish that succeeded is final: calling it again returns the same broadcasts. - pub fn finish(&mut self) -> anyhow::Result> { - match &self.input { - crate::container::Input::Open => {} - crate::container::Input::Finished => return Ok(self.broadcasts()), - crate::container::Input::Refused(err) | crate::container::Input::Failed(err) => { - return Err(err.clone().into()); - } - } - match self.end() { - Ok(()) => { - self.input = crate::container::Input::Finished; - Ok(self.broadcasts()) - } - Err(err) => { - // The copy keeps the whole chain in its message, since only the first report can move - // the original. - self.fail(crate::Error::from(anyhow::anyhow!("{err:#}"))); - Err(err) - } + /// The finished tracks stay servable while the [`Finished`] programs are held, so hand them and + /// their [`broadcasts`](Finished::broadcasts) to a [`Linger`](crate::Linger) to keep them up for + /// late subscribers. Fails when the input ended before its PAT listed a program or any program + /// declared a track. A finish that fails aborts every program with the cause, so it can't be + /// retried. + pub fn finish(mut self) -> anyhow::Result { + if let Err(err) = self.end() { + // The copy keeps the whole chain in its message, since only the first report can move the + // original. + self.fail(crate::Error::from(anyhow::anyhow!("{err:#}"))); + return Err(err); } + Ok(Finished { programs: self }) } /// Finish every program, stopping at the first failure. @@ -917,7 +897,7 @@ impl Programs { anyhow::ensure!(self.pat.is_some(), "input ended before its PAT listed a program"); let mut announced = self.announced; for program in &mut self.programs { - program.import.finish()?; + program.import.finish_in_place()?; announced |= program.announce_if_listed()?; program.catalog.finish()?; } @@ -938,13 +918,44 @@ impl Programs { self.fail(err.into()); } - /// Abort every program's catalog and media tracks with `cause`, which later calls report. + /// Abort every program's catalog and media tracks with `cause`. fn fail(&mut self, cause: crate::Error) { for program in &mut self.programs { program.catalog.clone().abort(cause.reason()); program.import.fail(cause.clone()); } - self.input = crate::container::Input::Failed(cause); + } +} + +/// Every program of a multiplex [finished](Programs::finish): every track has ended, and they can +/// still be read or aborted, but it takes no more input. +/// +/// Dropping it releases every program's tracks, which leave their broadcasts' listings. +/// +/// ```compile_fail,E0599 +/// # fn cannot_decode(mut programs: moq_mux::container::ts::programs::Finished) { +/// programs.decode(b"").unwrap(); +/// # } +/// ``` +#[must_use = "dropping it releases every program's tracks"] +pub struct Finished { + programs: Programs, +} + +impl Finished { + /// Each running program's broadcast; a retired one has already closed. + pub fn broadcasts(&self) -> Vec { + self.programs.broadcasts() + } + + /// Every program's counters in one map; see [`Programs::stats`]. + pub fn stats(&self) -> stats::Snapshot { + self.programs.stats() + } + + /// Abort every program's catalog and media tracks with `err`; see [`Programs::abort`]. + pub fn abort(self, err: moq_net::Error) { + self.programs.abort(err); } } diff --git a/rs/moq-mux/src/import/container.rs b/rs/moq-mux/src/import/container.rs index 4253f41db03..00edc7dd508 100644 --- a/rs/moq-mux/src/import/container.rs +++ b/rs/moq-mux/src/import/container.rs @@ -55,13 +55,13 @@ impl ContainerImpl { } } - fn finish(&mut self) -> Result<()> { - match self { - ContainerImpl::Fmp4(decoder) => decoder.finish(), - ContainerImpl::Mkv(decoder) => decoder.finish(), - ContainerImpl::Ts(decoder) => decoder.finish().map_err(Into::into), - ContainerImpl::Flv(decoder) => decoder.finish(), - } + fn finish(self) -> Result> { + Ok(match self { + ContainerImpl::Fmp4(decoder) => FinishedImpl::Fmp4(Box::new(decoder.finish()?)), + ContainerImpl::Mkv(decoder) => FinishedImpl::Mkv(Box::new(decoder.finish()?)), + ContainerImpl::Ts(decoder) => FinishedImpl::Ts(Box::new(decoder.finish()?)), + ContainerImpl::Flv(decoder) => FinishedImpl::Flv(Box::new(decoder.finish()?)), + }) } fn abort(self, err: moq_net::Error) { @@ -118,9 +118,14 @@ impl Container { self.inner.decode(data) } - /// Finish the importer, flushing any buffered data. - pub fn finish(&mut self) -> Result<()> { - self.inner.finish() + /// Finish the importer, flushing any buffered data, and return the [`Finished`] + /// importer. + /// + /// Every track ends either way, so a failed finish can't be retried. + pub fn finish(self) -> Result> { + Ok(Finished { + inner: self.inner.finish()?, + }) } /// Abort every published track with `err`, so subscribers see the real cause @@ -144,6 +149,43 @@ impl Container { } } +/// The finished importers [`Finished`] dispatches to. +enum FinishedImpl { + // Boxed like the importers they finished. + Fmp4(Box>), + Mkv(Box>), + Ts(Box>), + Flv(Box>), +} + +/// A container importer that [finished](Container::finish): every track has ended, and they can +/// still be read or aborted, but it takes no more input. +/// +/// Dropping it retires the tracks and their catalog renditions, as dropping the [`Container`] +/// does. +/// +/// ```compile_fail,E0599 +/// # fn cannot_decode(mut container: moq_mux::import::container::Finished) { +/// container.decode(b"").unwrap(); +/// # } +/// ``` +#[must_use = "dropping it retires the tracks and their catalog renditions"] +pub struct Finished { + inner: FinishedImpl, +} + +impl Finished { + /// Release every track's cache, reporting `err` to readers that have not drained it. + pub fn abort(self, err: moq_net::Error) { + match self.inner { + FinishedImpl::Fmp4(finished) => finished.abort(err), + FinishedImpl::Mkv(finished) => finished.abort(err), + FinishedImpl::Ts(finished) => finished.abort(err), + FinishedImpl::Flv(finished) => finished.abort(err), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/rs/moq-mux/src/import/mod.rs b/rs/moq-mux/src/import/mod.rs index 866629628e7..e0986d0848b 100644 --- a/rs/moq-mux/src/import/mod.rs +++ b/rs/moq-mux/src/import/mod.rs @@ -18,12 +18,12 @@ //! `broadcast.unique_track(suffix, catalog.track_info())`, so the max age the catalog //! declares reaches the track. -mod container; +pub mod container; mod format; mod init; mod track; -pub use container::*; +pub use container::Container; pub use format::{AudioFormat, ContainerFormat, Format, Kind, VideoFormat}; pub use init::{AudioInit, ContainerInit, VideoInit}; pub use track::*; diff --git a/rs/moq-mux/src/json.rs b/rs/moq-mux/src/json.rs index 387f1d07e11..b3c8daab62b 100644 --- a/rs/moq-mux/src/json.rs +++ b/rs/moq-mux/src/json.rs @@ -229,8 +229,10 @@ impl Snapshot { /// Consumes the handle, so a write after finishing cannot be expressed. Leaving the entry /// advertising a closed track would only mislead a consumer that subscribed afterwards, so /// dropping the handle without finishing retires the entry too. - pub fn finish(mut self) -> crate::Result<()> { - Ok(self.inner.finish()?) + pub fn finish(self) -> crate::Result<()> { + // Retired along with the entry, as promised. + drop(self.inner.finish()?); + Ok(()) } /// Abort the track with `err`, which consumers see in place of a clean end, and retire its @@ -328,8 +330,10 @@ impl Stream { /// Consumes the handle, so a write after finishing cannot be expressed. Leaving the entry /// advertising a closed track would only mislead a consumer that subscribed afterwards, so /// dropping the handle without finishing retires the entry too. - pub fn finish(mut self) -> crate::Result<()> { - Ok(self.inner.finish()?) + pub fn finish(self) -> crate::Result<()> { + // Retired along with the entry, as promised. + drop(self.inner.finish()?); + Ok(()) } /// Abort the track with `err`, which consumers see in place of a clean end, and retire its From 4d0db5ecd13e3c49430b6590201c68e969ba9905 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 9 Nov 2026 15:59:11 -0800 Subject: [PATCH 03/10] chore(quest): claim quest/m2/finished-handle.md Quest-Claim: 34da3003541c7597 From d5b9c55a3d07f77e908c6d04f4a533955dcd68f1 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 9 Nov 2026 16:34:11 -0800 Subject: [PATCH 04/10] wip: moq-mux tests and JS mirrors --- js/hang/src/container/consumer.rewind.test.ts | 4 +- js/hang/src/container/consumer.test.ts | 42 +-- js/hang/src/container/legacy.ts | 31 ++- js/json/src/snapshot/index.ts | 2 +- js/json/src/snapshot/producer.ts | 13 +- js/json/src/snapshot/snapshot.test.ts | 10 + js/json/src/stream/index.ts | 2 +- js/json/src/stream/producer.ts | 13 +- js/json/src/stream/stream.test.ts | 12 + js/json/src/window/index.ts | 2 +- js/json/src/window/producer.ts | 19 +- js/json/src/window/window.test.ts | 12 + rs/moq-mux/benches/containers.rs | 18 +- rs/moq-mux/benches/export.rs | 18 +- rs/moq-mux/benches/ts_programs.rs | 4 +- rs/moq-mux/benches/ts_video.rs | 4 +- rs/moq-mux/src/catalog/producer.rs | 6 +- rs/moq-mux/src/codec/annexb/export.rs | 4 +- rs/moq-mux/src/codec/video.rs | 2 +- rs/moq-mux/src/container/consumer.rs | 2 +- rs/moq-mux/src/container/flv/export_test.rs | 62 ++--- rs/moq-mux/src/container/flv/import_test.rs | 53 ++-- rs/moq-mux/src/container/fmp4/export_test.rs | 38 +-- rs/moq-mux/src/container/fmp4/import.rs | 21 +- rs/moq-mux/src/container/fmp4/import_test.rs | 87 +++--- rs/moq-mux/src/container/fmp4/muxer.rs | 6 +- rs/moq-mux/src/container/group.rs | 8 +- rs/moq-mux/src/container/mkv/export_test.rs | 30 +-- rs/moq-mux/src/container/mkv/import.rs | 45 ++-- rs/moq-mux/src/container/mkv/import_test.rs | 47 +--- rs/moq-mux/src/container/mp4.rs | 26 +- rs/moq-mux/src/container/producer.rs | 67 +++-- rs/moq-mux/src/container/ts/export.rs | 2 +- rs/moq-mux/src/container/ts/export_test.rs | 250 +++++++++--------- .../src/container/ts/export_timing_test.rs | 2 +- rs/moq-mux/src/container/ts/health.rs | 6 +- rs/moq-mux/src/container/ts/import.rs | 187 ++++++------- rs/moq-mux/src/container/ts/import_test.rs | 22 +- .../src/container/ts/passthrough_test.rs | 16 +- .../src/container/ts/pes_packet_test.rs | 10 +- rs/moq-mux/src/container/ts/programs.rs | 52 ++-- rs/moq-mux/src/container/ts/recovery_test.rs | 2 +- rs/moq-mux/src/container/ts/release_test.rs | 4 +- rs/moq-mux/src/container/ts/revision_test.rs | 70 ++--- rs/moq-mux/src/container/ts/routes_test.rs | 2 +- rs/moq-mux/src/container/ts/step_test.rs | 2 +- rs/moq-mux/src/import/mod.rs | 4 +- 47 files changed, 682 insertions(+), 659 deletions(-) diff --git a/js/hang/src/container/consumer.rewind.test.ts b/js/hang/src/container/consumer.rewind.test.ts index dbd8b143254..403c911ad13 100644 --- a/js/hang/src/container/consumer.rewind.test.ts +++ b/js/hang/src/container/consumer.rewind.test.ts @@ -76,7 +76,7 @@ for (const start of [16_000, 0]) { expect((await frame(consumer))?.group).toBe(1n); } finally { consumer.close(); - producer.close(); + producer.finish(); } }); } @@ -132,6 +132,6 @@ test("Producer refuses a rewind before closing the current group", () => { producer.encode(new Uint8Array([1]), Duration.micros(150_000), false); expect(() => producer.encode(new Uint8Array([1]), Duration.micros(99_000), false)).toThrow("below"); } finally { - producer.close(); + producer.finish(); } }); diff --git a/js/hang/src/container/consumer.test.ts b/js/hang/src/container/consumer.test.ts index e13fe20d41d..53731c956c3 100644 --- a/js/hang/src/container/consumer.test.ts +++ b/js/hang/src/container/consumer.test.ts @@ -162,7 +162,7 @@ test("Legacy Producer refuses a group below the previous group start", () => { expect(() => producer.encode(new Uint8Array([1]), Duration.micros(16_000), true)).toThrow( "below the previous group start", ); - producer.close(); + producer.finish(); }); test("Legacy Producer accepts B-frames within a group", () => { @@ -171,7 +171,7 @@ test("Legacy Producer accepts B-frames within a group", () => { producer.encode(new Uint8Array([1]), Duration.micros(0), true); producer.encode(new Uint8Array([1]), Duration.micros(66_000), false); producer.encode(new Uint8Array([1]), Duration.micros(33_000), false); - producer.close(); + producer.finish(); }); test("Legacy Producer accepts open-GOP leading pictures above the previous group", () => { @@ -181,7 +181,7 @@ test("Legacy Producer accepts open-GOP leading pictures above the previous group producer.encode(new Uint8Array([1]), Duration.micros(33_000), false); producer.encode(new Uint8Array([1]), Duration.micros(66_000), true); producer.encode(new Uint8Array([1]), Duration.micros(50_000), false); - producer.close(); + producer.finish(); }); test("Legacy Producer writes a duration marker at the next keyframe", async () => { @@ -191,7 +191,7 @@ test("Legacy Producer writes a duration marker at the next keyframe", async () = producer.encode(new Uint8Array([0xde, 0xad]), Duration.micros(0), true); producer.encode(new Uint8Array([0xbe, 0xef]), Duration.micros(10_000), false); producer.encode(new Uint8Array([0xca, 0xfe]), Duration.micros(20_000), true); - producer.close(); + producer.finish(); const group = await subscriber.recvGroup(); expect(group).toBeDefined(); @@ -218,7 +218,7 @@ test("Legacy Producer omits a reordered group's presentation endpoint marker", a } producer.encode(new Uint8Array([1]), Duration.micros(160_000), true); producer.discontinuity(Duration.micros(200_000)); - producer.close(); + producer.finish(); const group = await subscriber.recvGroup(); expect(group).toBeDefined(); let count = 0; @@ -242,7 +242,7 @@ test("Legacy Producer estimates the tail from the current cadence", async () => for (const [index, timestamp] of [0, 16_000, 32_000, 65_000, 98_000].entries()) { producer.encode(new Uint8Array([1]), Duration.micros(timestamp), index === 0); } - producer.close(); + producer.finish(); const group = await subscriber.recvGroup(); expect(group).toBeDefined(); let end = 0; @@ -263,7 +263,7 @@ test("Legacy Producer rejects a backwards discontinuity without closing the grou expect(() => producer.discontinuity(Duration.micros(10_000))).toThrow(); producer.encode(new Uint8Array([2]), Duration.micros(30_000), false); producer.discontinuity(Duration.micros(35_000)); - producer.close(); + producer.finish(); const group = await subscriber.recvGroup(); const timestamps = []; for (;;) { @@ -281,7 +281,7 @@ test("Legacy Producer keeps the cadence after rejecting a backwards discontinuit producer.encode(new Uint8Array([1]), Duration.micros(20_000), true); producer.encode(new Uint8Array([2]), Duration.micros(30_000), false); expect(() => producer.discontinuity(Duration.micros(10_000))).toThrow(); - producer.close(); + producer.finish(); const group = await subscriber.recvGroup(); const timestamps = []; for (;;) { @@ -300,7 +300,7 @@ test("Legacy Producer refuses a keyframe that rewinds the timeline", () => { expect(() => producer.encode(new Uint8Array([3]), Duration.micros(0), true)).toThrow( "below the previous group start", ); - producer.close(); + producer.finish(); }); /** Read every group a subscriber is served until `last`, as (sequence, [timestamp, payload size][]). */ @@ -330,7 +330,7 @@ test("Legacy Producer discontinuity marks the break with one empty frame at the producer.discontinuity(); producer.discontinuity(); // nothing new to mark producer.encode(new Uint8Array([1]), Duration.micros(5_000_000), true); - producer.close(); + producer.finish(); expect(await readGroups(subscriber, 3n)).toEqual([ [0n, [[0, 1]]], @@ -346,7 +346,7 @@ test("Legacy Producer discontinuity marks the break at the caller's end", async const producer = new LegacyProducer(track, new LegacyFormat("video")); producer.encode(new Uint8Array([1]), Duration.micros(0), true); producer.discontinuity(Duration.micros(33_000)); - producer.close(); + producer.finish(); expect(await readGroups(subscriber, 1n)).toEqual([ [ @@ -370,7 +370,7 @@ test("Legacy Producer discontinuity writes no end estimated from the cadence", a producer.discontinuity(); // The capture swap resumes sooner than one frame later: no end past it, so no rewind. producer.encode(new Uint8Array([1]), Duration.micros(80_000), true); - producer.close(); + producer.finish(); expect(await readGroups(subscriber, 2n)).toEqual([ [ @@ -402,7 +402,7 @@ test("Legacy Producer discontinuity on a data track reads as an end at the edge" producer.encode(new Uint8Array(), Duration.micros(20_000), true); producer.discontinuity(); producer.encode(new Uint8Array([1]), Duration.micros(5_000_000), true); - producer.close(); + producer.finish(); const consumer = new Consumer(subscriber, { format: new LegacyFormat("data"), @@ -449,7 +449,7 @@ test("Legacy Producer discontinuity keeps pre-pause media from reading as live", producer.encode(new Uint8Array([1]), Duration.micros(33_000), false); producer.discontinuity(); producer.encode(new Uint8Array([1]), Duration.micros(5_000_000), true); - producer.close(); + producer.finish(); const subscriber = track.subscribe({ delay: { max: Duration.millis(1_000) } }); expect((await readGroups(subscriber, 2n)).map(([sequence]) => sequence)).toEqual([1n, 2n]); @@ -1168,7 +1168,7 @@ test("Consumer preserves empty Legacy and LOC data frames", async () => { if (kind === "legacy") { const producer = new LegacyProducer(track, new LegacyFormat("data")); producer.encode(new Uint8Array(), Duration.micros(0), true); - producer.close(); + producer.finish(); } else { writeLoc(track, [[[0, []]]]); } @@ -2113,7 +2113,7 @@ test("audio discontinuity writes no duration marker", async () => { const group = await subscriber.recvGroup(); expect(await group?.readFrame()).toBeDefined(); expect(await group?.readFrame()).toBeUndefined(); - producer.close(); + producer.finish(); }); for (const code of [StreamCode.Cancel, StreamCode.Internal, StreamCode.Old, StreamCode.Evicted, StreamCode(1234)]) { @@ -2254,3 +2254,13 @@ test("LegacyFormat rejects a timestamp past 2^53 - 1 instead of rounding", () => const frame = Varint.encode(2n ** 53n + 1n); expect(() => new LegacyFormat("video").decode(frame)).toThrow(/larger than 53-bits/); }); + +// Mirrors the Rust `container::Finished` doctest: the track `finish` returns takes no frame, at +// compile time and at run time. +test("finishing a Legacy Producer returns a track that takes no frame", () => { + const producer = new LegacyProducer(new Track.Producer("test"), new LegacyFormat("video")); + producer.encode(new Uint8Array([1]), Duration.micros(0), true); + const finished = producer.finish(); + // @ts-expect-error a finished track takes no frame + expect(() => finished.encode(new Uint8Array([1]), Duration.micros(33_000), true)).toThrow(); +}); diff --git a/js/hang/src/container/legacy.ts b/js/hang/src/container/legacy.ts index 0853c53bc65..632e46d0c1d 100644 --- a/js/hang/src/container/legacy.ts +++ b/js/hang/src/container/legacy.ts @@ -68,6 +68,14 @@ function micros(timestamp: Duration): number { return Math.floor(timestamp.asMicros()); } +/** + * A track its {@link Producer} finished: every frame is written and it takes no more. + * {@link Producer.finish} returns it, mirroring the Rust `container::Finished`; the producer itself + * still refuses a frame after the finish. + */ +// biome-ignore lint/suspicious/noEmptyInterface: nothing is left to call once the track is released. +export interface Finished {} + /** Writes legacy-container frames into a MoQ track, starting a new group on each keyframe. */ export class Producer { #track: Moq.Track.Producer; @@ -190,15 +198,18 @@ export class Producer { this.#reordered = false; } - /** Close the track and current group, optionally with an error. */ - close(err?: Error) { - if (!err) this.#close(); - if (err) this.#group?.abort(err); - else this.#group?.finish(); - if (err) this.#track.abort(err); - else { - this.#track.finish(); - this.#track.close(); - } + /** Close the current group and finish the track, returning it as {@link Finished}. */ + finish(): Finished { + this.#close(); + this.#track.finish(); + this.#track.close(); + return this; + } + + /** Abort the current group and the track with `err`, which readers see in place of a clean end. */ + abort(err: Error) { + this.#group?.abort(err); + this.#group = undefined; + this.#track.abort(err); } } diff --git a/js/json/src/snapshot/index.ts b/js/json/src/snapshot/index.ts index 311ecbfdd2d..7b81e4b889c 100644 --- a/js/json/src/snapshot/index.ts +++ b/js/json/src/snapshot/index.ts @@ -40,4 +40,4 @@ export { MissingSnapshot } from "../error.ts"; export { Consumer } from "./consumer.ts"; export { Decoder } from "./decoder.ts"; export { type Config, type Encoded, Encoder, type Pending } from "./encoder.ts"; -export { Producer } from "./producer.ts"; +export { type Finished, Producer } from "./producer.ts"; diff --git a/js/json/src/snapshot/producer.ts b/js/json/src/snapshot/producer.ts index 671bbd09b49..ed5d45be3db 100644 --- a/js/json/src/snapshot/producer.ts +++ b/js/json/src/snapshot/producer.ts @@ -4,6 +4,14 @@ import { MAX_FRAME_BYTES } from "../limits.ts"; import { type Config as CodecConfig, type Encoded, Encoder } from "./encoder.ts"; +/** + * A value its producer finished: it takes no more updates. {@link Producer.finish} returns it, + * mirroring the Rust `snapshot::Finished`; the producer itself still refuses an update after the + * finish. + */ +// biome-ignore lint/suspicious/noEmptyInterface: nothing is left to call once the track is released. +export interface Finished {} + /** * Publishes a JSON value over a track, choosing snapshots and deltas automatically. * @@ -182,9 +190,10 @@ export class Producer { this.update({ value, at }); } - /** Finish the track, closing any open group. */ - finish(): void { + /** Finish the track, closing any open group, and return it as {@link Finished}. */ + finish(): Finished { this.#close(); + return this; } #close(abort?: Error): void { diff --git a/js/json/src/snapshot/snapshot.test.ts b/js/json/src/snapshot/snapshot.test.ts index 76142215667..39702a4c44b 100644 --- a/js/json/src/snapshot/snapshot.test.ts +++ b/js/json/src/snapshot/snapshot.test.ts @@ -739,3 +739,13 @@ test("each state reads back with its frame's timestamp", async () => { expect(head?.value.n).toBe(3); expect(head?.at?.as(Time.Timescale.MILLI)).toBe(3_000); }); + +// Mirrors the Rust `snapshot::Finished` doctest: the value `finish` returns takes no update, at +// compile time and at run time. +test("finishing returns a value that publishes nothing", () => { + const producer = new Producer({ track: new Track.Producer("test") }); + producer.update({ value: { a: 1 } }); + const finished = producer.finish(); + // @ts-expect-error a finished value takes no update + expect(() => finished.update({ value: { a: 2 } })).toThrow(); +}); diff --git a/js/json/src/stream/index.ts b/js/json/src/stream/index.ts index 4915c0bbb68..146f6f19a0f 100644 --- a/js/json/src/stream/index.ts +++ b/js/json/src/stream/index.ts @@ -23,4 +23,4 @@ export { Consumer, Rolled } from "./consumer.ts"; export { Decoder } from "./decoder.ts"; export { type Config, Encoder, type Pending } from "./encoder.ts"; -export { Producer } from "./producer.ts"; +export { type Finished, Producer } from "./producer.ts"; diff --git a/js/json/src/stream/producer.ts b/js/json/src/stream/producer.ts index 5987826425a..c74a4413b70 100644 --- a/js/json/src/stream/producer.ts +++ b/js/json/src/stream/producer.ts @@ -3,6 +3,14 @@ import { Error as NetError, type Timed } from "@moq/net"; import { type Config as CodecConfig, Encoder, type Pending } from "./encoder.ts"; +/** + * A log its producer finished: it takes no more records. {@link Producer.finish} returns it, + * mirroring the Rust `stream::Finished`; the producer itself still refuses an append after the + * finish. + */ +// biome-ignore lint/suspicious/noEmptyInterface: nothing is left to call once the track is released. +export interface Finished {} + /** * Publishes an ordered log of JSON records to a track, one record per frame in a single group. * @@ -76,12 +84,13 @@ export class Producer { this.#track.abort(abort); } - /** Finish the track, closing the group. */ - finish(): void { + /** Finish the track, closing the group, and return it as {@link Finished}. */ + finish(): Finished { this.#group?.finish(); this.#group = undefined; this.#track.finish(); this.#track.close(); + return this; } } diff --git a/js/json/src/stream/stream.test.ts b/js/json/src/stream/stream.test.ts index 70f05ef37b8..a80cb0e2cec 100644 --- a/js/json/src/stream/stream.test.ts +++ b/js/json/src/stream/stream.test.ts @@ -250,3 +250,15 @@ for (const compress of [false, true]) { expect(await fast.next()).toBeUndefined(); }); } + +// Mirrors the Rust `stream::Finished` doctest: the log `finish` returns takes no record, at +// compile time and at run time. +test("finishing returns a log that appends nothing", async () => { + const track = new Track.Producer("test"); + const producer = new Producer({ track }); + producer.append({ value: { n: 0 } }); + const finished = producer.finish(); + // @ts-expect-error a finished log appends no record + expect(() => finished.append({ value: { n: 1 } })).toThrow(); + expect(await drain(track.subscribe())).toEqual([0]); +}); diff --git a/js/json/src/window/index.ts b/js/json/src/window/index.ts index 3dce0f6f663..05c267a7982 100644 --- a/js/json/src/window/index.ts +++ b/js/json/src/window/index.ts @@ -39,4 +39,4 @@ export { Consumer, type ConsumerConfig } from "./consumer.ts"; export { Decoder, type Event, type Group, type Span } from "./decoder.ts"; export { type Config, type Encoded, Encoder, type Pending } from "./encoder.ts"; -export { Producer, type ProducerConfig } from "./producer.ts"; +export { type Finished, Producer, type ProducerConfig } from "./producer.ts"; diff --git a/js/json/src/window/producer.ts b/js/json/src/window/producer.ts index f0630fd701b..6ac771d93c5 100644 --- a/js/json/src/window/producer.ts +++ b/js/json/src/window/producer.ts @@ -6,6 +6,20 @@ import { type Config, type Encoded, Encoder } from "./encoder.ts"; /** Window producer options, including the destination track. */ export type ProducerConfig = Config & { track: Moq.Track.Producer }; +/** + * A window its producer finished: its records can still be read, but not edited. + * {@link Producer.finish} returns it, mirroring the Rust `window::Finished`; the producer itself + * still refuses an edit after the finish. + * + * @public + */ +export interface Finished { + /** The retained checkpoint suffix, oldest first; see {@link Producer.window}. */ + readonly window: T[]; + /** Absolute index of the oldest retained record; see {@link Producer.offset}. */ + readonly offset: number; +} + /** * Publishes a sliding window of JSON records to a track. * @@ -156,9 +170,10 @@ export class Producer { group.finish(); } - /** Finish the track, closing any open group. */ - finish(): void { + /** Finish the track, closing any open group, and return it as {@link Finished}. */ + finish(): Finished { this.#close(); + return this; } /** Abort the track with `err`, so readers see the cause rather than a complete window. Idempotent. */ diff --git a/js/json/src/window/window.test.ts b/js/json/src/window/window.test.ts index 10cc0bfdca8..c91204ca030 100644 --- a/js/json/src/window/window.test.ts +++ b/js/json/src/window/window.test.ts @@ -614,3 +614,15 @@ test("events on an untimed track have no timestamp", async () => { expect(events.length).toBe(2); expect(events.every((event) => event.at === undefined)).toBeTrue(); }); + +// Mirrors the Rust `window::Finished` doctest: the window `finish` returns still reads, but takes +// no edit, at compile time and at run time. +test("finishing returns a window that reads but takes no edit", () => { + const producer = new Producer({ track: new Track.Producer("test") }); + producer.push({ value: { n: 1 } }); + const finished = producer.finish(); + expect(finished.window).toEqual([{ n: 1 }]); + expect(finished.offset).toBe(0); + // @ts-expect-error a finished window takes no record + expect(() => finished.push({ value: { n: 2 } })).toThrow("track is closed"); +}); diff --git a/rs/moq-mux/benches/containers.rs b/rs/moq-mux/benches/containers.rs index 17bcf06d0a1..5360ad0e34f 100644 --- a/rs/moq-mux/benches/containers.rs +++ b/rs/moq-mux/benches/containers.rs @@ -81,8 +81,7 @@ async fn prepare(video: Video, payload: &Bytes) -> (moq_mux::Source, impl Stream }) .unwrap(); } - publisher.finish().unwrap(); - publishers.push(publisher); + publishers.push(publisher.finish().unwrap()); } catalog.finish().unwrap(); broadcast.announce().unwrap(); @@ -136,7 +135,7 @@ fn cmaf_import(passthrough: bool) -> (moq_mux::catalog::Producer, moq_mux::conta fn import_cmaf( import: (moq_mux::catalog::Producer, moq_mux::container::fmp4::Import), data: &Bytes, -) -> (moq_mux::catalog::Producer, moq_mux::container::fmp4::Import) { +) -> (moq_mux::catalog::Producer, moq_mux::container::fmp4::Finished) { import_cmaf_chunks(import, data, data.len()) } @@ -144,12 +143,11 @@ fn import_cmaf_chunks( (catalog, mut import): (moq_mux::catalog::Producer, moq_mux::container::fmp4::Import), data: &Bytes, chunk: usize, -) -> (moq_mux::catalog::Producer, moq_mux::container::fmp4::Import) { +) -> (moq_mux::catalog::Producer, moq_mux::container::fmp4::Finished) { for chunk in data.chunks(chunk) { import.decode(chunk).unwrap(); } - import.finish().unwrap(); - (catalog, import) + (catalog, import.finish().unwrap()) } struct Annexb { @@ -403,7 +401,7 @@ fn containers(c: &mut Criterion) { for passthrough in [false, true] { let (catalog, mut import) = cmaf_import(passthrough); import.decode(&data).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); assert_eq!(catalog.snapshot().video.renditions.len(), *tracks); } use mp4_atom::DecodeMaybe; @@ -759,8 +757,7 @@ fn mkv_chunks(c: &mut Criterion) { for bytes in std::hint::black_box(data).chunks(chunk) { import.decode(bytes).unwrap(); } - import.finish().unwrap(); - (catalog, import) + (catalog, import.finish().unwrap()) }, BatchSize::PerIteration, ); @@ -832,8 +829,7 @@ fn mkv_payloads(c: &mut Criterion) { for bytes in std::hint::black_box(&data).chunks(chunk) { import.decode(bytes).unwrap(); } - import.finish().unwrap(); - (catalog, import) + (catalog, import.finish().unwrap()) }, BatchSize::PerIteration, ); diff --git a/rs/moq-mux/benches/export.rs b/rs/moq-mux/benches/export.rs index 7e62a0c48a6..1234d49f94b 100644 --- a/rs/moq-mux/benches/export.rs +++ b/rs/moq-mux/benches/export.rs @@ -60,18 +60,20 @@ async fn prepare(tracks: usize, frames: usize, live: bool) -> (moq_mux::Source, }) .unwrap(); } - if !live { - track.finish().unwrap(); - } - } - if !live { - catalog.finish().unwrap(); } + let tracks: Box = match live { + true => Box::new(publishers), + false => { + let finished: Vec<_> = publishers.into_iter().map(|track| track.finish().unwrap()).collect(); + catalog.finish().unwrap(); + Box::new(finished) + } + }; broadcast.announce().unwrap(); let resolved = origin.consume().request("bench", None).await.unwrap(); let source = moq_mux::Source::new(origin.consume(), resolved); let fixture = Fixture { - _published: Box::new((broadcast, catalog, publishers)), + _published: Box::new((broadcast, catalog, tracks)), _origin: origin, driver, }; @@ -289,7 +291,7 @@ async fn prepare_video( }) .unwrap(); } - video.finish().unwrap(); + let video = video.finish().unwrap(); catalog.finish().unwrap(); broadcast.announce().unwrap(); let resolved = origin.consume().request("bench", None).await.unwrap(); diff --git a/rs/moq-mux/benches/ts_programs.rs b/rs/moq-mux/benches/ts_programs.rs index 2ed749dee6e..036681231af 100644 --- a/rs/moq-mux/benches/ts_programs.rs +++ b/rs/moq-mux/benches/ts_programs.rs @@ -228,8 +228,8 @@ fn ingest(programs: u16, input: &[u8]) -> (moq_mux::container::ts::stats::Snapsh let (mut importer, driver) = setup(); importer.decode(input).unwrap(); let finished = importer.finish().unwrap(); - assert_eq!(finished.len(), usize::from(programs)); - (importer.stats(), (driver, finished)) + assert_eq!(finished.broadcasts().len(), usize::from(programs)); + (finished.stats(), (driver, finished)) } fn programs(c: &mut Criterion) { diff --git a/rs/moq-mux/benches/ts_video.rs b/rs/moq-mux/benches/ts_video.rs index 72b89c78fd0..c4931c9c002 100644 --- a/rs/moq-mux/benches/ts_video.rs +++ b/rs/moq-mux/benches/ts_video.rs @@ -79,8 +79,8 @@ fn assembly(c: &mut Criterion) { }, |(mut import, _catalog)| { import.decode(&data).unwrap(); - import.finish().unwrap(); - std::hint::black_box(import); + let import = import.finish().unwrap(); + let _finished = std::hint::black_box(import); }, BatchSize::SmallInput, ) diff --git a/rs/moq-mux/src/catalog/producer.rs b/rs/moq-mux/src/catalog/producer.rs index 316e7e28dcd..089af632fc6 100644 --- a/rs/moq-mux/src/catalog/producer.rs +++ b/rs/moq-mux/src/catalog/producer.rs @@ -2116,9 +2116,9 @@ mod test { fn a_short_track_declares_its_interval_when_it_finishes() { let mut broadcast = moq_net::broadcast::Info::new().produce(); let catalog = Producer::new(&mut broadcast, Config::default()).unwrap(); - let mut video = video0(&mut broadcast, &catalog, &[0, 500]); + let video = video0(&mut broadcast, &catalog, &[0, 500]); assert_eq!(catalog.snapshot().media.join, None, "still measuring"); - video.finish().unwrap(); + let _finished = video.finish().unwrap(); assert_eq!( catalog.snapshot().media.interval(), Some(std::time::Duration::from_millis(500)) @@ -2303,7 +2303,7 @@ mod test { } } away.block_on(async { - video.finish().unwrap(); + let _finished = video.finish().unwrap(); catalog.finish().unwrap(); }); diff --git a/rs/moq-mux/src/codec/annexb/export.rs b/rs/moq-mux/src/codec/annexb/export.rs index 48775d5fe56..3237d5b2574 100644 --- a/rs/moq-mux/src/codec/annexb/export.rs +++ b/rs/moq-mux/src/codec/annexb/export.rs @@ -218,14 +218,14 @@ mod tests { moving.a.write(marked_frame(0, 0xa1)).unwrap(); assert_eq!(marks(&next(&mut export).await), [0xa1]); if finished { - moving.a.finish().unwrap(); + moving.a.end_with(None).unwrap(); assert!(export.poll_next(&kio::Waiter::noop()).is_pending()); } moving.b.write(marked_frame(30, 0xb1)).unwrap(); moving.b.write(marked_frame(60, 0xb2)).unwrap(); moving.point("b", None, SPS); assert_eq!(marks(&next(&mut export).await), [0xb2]); - moving.b.finish().unwrap(); + moving.b.end_with(None).unwrap(); moving.catalog.finish().unwrap(); assert!(export.next().await.unwrap().is_none()); } diff --git a/rs/moq-mux/src/codec/video.rs b/rs/moq-mux/src/codec/video.rs index 28e5a15cd6a..d73b8ba9c3c 100644 --- a/rs/moq-mux/src/codec/video.rs +++ b/rs/moq-mux/src/codec/video.rs @@ -373,6 +373,6 @@ mod tests { publishing.publish(&mut track, config).unwrap(); assert_ne!(track.name(), "video"); assert_ne!(track.name(), second); - track.finish().unwrap(); + let _finished = track.finish().unwrap(); } } diff --git a/rs/moq-mux/src/container/consumer.rs b/rs/moq-mux/src/container/consumer.rs index 87bbb1e2eb4..e69c749c1c7 100644 --- a/rs/moq-mux/src/container/consumer.rs +++ b/rs/moq-mux/src/container/consumer.rs @@ -1798,7 +1798,7 @@ mod tests { producer.write(frame(10_000, b"")).unwrap(); producer.discontinuity().unwrap(); producer.write(frame(2_405_070_000, b"b")).unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); assert_eq!(items(&mut reader).await, ["0", "10000", "end 10000", "2405070000"]); } diff --git a/rs/moq-mux/src/container/flv/export_test.rs b/rs/moq-mux/src/container/flv/export_test.rs index 69f964e6593..3c1e8bcca87 100644 --- a/rs/moq-mux/src/container/flv/export_test.rs +++ b/rs/moq-mux/src/container/flv/export_test.rs @@ -130,7 +130,7 @@ fn synth_flv() -> Vec { } /// Drive the exporter to completion, dropping an already-finished importer to signal EOS. -async fn drain_export(mut exporter: Export, importer: Import) -> Vec { +async fn drain_export(mut exporter: Export, importer: super::Finished) -> Vec { let mut exported = Vec::new(); let mut importer = Some(importer); for _ in 0..64 { @@ -159,7 +159,7 @@ async fn export_roundtrips_through_import() { let mut importer = Import::new(producer, catalog.reserve()); importer.decode(&bytes::BytesMut::from(synth_flv().as_slice())).unwrap(); - importer.finish().unwrap(); + let importer = importer.finish().unwrap(); catalog.finish().unwrap(); let exporter = open_export(crate::source::announced(&consumer).await).await; @@ -173,7 +173,7 @@ async fn export_roundtrips_through_import() { let cat2 = crate::catalog::Producer::new(&mut bcast2, crate::catalog::Config::default()).unwrap(); let mut imp2 = Import::new(bcast2, cat2.reserve()); imp2.decode(&bytes::BytesMut::from(exported.as_slice())).unwrap(); - imp2.finish().unwrap(); + let _finished = imp2.finish().unwrap(); let snap = cat2.snapshot(); assert_eq!(snap.video.renditions.len(), 1); @@ -197,7 +197,7 @@ async fn export_emits_sequence_headers_and_frames() { let mut importer = Import::new(producer, catalog.reserve()); importer.decode(&bytes::BytesMut::from(synth_flv().as_slice())).unwrap(); - importer.finish().unwrap(); + let importer = importer.finish().unwrap(); catalog.finish().unwrap(); let exporter = open_export(crate::source::announced(&consumer).await) @@ -284,7 +284,7 @@ async fn export_roundtrips_enhanced() { importer .decode(&bytes::BytesMut::from(synth_enhanced_flv().as_slice())) .unwrap(); - importer.finish().unwrap(); + let importer = importer.finish().unwrap(); catalog.finish().unwrap(); let exporter = open_export(crate::source::announced(&consumer).await).await; @@ -311,7 +311,7 @@ async fn export_roundtrips_enhanced() { let cat2 = crate::catalog::Producer::new(&mut bcast2, crate::catalog::Config::default()).unwrap(); let mut imp2 = Import::new(bcast2, cat2.reserve()); imp2.decode(&bytes::BytesMut::from(exported.as_slice())).unwrap(); - imp2.finish().unwrap(); + let _finished = imp2.finish().unwrap(); let snap = cat2.snapshot(); assert!(matches!( @@ -348,7 +348,7 @@ async fn export_roundtrips_mp3() { let mut importer = Import::new(producer, catalog.reserve()); importer.decode(&bytes::BytesMut::from(flv.as_slice())).unwrap(); - importer.finish().unwrap(); + let importer = importer.finish().unwrap(); catalog.finish().unwrap(); let exporter = open_export(crate::source::announced(&consumer).await).await; @@ -367,7 +367,7 @@ async fn export_roundtrips_mp3() { let cat2 = crate::catalog::Producer::new(&mut bcast2, crate::catalog::Config::default()).unwrap(); let mut imp2 = Import::new(bcast2, cat2.reserve()); imp2.decode(&bytes::BytesMut::from(exported.as_slice())).unwrap(); - imp2.finish().unwrap(); + let _finished = imp2.finish().unwrap(); let snap = cat2.snapshot(); let a = snap.audio.renditions.values().next().unwrap(); @@ -402,7 +402,7 @@ async fn export_roundtrips_av1() { importer .decode(&bytes::BytesMut::from(synth_av1_flv().as_slice())) .unwrap(); - importer.finish().unwrap(); + let importer = importer.finish().unwrap(); catalog.finish().unwrap(); let exporter = open_export(crate::source::announced(&consumer).await).await; @@ -428,7 +428,7 @@ async fn export_roundtrips_av1() { let cat2 = crate::catalog::Producer::new(&mut bcast2, crate::catalog::Config::default()).unwrap(); let mut imp2 = Import::new(bcast2, cat2.reserve()); imp2.decode(&bytes::BytesMut::from(exported.as_slice())).unwrap(); - imp2.finish().unwrap(); + let _finished = imp2.finish().unwrap(); let snap = cat2.snapshot(); assert!(matches!( @@ -458,7 +458,7 @@ async fn export_roundtrips_ac3() { synth_enhanced_audio_flv(b"ac-3", &AC3_FRAME).as_slice(), )) .unwrap(); - importer.finish().unwrap(); + let importer = importer.finish().unwrap(); catalog.finish().unwrap(); let exporter = open_export(crate::source::announced(&consumer).await).await; @@ -474,7 +474,7 @@ async fn export_roundtrips_ac3() { let cat2 = crate::catalog::Producer::new(&mut bcast2, crate::catalog::Config::default()).unwrap(); let mut imp2 = Import::new(bcast2, cat2.reserve()); imp2.decode(&bytes::BytesMut::from(exported.as_slice())).unwrap(); - imp2.finish().unwrap(); + let _finished = imp2.finish().unwrap(); assert!(matches!( cat2.snapshot().audio.renditions.values().next().unwrap().codec, AudioCodec::Ac3 @@ -493,7 +493,7 @@ async fn export_roundtrips_eac3() { synth_enhanced_audio_flv(b"ec-3", &EAC3_FRAME).as_slice(), )) .unwrap(); - importer.finish().unwrap(); + let importer = importer.finish().unwrap(); catalog.finish().unwrap(); let exporter = open_export(crate::source::announced(&consumer).await).await; @@ -509,7 +509,7 @@ async fn export_roundtrips_eac3() { let cat2 = crate::catalog::Producer::new(&mut bcast2, crate::catalog::Config::default()).unwrap(); let mut imp2 = Import::new(bcast2, cat2.reserve()); imp2.decode(&bytes::BytesMut::from(exported.as_slice())).unwrap(); - imp2.finish().unwrap(); + let _finished = imp2.finish().unwrap(); assert!(matches!( cat2.snapshot().audio.renditions.values().next().unwrap().codec, AudioCodec::Ec3 @@ -531,7 +531,7 @@ fn avcc_level(level: u8) -> Vec { type Keepalive = ( moq_net::broadcast::Producer, crate::catalog::Producer, - Vec>, + Vec>, ); /// Build a broadcast with two H.264 video renditions plus one AAC audio rendition, @@ -584,7 +584,7 @@ fn build_multitrack_broadcast() -> (moq_net::broadcast::Consumer, Vec>, keyframe: true, }) .unwrap(); - video.finish().unwrap(); + let video = video.finish().unwrap(); tracks.push(video); } @@ -610,7 +610,7 @@ fn build_multitrack_broadcast() -> (moq_net::broadcast::Consumer, Vec>, keyframe: true, }) .unwrap(); - audio.finish().unwrap(); + let audio = audio.finish().unwrap(); tracks.push(audio); catalog.finish().unwrap(); @@ -672,7 +672,7 @@ async fn export_multitrack_roundtrips_all_renditions() { let cat2 = crate::catalog::Producer::new(&mut bcast2, crate::catalog::Config::default()).unwrap(); let mut imp2 = Import::new(bcast2, cat2.reserve()); imp2.decode(&bytes::BytesMut::from(exported.as_slice())).unwrap(); - imp2.finish().unwrap(); + let _finished = imp2.finish().unwrap(); let snap = cat2.snapshot(); assert_eq!( @@ -716,7 +716,7 @@ async fn export_without_multitrack_keeps_best_rendition() { let cat2 = crate::catalog::Producer::new(&mut bcast2, crate::catalog::Config::default()).unwrap(); let mut imp2 = Import::new(bcast2, cat2.reserve()); imp2.decode(&bytes::BytesMut::from(exported.as_slice())).unwrap(); - imp2.finish().unwrap(); + let _finished = imp2.finish().unwrap(); let snap = cat2.snapshot(); let renditions: Vec<_> = snap.video.renditions.values().collect(); @@ -770,7 +770,7 @@ async fn export_without_multitrack_keeps_best_audio_rendition() { keyframe: true, }) .unwrap(); - audio.finish().unwrap(); + let audio = audio.finish().unwrap(); tracks.push(audio); }; @@ -801,7 +801,7 @@ async fn export_without_multitrack_keeps_best_audio_rendition() { let cat2 = crate::catalog::Producer::new(&mut bcast2, crate::catalog::Config::default()).unwrap(); let mut imp2 = Import::new(bcast2, cat2.reserve()); imp2.decode(&bytes::BytesMut::from(exported.as_slice())).unwrap(); - imp2.finish().unwrap(); + let _finished = imp2.finish().unwrap(); let snap = cat2.snapshot(); assert_eq!( @@ -857,7 +857,7 @@ async fn export_picks_the_best_selected_rendition() { let cat2 = crate::catalog::Producer::new(&mut bcast2, crate::catalog::Config::default()).unwrap(); let mut imp2 = Import::new(bcast2, cat2.reserve()); imp2.decode(&bytes::BytesMut::from(exported.as_slice())).unwrap(); - imp2.finish().unwrap(); + let _finished = imp2.finish().unwrap(); let snap = cat2.snapshot(); let renditions: Vec<_> = snap.video.renditions.values().collect(); @@ -1235,7 +1235,7 @@ async fn export_preserves_timestamps() { let mut importer = Import::new(producer, catalog.reserve()); importer.decode(&bytes::BytesMut::from(synth_flv().as_slice())).unwrap(); - importer.finish().unwrap(); + let importer = importer.finish().unwrap(); catalog.finish().unwrap(); let exporter = open_export(crate::source::announced(&consumer).await).await; @@ -1302,7 +1302,7 @@ async fn export_authors_dts_and_composition_time_for_reordered_avc() { video.write(video_frame(80, &[0, 0, 0, 1, 0x41], false)).unwrap(); video.write(video_frame(40, &[0, 0, 0, 1, 0x01], false)).unwrap(); video.write(video_frame(120, &[0, 0, 0, 1, 0x41], false)).unwrap(); - video.finish().unwrap(); + let _finished = video.finish().unwrap(); let mut audio = crate::container::Producer::new( audio_track, @@ -1316,7 +1316,7 @@ async fn export_authors_dts_and_composition_time_for_reordered_avc() { keyframe: true, }) .unwrap(); - audio.finish().unwrap(); + let _finished = audio.finish().unwrap(); catalog.finish().unwrap(); let exporter = open_export(crate::source::announced(&consumer).await).await; @@ -1407,14 +1407,14 @@ async fn export_declared(reorder: u8, groups: &[&[u64]], open: bool) -> anyhow:: } match open { true => video.cut(None).unwrap(), - false => video.finish().unwrap(), + false => video.end_with(None).unwrap(), } let mut audio = crate::container::Producer::new( audio_track, crate::catalog::hang::Container::Legacy(crate::container::Kind::Data), ); audio.write(frame(20, &[0xde, 0xad], true)).unwrap(); - audio.finish().unwrap(); + let _finished = audio.finish().unwrap(); catalog.finish().unwrap(); let mut exporter = open_export(crate::source::announced(&consumer).await) @@ -1546,7 +1546,7 @@ async fn a_reorder_declared_after_the_header_raises_the_shift() { for (ms, keyframe) in [(1, true), (5, false), (3, false)] { video.write(frame(ms, keyframe)).unwrap(); } - video.finish().unwrap(); + let _finished = video.finish().unwrap(); catalog.finish().unwrap(); drain_until_idle(&mut exporter, &mut exported).await.unwrap().unwrap(); @@ -1700,9 +1700,9 @@ async fn a_track_leaving_the_catalog_is_read_to_its_end() { // The audio rendition retires before its tail lands; the video carries on. catalog.modify().unwrap().audio.renditions.remove(&audio_name); audio.write(frame(23, &[0xbe, 0xef], true)).unwrap(); - audio.finish().unwrap(); + let _finished = audio.finish().unwrap(); video.write(frame(33, &[0, 0, 0, 1, 0x41], false)).unwrap(); - video.finish().unwrap(); + let _finished = video.finish().unwrap(); catalog.finish().unwrap(); let end = drain_until_idle(&mut exporter, &mut exported).await; @@ -1778,7 +1778,7 @@ async fn delay_drops_late_frames_and_drains_the_final_frame() { live.track.write(raw_frame(20_000, b"expired", true)).unwrap(); // Sent a delay ahead: any further would be a step in its timeline, which re-acquires. live.track.write(raw_frame(300_000, b"final", true)).unwrap(); - live.track.finish().unwrap(); + live.track.end_with(None).unwrap(); live.catalog.finish().unwrap(); let final_frame = export.next().await.unwrap().unwrap(); assert!(final_frame.windows(5).any(|part| part == b"final")); diff --git a/rs/moq-mux/src/container/flv/import_test.rs b/rs/moq-mux/src/container/flv/import_test.rs index ac5fd2b25f2..50d7e053c30 100644 --- a/rs/moq-mux/src/container/flv/import_test.rs +++ b/rs/moq-mux/src/container/flv/import_test.rs @@ -95,7 +95,7 @@ async fn import_populates_catalog() { let mut importer = Import::new(producer, catalog.reserve()); let buf = bytes::BytesMut::from(synth_flv().as_slice()); importer.decode(&buf).unwrap(); - importer.finish().unwrap(); + let _finished = importer.finish().unwrap(); let snap = catalog.snapshot(); assert_eq!(snap.video.renditions.len(), 1); @@ -121,7 +121,7 @@ async fn import_emits_frames() { let mut importer = Import::new(producer, catalog.reserve()); let buf = bytes::BytesMut::from(synth_flv().as_slice()); importer.decode(&buf).unwrap(); - importer.finish().unwrap(); + let _finished = importer.finish().unwrap(); let snap = catalog.snapshot(); let video_name = snap.video.renditions.keys().next().unwrap().clone(); @@ -155,7 +155,7 @@ async fn public_container_preserves_loc_for_flv() { let reserved = catalog.reserve(); let mut import = super::Import::new(broadcast, reserved).with_container(hang::catalog::Container::Loc); import.decode(&data).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let snapshot = catalog.snapshot(); let (name, config) = snapshot.video.renditions.iter().next().unwrap(); @@ -186,7 +186,7 @@ async fn import_handles_split_input() { let mut importer = Import::new(producer, catalog.reserve()); importer.decode(&bytes::BytesMut::from(head)).unwrap(); importer.decode(&bytes::BytesMut::from(tail)).unwrap(); - importer.finish().unwrap(); + let _finished = importer.finish().unwrap(); let snap = catalog.snapshot(); assert_eq!(snap.video.renditions.len(), 1); @@ -218,7 +218,7 @@ async fn import_enhanced_vp9() { let catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); let mut importer = Import::new(producer, catalog.reserve()); importer.decode(&bytes::BytesMut::from(out.as_slice())).unwrap(); - importer.finish().unwrap(); + let _finished = importer.finish().unwrap(); let snap = catalog.snapshot(); assert_eq!(snap.video.renditions.len(), 1); @@ -258,7 +258,7 @@ async fn import_enhanced_opus() { let catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); let mut importer = Import::new(producer, catalog.reserve()); importer.decode(&bytes::BytesMut::from(out.as_slice())).unwrap(); - importer.finish().unwrap(); + let _finished = importer.finish().unwrap(); let snap = catalog.snapshot(); assert_eq!(snap.audio.renditions.len(), 1); @@ -293,7 +293,7 @@ async fn import_legacy_mp3() { let catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); let mut importer = Import::new(producer, catalog.reserve()); importer.decode(&bytes::BytesMut::from(out.as_slice())).unwrap(); - importer.finish().unwrap(); + let _finished = importer.finish().unwrap(); let snap = catalog.snapshot(); assert_eq!(snap.audio.renditions.len(), 1); @@ -343,7 +343,7 @@ async fn legacy_mp3_channel_change_starts_a_new_track() { let (name, a) = snap.audio.renditions.iter().next().unwrap(); assert_ne!(name, &first, "the mono frames publish under a new name"); assert_eq!(a.channel_count, 1); - importer.finish().unwrap(); + let _finished = importer.finish().unwrap(); } /// A minimal avcC like [`avcc`], but with a caller-chosen level byte so two video @@ -423,7 +423,7 @@ async fn import_multitrack_video_many_tracks() { let catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); let mut importer = Import::new(producer, catalog.reserve()); importer.decode(&bytes::BytesMut::from(out.as_slice())).unwrap(); - importer.finish().unwrap(); + let _finished = importer.finish().unwrap(); let snap = catalog.snapshot(); assert_eq!(snap.video.renditions.len(), 2, "one rendition per multitrack track id"); @@ -463,7 +463,7 @@ async fn import_multitrack_video_many_codecs() { let catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); let mut importer = Import::new(producer, catalog.reserve()); importer.decode(&bytes::BytesMut::from(out.as_slice())).unwrap(); - importer.finish().unwrap(); + let _finished = importer.finish().unwrap(); assert_eq!(catalog.snapshot().video.renditions.len(), 2); } @@ -566,7 +566,7 @@ async fn import_enhanced_av1() { let catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); let mut importer = Import::new(producer, catalog.reserve()); importer.decode(&bytes::BytesMut::from(out.as_slice())).unwrap(); - importer.finish().unwrap(); + let _finished = importer.finish().unwrap(); let snap = catalog.snapshot(); let (name, v) = snap.video.renditions.iter().next().unwrap(); @@ -605,7 +605,7 @@ async fn import_explicit_sbr_names_its_output() { let catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); let mut importer = Import::new(producer, catalog.reserve()); importer.decode(&bytes::BytesMut::from(data)).unwrap(); - importer.finish().unwrap(); + let _finished = importer.finish().unwrap(); let snap = catalog.snapshot(); let a = snap.audio.renditions.values().next().expect("an AAC track"); @@ -627,7 +627,7 @@ async fn import_enhanced_ac3() { let catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); let mut importer = Import::new(producer, catalog.reserve()); importer.decode(&bytes::BytesMut::from(out.as_slice())).unwrap(); - importer.finish().unwrap(); + let _finished = importer.finish().unwrap(); let snap = catalog.snapshot(); let a = snap.audio.renditions.values().next().unwrap(); @@ -649,7 +649,7 @@ async fn import_enhanced_eac3() { let catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); let mut importer = Import::new(producer, catalog.reserve()); importer.decode(&bytes::BytesMut::from(out.as_slice())).unwrap(); - importer.finish().unwrap(); + let _finished = importer.finish().unwrap(); let snap = catalog.snapshot(); let a = snap.audio.renditions.values().next().unwrap(); @@ -706,7 +706,7 @@ async fn import_reports_negative_pts_and_can_resume() { } )); importer.decode(&[]).unwrap(); - importer.finish().unwrap(); + let _finished = importer.finish().unwrap(); let snap = catalog.snapshot(); let name = snap.video.renditions.keys().next().unwrap(); @@ -749,7 +749,7 @@ async fn import_enhanced_hvc1_applies_composition_time() { let catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); let mut importer = Import::new(producer, catalog.reserve()); importer.decode(&bytes::BytesMut::from(out.as_slice())).unwrap(); - importer.finish().unwrap(); + let _finished = importer.finish().unwrap(); let snap = catalog.snapshot(); let name = snap.video.renditions.keys().next().unwrap(); @@ -854,7 +854,7 @@ async fn import(chunks: &[Vec]) -> Imported { after = kio::clock::wall(); } } - importer.finish().unwrap(); + let _finished = importer.finish().unwrap(); let snapshot = catalog.snapshot(); Imported { @@ -928,7 +928,7 @@ async fn first_catalog_carries_the_anchored_clock() { assert!(!published.is_empty(), "the first frame publishes the catalog"); assert!(published.iter().all(|clock| *clock == Some(anchored)), "{published:?}"); - importer.finish().unwrap(); + let _finished = importer.finish().unwrap(); assert!(clocks.drain().iter().all(|clock| *clock == Some(anchored))); } @@ -948,8 +948,8 @@ fn script_metadata_drops_warn_once() { ); } import.decode(&data).unwrap(); - import.finish().unwrap(); assert_eq!(import.dropped_metadata(), 2); + let _import = import.finish().unwrap(); assert_eq!(catalog.snapshot().audio.renditions.len(), 1); logs_assert(|lines| { let warnings = lines @@ -1000,7 +1000,7 @@ fn unsupported_codecs_fail_at_the_public_importer_boundary() { import.decode(chunk)?; } } - import.finish() + import.finish().map(drop) })(); assert!( matches!(result, Err(crate::Error::UnknownFormat(_))), @@ -1010,16 +1010,3 @@ fn unsupported_codecs_fail_at_the_public_importer_boundary() { } } -/// Input after finish is refused, as every importer refuses it, rather than publishing new tracks. -#[test] -fn input_after_finish_is_refused() { - let mut producer = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut producer, crate::catalog::Config::default()).unwrap(); - let mut importer = Import::new(producer, catalog.reserve()); - importer.finish().unwrap(); - importer.finish().unwrap(); - - let err = importer.decode(&synth_flv()).unwrap_err(); - assert!(matches!(err, crate::Error::Finished), "{err:?}"); - assert!(catalog.snapshot().video.renditions.is_empty(), "no track was declared"); -} diff --git a/rs/moq-mux/src/container/fmp4/export_test.rs b/rs/moq-mux/src/container/fmp4/export_test.rs index bf6798936de..067feea4912 100644 --- a/rs/moq-mux/src/container/fmp4/export_test.rs +++ b/rs/moq-mux/src/container/fmp4/export_test.rs @@ -26,7 +26,7 @@ const RECORDING_MAX_DELAY: std::time::Duration = std::time::Duration::from_secs( async fn avc3_source_to_cmaf_export_roundtrip() { let mut live = Live::avc3(); live.track.write(video_frame(0, true)).unwrap(); - live.track.finish().unwrap(); + live.track.end_with(None).unwrap(); let mut exporter = crate::container::fmp4::Export::new(live.source().await, live.catalog_stream().await); let init = chunk_now(&mut exporter) @@ -99,7 +99,7 @@ async fn legacy_aac_source_to_cmaf_export_synthesizes_esds() { let mut live = Live::audio(config); live.track.write(raw_frame(0, &[0x01, 0x02, 0x03, 0x04], true)).unwrap(); - live.track.finish().unwrap(); + live.track.end_with(None).unwrap(); let mut exporter = crate::container::fmp4::Export::new(live.source().await, live.catalog_stream().await); let init = chunk_now(&mut exporter) @@ -164,7 +164,7 @@ async fn vp8_source_to_cmaf_export_synthesizes_vp08() { true, )) .unwrap(); - live.track.finish().unwrap(); + live.track.end_with(None).unwrap(); let mut exporter = crate::container::fmp4::Export::new(live.source().await, live.catalog_stream().await); let init = chunk_now(&mut exporter) @@ -296,7 +296,7 @@ async fn vp9_source_to_cmaf_export_synthesizes_vp09() { catalog.modify().unwrap().video.renditions.insert(name, config); }); live.track.write(raw_frame(0, &[0x82, 0x49, 0x83, 0x42], true)).unwrap(); - live.track.finish().unwrap(); + live.track.end_with(None).unwrap(); let mut exporter = crate::container::fmp4::Export::new(live.source().await, live.catalog_stream().await); let init = chunk_now(&mut exporter) @@ -361,7 +361,7 @@ async fn av1_source_to_cmaf_export_synthesizes_av01() { catalog.modify().unwrap().video.renditions.insert(name, config); }); live.track.write(raw_frame(0, &[0x12, 0x00, 0x0a, 0x0b], true)).unwrap(); - live.track.finish().unwrap(); + live.track.end_with(None).unwrap(); let mut exporter = crate::container::fmp4::Export::new(live.source().await, live.catalog_stream().await); let init = chunk_now(&mut exporter) @@ -496,7 +496,7 @@ async fn single_track_export_init_matches_fragment_track_id() { // A fragment is a group, and ending the track is what closes the last one. The // next non-init fragment is a moof+mdat for the same (only) track. - importer.finish().unwrap(); + let importer = importer.finish().unwrap(); let fragment = kio::clock::timeout(std::time::Duration::from_secs(1), exporter.next()) .await .expect("exporter timed out") @@ -544,7 +544,7 @@ async fn next_chunk_reports_segment_metadata() { live.track.write(video_frame(0, true)).unwrap(); live.track.write(video_frame(33_000, false)).unwrap(); live.track.write(video_frame(66_000, true)).unwrap(); - live.track.finish().unwrap(); + live.track.end_with(None).unwrap(); // Sub-GOP cap so GOP 0 splits into two parts (the trailing part non-independent). let mut exporter = crate::container::fmp4::Export::new(live.source().await, live.catalog_stream().await) @@ -679,7 +679,7 @@ async fn ntsc_tail_uses_a_representable_catalog_cadence() { catalog.modify().unwrap().video.renditions.insert(name, config); }); live.track.write(video_frame(0, true)).unwrap(); - live.track.finish().unwrap(); + live.track.end_with(None).unwrap(); let mut exporter = crate::container::fmp4::Export::new(live.source().await, live.catalog_stream().await); chunk_now(&mut exporter) @@ -703,7 +703,7 @@ async fn unusable_framerate_uses_the_standard_fallback_rate() { catalog.modify().unwrap().video.renditions.insert(name, config); }); live.track.write(raw_frame(0, &[0x82, 0x00], true)).unwrap(); - live.track.finish().unwrap(); + live.track.end_with(None).unwrap(); let mut exporter = crate::container::fmp4::Export::new(live.source().await, live.catalog_stream().await); chunk_now(&mut exporter) @@ -1070,8 +1070,8 @@ async fn export_av(group: u64) -> (Vec<(u32, usize)>, (u32, u32)) { .write(raw_frame(i * 20_000, &[0x08, 0xaa, 0xbb, 0xcc], i % group == 0)) .unwrap(); } - live.track.finish().unwrap(); - audio.finish().unwrap(); + live.track.end_with(None).unwrap(); + let _finished = audio.finish().unwrap(); let mut exporter = crate::container::fmp4::Export::new(live.source().await, live.catalog_stream().await) .with_delay(moq_net::track::Delay::default().with_max(RECORDING_MAX_DELAY)); @@ -1132,7 +1132,7 @@ async fn video_that_ends_first_writes_its_tail_in_order() { for i in 0..6u64 { live.track.write(video_frame(i * 33_000, i % 3 == 0)).unwrap(); } - live.track.finish().unwrap(); + live.track.end_with(None).unwrap(); // The audio runs on for another half second in 100 ms groups, always ready. for i in 0..30u64 { audio @@ -1820,7 +1820,7 @@ async fn a_new_parameter_set_stays_in_band() { let sps2: &[u8] = &[0x67, 0x42, 0xc0, 0x1f, 0xde, 0xad, 0xbe, 0xee]; live.track.write(video_frame(0, true)).unwrap(); live.track.write(annexb_frame(33_000, &[sps2, PPS, IDR])).unwrap(); - live.track.finish().unwrap(); + live.track.end_with(None).unwrap(); let mut exporter = crate::container::fmp4::Export::new(live.source().await, live.catalog_stream().await) .with_delay(moq_net::track::Delay::default().with_max(RECORDING_MAX_DELAY)); @@ -1874,7 +1874,7 @@ async fn split_parameter_sets_case( live.track.write(frame(0, &leading, true)).unwrap(); live.track.write(frame(0, &[idr], idr_opens_group)).unwrap(); live.track.write(frame(33_000, &[delta], false)).unwrap(); - live.track.finish().unwrap(); + live.track.end_with(None).unwrap(); let mut exporter = crate::container::fmp4::Export::new(live.source().await, live.catalog_stream().await) .with_delay(moq_net::track::Delay::default().with_max(RECORDING_MAX_DELAY)); @@ -1952,7 +1952,7 @@ async fn an_in_band_keyframe_without_parameter_sets_fails() { chunk_now(&mut exporter).await.init().expect("init"); live.track.write(annexb_frame(0, &[IDR])).unwrap(); - live.track.finish().unwrap(); + live.track.end_with(None).unwrap(); let err = error_now(&mut exporter).await; assert!( matches!( @@ -2265,7 +2265,7 @@ async fn a_bare_keyframe_is_prefixed_with_cached_parameter_sets() { let mut live = Live::avc3(); live.track.write(video_frame(0, true)).unwrap(); live.track.write(annexb_frame(33_000, &[IDR])).unwrap(); - live.track.finish().unwrap(); + live.track.end_with(None).unwrap(); let mut exporter = crate::container::fmp4::Export::new(live.source().await, live.catalog_stream().await) .with_delay(moq_net::track::Delay::default().with_max(RECORDING_MAX_DELAY)); @@ -2345,7 +2345,7 @@ async fn high10_waits_for_its_sps() { assert!(pending.is_err(), "an init before the SPS: {pending:?}"); live.track.write(video_frame(0, true)).unwrap(); - live.track.finish().unwrap(); + live.track.end_with(None).unwrap(); let init = chunk_now(&mut exporter).await.init().expect("init"); let mp4_atom::Codec::Avc1(avc1) = video_codec(&init) else { panic!("expected an avc1 sample entry"); @@ -2408,7 +2408,7 @@ async fn annexb_without_dimensions_waits_for_the_sps() { let sps = crate::codec::h264::fixtures::SPS_IPB; let pps = crate::codec::h264::fixtures::PPS; live.track.write(annexb_frame(0, &[sps, pps, IDR])).unwrap(); - live.track.finish().unwrap(); + live.track.end_with(None).unwrap(); let init = chunk_now(&mut exporter).await.init().expect("init"); let mp4_atom::Codec::Avc1(avc1) = video_codec(&init) else { panic!("expected an avc1 sample entry built from the SPS"); @@ -2615,7 +2615,7 @@ async fn export_reordered_clip(cap: Option) -> (Bytes, Vec< let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = crate::container::ts::Import::new(broadcast, catalog.reserve()); import.decode(&BytesMut::from(&data[..])).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let catalog_stream = crate::catalog::Consumer::<()>::new(&consumer, crate::catalog::CatalogFormat::Hang) .await diff --git a/rs/moq-mux/src/container/fmp4/import.rs b/rs/moq-mux/src/container/fmp4/import.rs index 5f8164a3d28..cc4bf56ad93 100644 --- a/rs/moq-mux/src/container/fmp4/import.rs +++ b/rs/moq-mux/src/container/fmp4/import.rs @@ -2007,7 +2007,7 @@ mod tests { assert!(matches!(err, crate::Error::Cmaf(Error::BackwardsDataOffset)), "{err:?}"); assert!(consumer.recv_group().now_or_never().is_none()); assert_eq!(import.decode(&[0xA5; 8]).unwrap_err().to_string(), err.to_string()); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); assert!(consumer.recv_group().now_or_never().unwrap().unwrap().is_none()); } } @@ -2041,12 +2041,8 @@ mod tests { assert_eq!(import.cut().unwrap_err().to_string(), first); assert_eq!(import.seek(100).unwrap_err().to_string(), first); assert_eq!(import.discontinuity().unwrap_err().to_string(), first); - import.finish().unwrap(); - assert_eq!(import.decode(&[]).unwrap_err().to_string(), first); - assert_eq!( - import.decode(&box_header(b"free", Some(0))).unwrap_err().to_string(), - first - ); + // The accepted media still ends cleanly. + let _import = import.finish().unwrap(); } } @@ -2141,7 +2137,7 @@ mod tests { import.decode(&box_header(b"mdat", Some(4))).unwrap_err().to_string(), err.to_string() ); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); } // A short moof declares one field-less sample per body byte past the base, and no more. @@ -2350,7 +2346,7 @@ mod tests { "unreferenced payload must not enter the output cache" ); } - import.finish().unwrap(); + let _finished = import.finish().unwrap(); } } } @@ -2408,7 +2404,7 @@ mod tests { .decode(&media_header(id, 0, &[0], SIZE as u32, Some(SIZE))) .unwrap(); import.decode(&sample).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); assert_eq!(groups(&mut consumer), [(100, Some(1))], "passthrough: {passthrough}"); } } @@ -2427,7 +2423,8 @@ mod tests { .decode(&media_header(id, 1024, &[0], SIZE as u32, Some(SIZE))) .unwrap(); import.decode(&[0xA5; SIZE / 2]).unwrap(); - let err = import.finish().unwrap_err(); + // In place, so the tracks can be inspected after the failure. + let err = import.finish_in_place().unwrap_err(); assert!(matches!(err, crate::Error::Mp4(_)), "{err:?}"); assert_eq!( groups(&mut consumer), @@ -2463,7 +2460,7 @@ mod tests { import.decode(&[0xA5; SIZE / 2]).unwrap_err().to_string(), err.to_string() ); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); assert_eq!(groups(&mut consumer), [(0, Some(1))], "passthrough: {passthrough}"); } } diff --git a/rs/moq-mux/src/container/fmp4/import_test.rs b/rs/moq-mux/src/container/fmp4/import_test.rs index e25b5e72860..ecab2bc6a44 100644 --- a/rs/moq-mux/src/container/fmp4/import_test.rs +++ b/rs/moq-mux/src/container/fmp4/import_test.rs @@ -94,7 +94,7 @@ fn video_config_result(codec: mp4_atom::Codec) -> crate::Result drop(fmp4.finish().unwrap()), + false => drop(fmp4), } - drop(fmp4); let snapshot = catalog.snapshot(); assert!(snapshot.video.renditions.is_empty(), "{label}"); assert!(snapshot.audio.renditions.is_empty(), "{label}"); @@ -695,7 +695,7 @@ async fn bbb_samples(passthrough: bool) -> [Vec<(u128, bool, bytes::Bytes)>; 2] let mut fmp4 = crate::container::fmp4::Import::new(broadcast, catalog.reserve()).with_cmaf(passthrough); // The file ends on a malformed fragment, after every whole one has published. let _ = fmp4.decode(include_bytes!("test_data/bbb.mp4")); - fmp4.finish().unwrap(); + let _finished = fmp4.finish().unwrap(); published(&consumer, &catalog).await } @@ -855,7 +855,7 @@ async fn separated_runs_keep_their_samples() { for chunk in fragment.chunks(7) { fmp4.decode(chunk).unwrap(); } - fmp4.finish().unwrap(); + let _finished = fmp4.finish().unwrap(); let [got_video, got_audio] = published(&consumer, &catalog).await; assert_eq!(got_video, video, "video, passthrough {passthrough}"); assert_eq!(got_audio, audio, "audio, passthrough {passthrough}"); @@ -1082,7 +1082,7 @@ async fn unwrapped_samples_present_at_their_composition_time() { .encode(&mut fragment) .unwrap(); fmp4.decode(&fragment).unwrap(); - fmp4.finish().unwrap(); + let _finished = fmp4.finish().unwrap(); let mut group = track.recv_group().await.unwrap().unwrap(); let mut frames = Vec::new(); @@ -1225,7 +1225,7 @@ async fn audio_presents_from_its_edit() { // A fragment wholly before the edit publishes nothing. fmp4.decode(&audio_fragment(0, 512, 4)).unwrap(); fmp4.decode(&audio_fragment_samples(512, 1024, 4, 3)).unwrap(); - fmp4.finish().unwrap(); + let _finished = fmp4.finish().unwrap(); let snapshot = catalog.snapshot(); if let Container::Cmaf { init } = &snapshot.audio.renditions.values().next().unwrap().container { @@ -1337,7 +1337,7 @@ async fn video_drops_the_samples_before_its_edit() { for fragment in &fragments { fmp4.decode(fragment).unwrap(); } - fmp4.finish().unwrap(); + let _finished = fmp4.finish().unwrap(); let [video, _] = published(&consumer, &catalog).await; assert_eq!(video, expected, "edit at {media_time}, passthrough {passthrough}"); } @@ -1390,7 +1390,7 @@ async fn video_joined_mid_gop_starts_at_a_sync_sample() { for fragment in &after_seek { fmp4.decode(fragment).unwrap(); } - fmp4.finish().unwrap(); + let _finished = fmp4.finish().unwrap(); let [video, _] = published(&consumer, &catalog).await; assert_eq!(video, expected, "passthrough {passthrough}"); } @@ -1592,7 +1592,7 @@ async fn test_seek_sets_initial_sequence_in(passthrough: bool) { fmp4.seek(100).unwrap(); // Trailing partial fragments may error; ignore. let _ = fmp4.decode(&frag_buf); - fmp4.finish().unwrap(); + let _finished = fmp4.finish().unwrap(); let sequences = drain_group_sequences(&mut video_track); assert!(!sequences.is_empty(), "expected at least one group"); @@ -2046,7 +2046,7 @@ async fn segmented_source_groups_per_segment_in(passthrough: bool) { fmp4.decode(&frag).unwrap(); } } - fmp4.finish().unwrap(); + let _finished = fmp4.finish().unwrap(); // An unwrapped video group also ends on a Legacy duration marker. let marker = usize::from(!passthrough); @@ -2109,7 +2109,7 @@ async fn segmented_source_writes_one_group_per_segment_in(passthrough: bool) { Some(std::time::Duration::from_secs(1)), "passthrough={passthrough}" ); - fmp4.finish().unwrap(); + let _finished = fmp4.finish().unwrap(); catalog.finish().unwrap(); assert_eq!( @@ -2167,7 +2167,7 @@ async fn groups_with_skew(video_offset_us: i64, passthrough: bool) -> (Vec, fmp4.decode(&frag).unwrap(); } } - fmp4.finish().unwrap(); + let _finished = fmp4.finish().unwrap(); catalog.finish().unwrap(); ( @@ -2247,7 +2247,7 @@ async fn a_single_leading_styp_still_segments_on_keyframes_in(passthrough: bool) .unwrap(); fmp4.decode(&frag).unwrap(); } - fmp4.finish().unwrap(); + let _finished = fmp4.finish().unwrap(); // An unwrapped video group also ends on a Legacy duration marker. let marker = usize::from(!passthrough); @@ -2306,7 +2306,7 @@ async fn a_mid_gop_join_keeps_tracks_on_the_same_groups_in(passthrough: bool) { .unwrap(); fmp4.decode(&frag).unwrap(); } - fmp4.finish().unwrap(); + let _finished = fmp4.finish().unwrap(); let video = drain_group_sequences(&mut video_track); let audio = drain_group_sequences(&mut audio_track); @@ -2489,7 +2489,7 @@ fn fragment_jitter_never_shrinks_in(passthrough: bool) { "a tighter fragment pair lowered the jitter" ); - fmp4.finish().unwrap(); + let _finished = fmp4.finish().unwrap(); assert_eq!(audio_jitter(&catalog), Some(cadence)); } @@ -2603,7 +2603,7 @@ async fn import_session(chunks: &[Vec]) -> Imported { after = kio::clock::wall(); } } - fmp4.finish().unwrap(); + let _finished = fmp4.finish().unwrap(); catalog.finish().unwrap(); let snapshot = catalog.snapshot(); @@ -2691,7 +2691,7 @@ async fn join_after(earlier: Earlier, start_us: u64) { .append(&serde_json::json!("after"), catalog.clock().now()) .unwrap(); } - fmp4.finish().unwrap(); + let _finished = fmp4.finish().unwrap(); let snapshot = catalog.snapshot(); assert_eq!( @@ -2755,7 +2755,7 @@ async fn an_import_shifts_onto_an_explicit_clock() { let before = clock.now().as_micros(); fmp4.decode(&live_session(3_600_000_000, 5)).unwrap(); let after = clock.now().as_micros(); - fmp4.finish().unwrap(); + let _finished = fmp4.finish().unwrap(); let snapshot = catalog.snapshot(); assert_eq!(snapshot.media.clock, clock.wall()); @@ -2844,7 +2844,7 @@ async fn first_catalog_carries_the_anchored_clock() { assert!(!published.is_empty(), "the first fragment publishes the catalog"); assert!(published.iter().all(|clock| *clock == Some(anchored)), "{published:?}"); - fmp4.finish().unwrap(); + let _finished = fmp4.finish().unwrap(); assert!(clocks.drain().iter().all(|clock| *clock == Some(anchored))); } @@ -2886,21 +2886,6 @@ async fn a_moov_with_nothing_selected_releases_its_reservation() { assert_eq!(clocks.drain().len(), 1, "no reservation still gates the catalog"); } -/// A moov after `finish()` is refused: its tracks would be declared but never finished. -#[test] -fn a_moov_after_finish_is_refused() { - let (init, _, _) = bbb_init(); - let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast, Default::default()).unwrap(); - let mut fmp4 = crate::container::fmp4::Import::new(broadcast, catalog.reserve()); - - fmp4.finish().unwrap(); - let err = fmp4.decode(&init).unwrap_err(); - assert!(matches!(err, crate::Error::Finished), "{err:?}"); - let snapshot = catalog.snapshot(); - assert!(snapshot.video.renditions.is_empty() && snapshot.audio.renditions.is_empty()); -} - /// The catalog waits for a selected track's fragment: a deselected track's fragment is skipped, /// so it doesn't release the reservation even when it arrives first. #[tokio::test] @@ -2967,7 +2952,7 @@ async fn imports_the_sample_defaults_fixtures_in(passthrough: bool) { fmp4.decode(&fixture.fragment).unwrap(); fmp4.decode(&later).unwrap(); - fmp4.finish().unwrap(); + let _finished = fmp4.finish().unwrap(); let Some(wire) = wire else { // Unwrapped, each sample is its own frame and each sync sample opens a group. Legacy @@ -3033,7 +3018,7 @@ fn an_unknown_subtitle_track_does_not_refuse_supported_media() { let catalog = crate::catalog::Producer::new(&mut broadcast, Default::default()).unwrap(); let mut importer = super::Import::new(broadcast, catalog.reserve()); importer.decode(&input).unwrap(); - importer.finish().unwrap(); + let _finished = importer.finish().unwrap(); assert!(!catalog.snapshot().video.renditions.is_empty()); assert!(catalog.snapshot().text.renditions.is_empty()); } @@ -3118,7 +3103,7 @@ async fn webvtt_samples_become_timed_text_with_source_metadata() { let catalog = crate::catalog::Producer::new(&mut broadcast, Default::default()).unwrap(); let mut importer = super::Import::new(broadcast, catalog.reserve()); importer.decode(&input).unwrap(); - importer.finish().unwrap(); + let _finished = importer.finish().unwrap(); let snapshot = catalog.snapshot(); assert_eq!(snapshot.text.renditions.len(), 1); let (name, config) = snapshot.text.renditions.iter().next().unwrap(); @@ -3181,7 +3166,7 @@ fn empty_and_multiple_webvtt_cues_are_supported() { let catalog = crate::catalog::Producer::new(&mut broadcast, Default::default()).unwrap(); let mut importer = super::Import::new(broadcast, catalog.reserve()); importer.decode(&input).unwrap(); - importer.finish().unwrap(); + let _finished = importer.finish().unwrap(); assert_eq!(catalog.snapshot().text.renditions.len(), 1); } } @@ -3240,7 +3225,7 @@ fn oversized_text_sample_is_refused_before_staging() { .to_string(), err.to_string() ); - importer.finish().unwrap(); + let _finished = importer.finish().unwrap(); assert_eq!(catalog.snapshot().text.renditions.len(), 1); } @@ -3258,7 +3243,7 @@ async fn webvtt_kind_and_lone_cr_terminators() { let catalog = crate::catalog::Producer::new(&mut broadcast, Default::default()).unwrap(); let mut importer = super::Import::new(broadcast, catalog.reserve()); importer.decode(&input).unwrap(); - importer.finish().unwrap(); + let _finished = importer.finish().unwrap(); let snapshot = catalog.snapshot(); let (name, config) = snapshot.text.renditions.iter().next().unwrap(); assert_eq!(config.role, hang::catalog::TextRole::Caption); @@ -3289,7 +3274,7 @@ fn webvtt_seek_after_several_cues_does_not_reuse_a_group() { // Segment 101 was missed; groups 100..=102 already hold the three cues. importer.seek(102).unwrap(); importer.decode(&webvtt_fragment(4000, &[cue], 1000)).unwrap(); - importer.finish().unwrap(); + let _finished = importer.finish().unwrap(); } /// A text fragment may not start inside the decode span of the previous fragment's samples. @@ -3334,8 +3319,8 @@ fn emsg_metadata_drops_warn_once() { data.extend_from_slice(&fixture[..cursor.position() as usize]); data.extend_from_slice(&audio_fragment(0, 1024, 327)); import.decode(&data).unwrap(); - import.finish().unwrap(); assert_eq!(import.dropped_metadata(), 2); + let _import = import.finish().unwrap(); assert_eq!(catalog.snapshot().video.renditions.len(), 1); assert_eq!(catalog.snapshot().audio.renditions.len(), 1); logs_assert(|lines| { @@ -3370,7 +3355,7 @@ async fn webvtt_sub_millisecond_cue_stays_positive() { let catalog = crate::catalog::Producer::new(&mut broadcast, Default::default()).unwrap(); let mut importer = super::Import::new(broadcast, catalog.reserve()); importer.decode(&init).unwrap(); - importer.finish().unwrap(); + let _finished = importer.finish().unwrap(); let snapshot = catalog.snapshot(); let name = snapshot.text.renditions.keys().next().unwrap(); let track = consumer.track(name).unwrap().subscribe(None).await.unwrap(); @@ -3388,7 +3373,7 @@ async fn imported_webvtt(payload: Vec) -> bytes::Bytes { let catalog = crate::catalog::Producer::new(&mut broadcast, Default::default()).unwrap(); let mut importer = super::Import::new(broadcast, catalog.reserve()); importer.decode(&webvtt_input(payload, 1250)).unwrap(); - importer.finish().unwrap(); + let _finished = importer.finish().unwrap(); let snapshot = catalog.snapshot(); let name = snapshot.text.renditions.keys().next().unwrap(); let track = consumer.track(name).unwrap().subscribe(None).await.unwrap(); @@ -3491,7 +3476,7 @@ async fn imported_ttml( input.extend(webvtt_fragment(100_000, &[TTML.as_bytes().to_vec()], 4000)); let mut importer = super::Import::new(broadcast, timebase.reserve()); importer.decode(&input).unwrap(); - importer.finish().unwrap(); + let _finished = importer.finish().unwrap(); let snapshot = catalog.snapshot(); let (name, config) = snapshot.text.renditions.iter().next().unwrap(); let track = consumer.track(name).unwrap().subscribe(None).await.unwrap(); @@ -3551,7 +3536,7 @@ fn unsupported_stpp_tracks_are_skipped() { let catalog = crate::catalog::Producer::new(&mut broadcast, Default::default()).unwrap(); let mut importer = super::Import::new(broadcast, catalog.reserve()); importer.decode(&input).unwrap(); - importer.finish().unwrap(); + let _finished = importer.finish().unwrap(); assert!(catalog.snapshot().text.renditions.is_empty(), "{namespace} {mime}"); } } @@ -3584,7 +3569,7 @@ async fn ttml_body_begins_at_the_serialized_frame_timestamp() { let catalog = crate::catalog::Producer::new(&mut broadcast, Default::default()).unwrap(); let mut importer = super::Import::new(broadcast, catalog.reserve()); importer.decode(&input).unwrap(); - importer.finish().unwrap(); + let _finished = importer.finish().unwrap(); let snapshot = catalog.snapshot(); let name = snapshot.text.renditions.keys().next().unwrap(); let track = consumer.track(name).unwrap().subscribe(None).await.unwrap(); @@ -3831,7 +3816,7 @@ async fn declared_boundaries_wait_for_sync_samples() { .unwrap(); import.decode(&fragment).unwrap(); } - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let marker = usize::from(!passthrough); assert_eq!( drain_group_frames(&mut track), @@ -3874,7 +3859,7 @@ async fn declared_boundaries_do_not_split_passthrough_fragments() { import .decode(&video_fragment(video_id, 4 * tick, tick, &[(0, 0, 0xE)])) .unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); assert_eq!( drain_group_frames(&mut track), if passthrough { vec![3, 1] } else { vec![3, 3, 2] } diff --git a/rs/moq-mux/src/container/fmp4/muxer.rs b/rs/moq-mux/src/container/fmp4/muxer.rs index 15a4b3d384d..dbf7ae7461c 100644 --- a/rs/moq-mux/src/container/fmp4/muxer.rs +++ b/rs/moq-mux/src/container/fmp4/muxer.rs @@ -410,7 +410,7 @@ mod tests { let mut producer = crate::container::Producer::new(track, HangContainer::Legacy(crate::container::Kind::Data)); producer.write(frame(10_000_000, true)).unwrap(); producer.write(frame(10_033_000, false)).unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let mut group = subscriber.next_group().await.unwrap().expect("a group"); @@ -453,7 +453,7 @@ mod tests { producer.write(frame(10_000_000, true)).unwrap(); producer.write(frame(10_033_000, false)).unwrap(); producer.cut(Some(Timestamp::from_micros(10_066_000).unwrap())).unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let mut group = subscriber.next_group().await.unwrap().expect("a group"); let mut muxer = video_muxer(); @@ -482,7 +482,7 @@ mod tests { duration: None, }) .unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let mut group = subscriber.next_group().await.unwrap().expect("a group"); let config = VideoConfig::new(VideoCodec::VP8); diff --git a/rs/moq-mux/src/container/group.rs b/rs/moq-mux/src/container/group.rs index f2957c9dc5c..96c4801079d 100644 --- a/rs/moq-mux/src/container/group.rs +++ b/rs/moq-mux/src/container/group.rs @@ -112,7 +112,7 @@ mod tests { let mut media = crate::container::Producer::new(track, Hang::Legacy(crate::container::Kind::Data)); media.write(frame(1_000_000, b"keyframe", true)).unwrap(); media.write(frame(1_020_000, b"delta", false)).unwrap(); - media.finish().unwrap(); + let _finished = media.finish().unwrap(); let group = consumer.track("media").unwrap().fetch_group(0, None).await.unwrap(); let mut group = GroupConsumer::new(group, Hang::Legacy(crate::container::Kind::Data)); @@ -140,7 +140,7 @@ mod tests { crate::container::Producer::new(track, Hang::new(&config, crate::container::Kind::Data).unwrap()); producer.write(frame(0, b"", true)).unwrap(); producer.write(frame(10_000, b"data", false)).unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let mut live = crate::container::Consumer::new( subscription, Hang::new(&config, crate::container::Kind::Data).unwrap(), @@ -171,7 +171,7 @@ mod tests { media .cut(Some(moq_net::Timestamp::from_micros(1_053_000).unwrap())) .unwrap(); - media.finish().unwrap(); + let _finished = media.finish().unwrap(); let group = consumer.track("media").unwrap().fetch_group(0, None).await.unwrap(); let mut group = GroupConsumer::new(group, Hang::Legacy(crate::container::Kind::Video)); @@ -248,7 +248,7 @@ mod tests { }) .unwrap(); } - media.finish().unwrap(); + let _finished = media.finish().unwrap(); let group = consumer.track("video").unwrap().fetch_group(0, None).await.unwrap(); let mut group = GroupConsumer::new(group, Hang::new(&cmaf, crate::container::Kind::Video).unwrap()); diff --git a/rs/moq-mux/src/container/mkv/export_test.rs b/rs/moq-mux/src/container/mkv/export_test.rs index 721d891abd1..4d5456ccba7 100644 --- a/rs/moq-mux/src/container/mkv/export_test.rs +++ b/rs/moq-mux/src/container/mkv/export_test.rs @@ -60,7 +60,7 @@ async fn a_declared_depth_bounds_malformed_video_by_count_and_payload() { frame.payload = bytes.into(); live.track.write(frame).unwrap(); } - live.track.finish().unwrap(); + live.track.end_with(None).unwrap(); live.catalog.finish().unwrap(); let mut export = super::Export::new(live.source().await, live.catalog_stream().await) .with_fragment_duration(std::time::Duration::ZERO) @@ -109,8 +109,8 @@ async fn declared_reorder_starts_before_zero_and_resets_each_group() { } audio.write(raw_frame(start, b"audio", true)).unwrap(); } - live.track.finish().unwrap(); - audio.finish().unwrap(); + live.track.end_with(None).unwrap(); + let _finished = audio.finish().unwrap(); live.catalog.finish().unwrap(); let mut export = super::Export::new(live.source().await, live.catalog_stream().await) .with_delay( @@ -153,7 +153,7 @@ async fn export_header_roundtrip_vp9_opus() { let mut importer = crate::container::mkv::Import::new(producer, catalog.reserve()); let buf = bytes::BytesMut::from(import_bytes.as_slice()); importer.decode(&buf).unwrap(); - importer.finish().unwrap(); + let _finished = importer.finish().unwrap(); // Now subscribe via the exporter and pull bytes. let catalog_stream = crate::catalog::Consumer::<()>::new(&consumer, crate::catalog::CatalogFormat::Hang) @@ -382,7 +382,7 @@ async fn export_header_roundtrip_mp3() { importer .decode(&bytes::BytesMut::from(import_bytes.as_slice())) .unwrap(); - importer.finish().unwrap(); + let _finished = importer.finish().unwrap(); let catalog_stream = crate::catalog::Consumer::<()>::new(&consumer, crate::catalog::CatalogFormat::Hang) .await @@ -500,7 +500,7 @@ async fn export_derives_video_geometry_before_header() { true, )) .unwrap(); - live.track.finish().unwrap(); + live.track.end_with(None).unwrap(); let mut exporter = crate::container::mkv::Export::new(live.source().await, live.catalog_stream().await); let header = kio::clock::timeout(std::time::Duration::from_secs(1), exporter.next()) @@ -533,7 +533,7 @@ async fn export_emits_blocks_for_each_frame() { let mut importer = crate::container::mkv::Import::new(producer, catalog.reserve()); let buf = bytes::BytesMut::from(import_bytes.as_slice()); importer.decode(&buf).unwrap(); - importer.finish().unwrap(); + let importer = importer.finish().unwrap(); let catalog_stream = crate::catalog::Consumer::<()>::new(&consumer, crate::catalog::CatalogFormat::Hang) .await @@ -591,7 +591,7 @@ async fn export_emits_blocks_for_each_frame() { let mut imp2 = crate::container::mkv::Import::new(bcast2, cat2.reserve()); let rt = bytes::BytesMut::from(exported.as_slice()); imp2.decode(&rt).unwrap(); - imp2.finish().unwrap(); + let _finished = imp2.finish().unwrap(); let snap = cat2.snapshot(); assert_eq!(snap.video.renditions.len(), 1); assert_eq!(snap.audio.renditions.len(), 1); @@ -648,7 +648,7 @@ async fn export_avc3_source_synthesizes_avcc_and_length_prefixes() { let mut live = Live::avc3(); live.track.write(video_frame(0, true)).unwrap(); live.track.write(raw_frame(33_000, PSLICE_ANNEXB, false)).unwrap(); - live.track.finish().unwrap(); + live.track.end_with(None).unwrap(); live.catalog.finish().unwrap(); let mut exporter = crate::container::mkv::Export::new(live.source().await, live.catalog_stream().await) @@ -755,7 +755,7 @@ async fn export_avc3_source_synthesizes_avcc_and_length_prefixes() { let mut imp2 = crate::container::mkv::Import::new(bcast2, cat2.reserve()); let rt = bytes::BytesMut::from(exported.as_slice()); imp2.decode(&rt).unwrap(); - imp2.finish().unwrap(); + let _finished = imp2.finish().unwrap(); let snap = cat2.snapshot(); assert_eq!(snap.video.renditions.len(), 1); let v = snap.video.renditions.values().next().unwrap(); @@ -781,7 +781,7 @@ async fn export_fragment_duration_batches_blocks() { let mut importer = crate::container::mkv::Import::new(producer, catalog.reserve()); let buf = bytes::BytesMut::from(import_bytes.as_slice()); importer.decode(&buf).unwrap(); - importer.finish().unwrap(); + let importer = importer.finish().unwrap(); catalog.finish().unwrap(); let catalog_stream = crate::catalog::Consumer::<()>::new(&consumer, crate::catalog::CatalogFormat::Hang) @@ -1175,7 +1175,7 @@ async fn delay_drops_late_frames_and_drains_the_final_frame() { live.track.write(raw_frame(20_000, b"expired", true)).unwrap(); // Sent a delay ahead: any further would be a step in its timeline, which re-acquires. live.track.write(raw_frame(300_000, b"final", true)).unwrap(); - live.track.finish().unwrap(); + live.track.end_with(None).unwrap(); live.catalog.finish().unwrap(); let final_frame = export.next().await.unwrap().unwrap(); assert!(final_frame.windows(5).any(|part| part == b"final")); @@ -1245,8 +1245,8 @@ async fn reordered_video_uses_decode_deadlines_but_keeps_block_pts() { live.track.write(video_frame(timestamp * 1000, index == 0)).unwrap(); } audio.write(raw_frame(1_060_000, b"audio", true)).unwrap(); - live.track.finish().unwrap(); - audio.finish().unwrap(); + live.track.end_with(None).unwrap(); + let _finished = audio.finish().unwrap(); live.catalog.finish().unwrap(); let mut export = super::Export::new(live.source().await, live.catalog_stream().await) .with_delay( @@ -1455,7 +1455,7 @@ async fn live_reordered_video_keeps_every_frame() { count(chunk.unwrap().unwrap()); } } - live.track.finish().unwrap(); + live.track.end_with(None).unwrap(); live.catalog.finish().unwrap(); while let Some(chunk) = export.next().await.unwrap() { count(chunk); diff --git a/rs/moq-mux/src/container/mkv/import.rs b/rs/moq-mux/src/container/mkv/import.rs index a4024c63e97..3e7e5246336 100644 --- a/rs/moq-mux/src/container/mkv/import.rs +++ b/rs/moq-mux/src/container/mkv/import.rs @@ -1655,9 +1655,7 @@ mod tests { } assert_eq!(import.cluster_timestamp, timestamp); } - import.finish().unwrap(); - import.finish().unwrap(); - assert!(matches!(import.decode(&[0]), Err(crate::Error::Finished))); + let _import = import.finish().unwrap(); } /// A WebM prefix declaring `entries`, leaving an unknown-size Segment and Cluster open. @@ -1752,7 +1750,7 @@ mod tests { read.extend_from_slice(&chunk); } assert_eq!(read.len(), 1 + SIZE); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); } #[test] @@ -1770,7 +1768,7 @@ mod tests { import.decode(&[0]), Err(crate::Error::Moq(moq_net::Error::FrameTooLarge)) )); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); } #[test] @@ -1865,7 +1863,7 @@ mod tests { import.decode(&entry_ending_in(&header(id, SIZE), SIZE)).unwrap(); feed_unretained(&mut import, SIZE).unwrap(); assert_eq!(import.tracks.len(), 1, "{id:x?}"); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); } } @@ -2099,7 +2097,7 @@ mod tests { .unwrap(); feed_unretained(&mut import, SIZE).unwrap(); assert_eq!(import.tracks.len(), 1); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); } #[tokio::test] @@ -2121,7 +2119,7 @@ mod tests { import.decode(b"key").unwrap(); let mut frame = first_frame(&catalog, &consumer).await; assert_eq!(frame.read_all().await.unwrap(), b"\0key"[..]); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); } /// Output nobody reads is evicted by its track, so the importer keeps accepting input. @@ -2135,7 +2133,7 @@ mod tests { import.decode(&simple_block(1, payload.len() as u64)).unwrap(); import.decode(&payload).unwrap(); } - import.finish().unwrap(); + let _import = import.finish().unwrap(); } #[tokio::test(start_paused = true)] @@ -2147,9 +2145,9 @@ mod tests { import.decode(&simple_block(1, 1024)).unwrap(); import.decode(&[4; 512]).unwrap(); let mut frame = first_frame(&catalog, &consumer).await; - let err = import.finish().unwrap_err(); + // In place, so the frame fails from the finish rather than from dropping the importer. + let err = import.finish_in_place().unwrap_err(); assert!(matches!(err, crate::Error::Mkv(Error::Truncated))); - // The importer is still owned, and the frame fails rather than waiting on it. let read = frame.read_all().now_or_never().expect("the cut-short frame must fail"); assert!(matches!( read, @@ -2172,7 +2170,11 @@ mod tests { for piece in data.chunks(chunk) { import.decode(piece).unwrap(); } - assert!(matches!(import.finish(), Err(crate::Error::Mkv(Error::Truncated)))); + // In place, so the catalog still lists the ended tracks. + assert!(matches!( + import.finish_in_place(), + Err(crate::Error::Mkv(Error::Truncated)) + )); let snapshot = catalog.snapshot(); let (name, config) = snapshot.audio.renditions.iter().next().unwrap(); let subscription = moq_net::track::Subscription::default() @@ -2205,7 +2207,11 @@ mod tests { .unwrap(); import.decode(&[0xec, 0x81]).unwrap(); assert!(clocks.drain().is_empty(), "no block has anchored the catalog yet"); - assert!(matches!(import.finish(), Err(crate::Error::Mkv(Error::Truncated)))); + // In place, so the catalog still lists the ended tracks. + assert!(matches!( + import.finish_in_place(), + Err(crate::Error::Mkv(Error::Truncated)) + )); assert!( !clocks.drain().is_empty(), "finish must release the initial catalog reservation" @@ -2238,17 +2244,4 @@ mod tests { assert!(read.is_err(), "a frame cut short must not read as complete: {read:?}"); } } - - /// A failed finish is final: later input and a retried finish report its cause. - #[test] - fn a_failed_finish_reports_its_cause() { - let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast, Default::default()).unwrap(); - let mut import = Import::new(broadcast, catalog.reserve()); - // An EBML header declaring a byte it never sends. - import.decode(&[0x1a, 0x45, 0xdf, 0xa3, 0x81]).unwrap(); - assert!(matches!(import.finish(), Err(crate::Error::Mkv(Error::Truncated)))); - assert!(matches!(import.decode(&[0]), Err(crate::Error::Mkv(Error::Truncated)))); - assert!(matches!(import.finish(), Err(crate::Error::Mkv(Error::Truncated)))); - } } diff --git a/rs/moq-mux/src/container/mkv/import_test.rs b/rs/moq-mux/src/container/mkv/import_test.rs index 3e15aadb366..53d99923e44 100644 --- a/rs/moq-mux/src/container/mkv/import_test.rs +++ b/rs/moq-mux/src/container/mkv/import_test.rs @@ -184,7 +184,7 @@ fn run(data: &[u8]) -> crate::catalog::hang::Catalog { let mut mkv = crate::container::mkv::Import::new(broadcast, catalog.reserve()); let buf = bytes::BytesMut::from(data); mkv.decode(&buf).expect("decode"); - mkv.finish().expect("finish"); + let _finished = mkv.finish().expect("finish"); catalog.snapshot() } @@ -205,7 +205,7 @@ fn assert_out_of_range(track: MatroskaSpec, field: &str) { let mut broadcast = moq_net::broadcast::Info::new().produce(); let catalog = crate::catalog::Producer::new(&mut broadcast, Default::default()).unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); - let result = import.decode(&data).and_then(|()| import.finish()); + let result = import.decode(&data).and_then(|()| import.finish().map(drop)); assert!( matches!(&result, Err(crate::Error::Mkv(super::Error::ValueOutOfRange(name))) if *name == field), "{field}, reverse {reverse}: {result:?}" @@ -537,7 +537,7 @@ fn assert_invalid_aspect(data: &[u8], context: &str) { let mut broadcast = moq_net::broadcast::Info::new().produce(); let catalog = crate::catalog::Producer::new(&mut broadcast, Default::default()).unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); - let result = import.decode(data).and_then(|()| import.finish()); + let result = import.decode(data).and_then(|()| import.finish().map(drop)); assert!( matches!(result, Err(crate::Error::Mkv(super::Error::InvalidAspect(_)))), "{context}: {result:?}" @@ -827,7 +827,7 @@ async fn public_container_preserves_loc_for_mkv() { let reserved = catalog.reserve(); let mut import = super::Import::new(broadcast, reserved).with_container(hang::catalog::Container::Loc); import.decode(&data).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let snapshot = catalog.snapshot(); let (name, config) = snapshot.video.renditions.iter().next().unwrap(); @@ -983,7 +983,7 @@ async fn import_chunked(data: &[u8], chunk: usize) -> Imported { } } let after = kio::clock::wall(); - mkv.finish().unwrap(); + let _finished = mkv.finish().unwrap(); let snapshot = catalog.snapshot(); Imported { @@ -1120,31 +1120,10 @@ async fn first_catalog_carries_the_anchored_clock() { assert!(!published.is_empty(), "the first block publishes the catalog"); assert!(published.iter().all(|clock| *clock == Some(anchored)), "{published:?}"); - mkv.finish().unwrap(); + let _finished = mkv.finish().unwrap(); assert!(clocks.drain().iter().all(|clock| *clock == Some(anchored))); } -/// A Tracks element after `finish()` is refused: its tracks would be declared but never finished. -#[test] -fn tracks_after_finish_are_refused() { - let data = MkvBuilder::new() - .header("webm") - .segment_start() - .info(1_000_000) - .tracks(vec![track_entry_audio_opus(1, 48_000.0, 2)]) - .segment_end() - .build(); - - let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast, Default::default()).unwrap(); - let mut mkv = crate::container::mkv::Import::new(broadcast, catalog.reserve()); - - mkv.finish().unwrap(); - let err = mkv.decode(&data).unwrap_err(); - assert!(matches!(err, crate::Error::Finished), "{err:?}"); - assert!(catalog.snapshot().audio.renditions.is_empty(), "no track was declared"); -} - fn text_entry(number: u64, codec: &str, language: &str, name: &str) -> MatroskaSpec { MatroskaSpec::TrackEntry(Master::Full(vec![ MatroskaSpec::TrackNumber(number), @@ -1197,7 +1176,7 @@ async fn laced_opus_frames() { for piece in data.chunks(11) { importer.decode(piece).unwrap(); } - importer.finish().unwrap(); + let _finished = importer.finish().unwrap(); let snapshot = catalog.snapshot(); let (name, config) = snapshot.audio.renditions.iter().next().unwrap(); let track = consumer.track(name).unwrap().subscribe(None).await.unwrap(); @@ -1311,7 +1290,7 @@ async fn unlaced_block_duration_does_not_use_track_default() { let catalog = crate::catalog::Producer::new(&mut broadcast, Default::default()).unwrap(); let mut importer = super::Import::new(broadcast, catalog.reserve()); importer.decode(&data).unwrap(); - importer.finish().unwrap(); + let _finished = importer.finish().unwrap(); let snapshot = catalog.snapshot(); let name = snapshot.video.renditions.keys().next().unwrap(); let mut track = consumer.track(name).unwrap().subscribe(None).await.unwrap(); @@ -1422,7 +1401,7 @@ fn import_result(data: &[u8], initial: bool) -> crate::Result<()> { import.decode(chunk)?; } } - import.finish() + import.finish().map(drop) } #[test] @@ -1895,7 +1874,7 @@ async fn a_seek_mid_block_waits_for_the_block() { importer.decode(&data[..split]).unwrap(); importer.seek(100).unwrap(); importer.decode(&data[split..]).unwrap(); - importer.finish().unwrap(); + let _finished = importer.finish().unwrap(); let snapshot = catalog.snapshot(); let name = snapshot.audio.renditions.keys().next().unwrap(); @@ -2159,10 +2138,10 @@ fn repeated_reinit_keeps_flat_state() { let snapshot = catalog.snapshot(); assert_eq!(snapshot.video.renditions.len(), 1, "{label}"); assert_eq!(snapshot.audio.renditions.len(), 1, "{label}"); - if generation % 2 == 1 { - mkv.finish().unwrap(); + match generation % 2 == 1 { + true => drop(mkv.finish().unwrap()), + false => drop(mkv), } - drop(mkv); let snapshot = catalog.snapshot(); assert!(snapshot.video.renditions.is_empty(), "{label}"); assert!(snapshot.audio.renditions.is_empty(), "{label}"); diff --git a/rs/moq-mux/src/container/mp4.rs b/rs/moq-mux/src/container/mp4.rs index 77e973d2d22..a5595a99862 100644 --- a/rs/moq-mux/src/container/mp4.rs +++ b/rs/moq-mux/src/container/mp4.rs @@ -942,8 +942,8 @@ mod tests { for i in 0..12u64 { audio.write(raw_frame(i * 20_000, OPUS, i % 5 == 0)).unwrap(); } - live.track.finish().unwrap(); - audio.finish().unwrap(); + live.track.end_with(None).unwrap(); + let _finished = audio.finish().unwrap(); let mut export = export(&live).await; let mut file = Vec::new(); @@ -1006,9 +1006,9 @@ mod tests { for i in 0..10u64 { audio.write(raw_frame(i * 20_000, OPUS, i % 5 == 0)).unwrap(); } - audio.finish().unwrap(); + let _finished = audio.finish().unwrap(); live.track.write(video_frame(200_000, true)).unwrap(); - live.track.finish().unwrap(); + live.track.end_with(None).unwrap(); record(&mut export, &mut file).await; let unfinished = atoms(&file); @@ -1052,12 +1052,12 @@ mod tests { for i in 0..5u64 { audio.write(raw_frame(i * 20_000, OPUS, i == 0)).unwrap(); } - audio.finish().unwrap(); + let audio = audio.finish().unwrap(); record(&mut export, &mut file).await; let name = audio.name().to_string(); live.catalog.modify().unwrap().audio.renditions.remove(&name); live.track.write(video_frame(200_000, true)).unwrap(); - live.track.finish().unwrap(); + live.track.end_with(None).unwrap(); record(&mut export, &mut file).await; assert_eq!(export.fmp4.declarations().count(), 2); @@ -1076,12 +1076,12 @@ mod tests { for i in 0..10u64 { audio.write(raw_frame(i * 20_000, OPUS, i % 5 == 0)).unwrap(); } - audio.finish().unwrap(); + let _finished = audio.finish().unwrap(); // Video starts 100 ms in, and its second GOP comes a second after the first. for (timestamp, keyframe) in [(100_000, true), (133_000, false), (1_133_000, true), (1_166_000, false)] { live.track.write(video_frame(timestamp, keyframe)).unwrap(); } - live.track.finish().unwrap(); + live.track.end_with(None).unwrap(); let mut export = export(&live).await; let mut file = Vec::new(); @@ -1126,7 +1126,7 @@ mod tests { for (i, ×tamp) in presentations.iter().enumerate() { live.track.write(video_frame(timestamp as u64, i % 4 == 0)).unwrap(); } - live.track.finish().unwrap(); + live.track.end_with(None).unwrap(); let mut export = export(&live).await.with_fragment_duration(Duration::ZERO); let mut file = Vec::new(); @@ -1280,11 +1280,11 @@ mod tests { for i in 0..10u64 { audio.write(raw_frame(100_000 + i * 20_000, OPUS, i % 5 == 0)).unwrap(); } - audio.finish().unwrap(); + let _finished = audio.finish().unwrap(); for i in 4..9u64 { live.track.write(video_frame(i * 33_000, i % 3 == 0)).unwrap(); } - live.track.finish().unwrap(); + live.track.end_with(None).unwrap(); record_indexed(&mut export, &mut file, &mut indexes).await; assert!(indexes.len() > 4, "several writes to cut between"); @@ -1324,8 +1324,8 @@ mod tests { live.track.write(video_frame(i * 33_000, i % 3 == 0)).unwrap(); audio.write(raw_frame(i * 20_000, OPUS, i == 0)).unwrap(); } - live.track.finish().unwrap(); - audio.finish().unwrap(); + live.track.end_with(None).unwrap(); + let _finished = audio.finish().unwrap(); let mut export = export(&live).await; let mut file = Vec::new(); record(&mut export, &mut file).await; diff --git a/rs/moq-mux/src/container/producer.rs b/rs/moq-mux/src/container/producer.rs index cfee461effc..6a016ca3481 100644 --- a/rs/moq-mux/src/container/producer.rs +++ b/rs/moq-mux/src/container/producer.rs @@ -1042,6 +1042,12 @@ where self.producer.inner.subscribe(None) } + /// The jitter, bitrate, delay, and framerate measured from every frame written; see + /// [`Producer::estimate`]. + pub fn estimate(&self) -> crate::catalog::Estimate { + self.producer.estimate() + } + /// Release the track's cache, reporting `err` to readers that have not drained it; see /// [`moq_net::track::Finished::abort`]. pub fn abort(self, err: moq_net::Error) { @@ -1116,7 +1122,7 @@ mod tests { for i in 0..80u64 { producer.write(sized_frame(i * 25_000, i % 10 == 0, 5_000)).unwrap(); } - producer.finish().unwrap(); + let producer = producer.finish().unwrap(); let estimate = producer.estimate(); assert_eq!(estimate.jitter, None, "PTS spacing alone is not jitter"); @@ -1269,11 +1275,14 @@ mod tests { /// A finished rendition may be kept around, but its media stops holding the others back. #[test] fn a_finished_rendition_stops_holding_others_back() { - let (_broadcast, catalog, mut tracks) = renditions(&["ended", "live"]); - tracks[0].write(frame(10_000_000, true)).unwrap(); - tracks[0].finish().unwrap(); + let (_broadcast, catalog, tracks) = renditions(&["ended", "live"]); + let Ok([mut ended, mut live]) = <[_; 2]>::try_from(tracks) else { + unreachable!("two renditions") + }; + ended.write(frame(10_000_000, true)).unwrap(); + let _ended = ended.finish().unwrap(); - tracks[1].write(frame(1_000_000, true)).unwrap(); + live.write(frame(1_000_000, true)).unwrap(); assert_eq!(catalog.snapshot().video.renditions["live"].delay, None); } @@ -1390,7 +1399,7 @@ mod tests { producer.discontinuity().unwrap(); // Resumed 40 minutes later, on a re-anchored clock. producer.write(sized_frame(2_405_070_000, true, 5_000)).unwrap(); - producer.finish().unwrap(); + let producer = producer.finish().unwrap(); assert_eq!(producer.estimate().jitter, None); assert_eq!(producer.estimate().bitrate, None); @@ -1486,7 +1495,7 @@ mod tests { producer.discontinuity().unwrap(); // Resumed on a re-anchored clock, 40 minutes later. producer.write(frame(2_405_070_000, true)).unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let groups = collect_payloads(consumer).await; assert_eq!(groups.len(), 3); @@ -1533,7 +1542,7 @@ mod tests { // With no group open, a cut's bound is a point nothing reached. producer.cut(Some(Timestamp::from_micros(90_000).unwrap())).unwrap(); producer.discontinuity().unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let groups = collect_payloads(consumer).await; assert_eq!( @@ -1556,7 +1565,7 @@ mod tests { producer.write(frame(10_000, false)).unwrap(); producer.write(frame(20_000, true)).unwrap(); // keyframe → new group producer.write(frame(30_000, false)).unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); assert_eq!(collect_groups(consumer).await, vec![2, 2]); } @@ -1583,7 +1592,7 @@ mod tests { let keyframe = producer.needs_keyframe(); producer.write(frame(ts, keyframe)).unwrap(); } - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); assert_eq!(collect_groups(consumer).await, vec![3, 2]); } @@ -1615,7 +1624,7 @@ mod tests { .cut(Some(moq_net::Timestamp::from_micros(15_000).unwrap())) .unwrap(); producer.write(frame(20_000, true)).unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let groups = collect_payloads(consumer).await; assert_eq!(groups[0], vec![(0, 2), (10_000, 2), (15_000, 0)]); @@ -1632,7 +1641,7 @@ mod tests { producer.write(frame(0, true)).unwrap(); producer.write(frame(20_000, false)).unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); assert_eq!(collect_payloads(consumer).await, vec![vec![(0, 2), (20_000, 2)]]); } @@ -1664,7 +1673,7 @@ mod tests { .cut(Some(moq_net::Timestamp::from_micros(15_000).unwrap())) .unwrap(); producer.write(frame(20_000, true)).unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let groups = collect_loc_payloads(consumer).await; assert_eq!(groups[0], vec![(0, 2), (10_000, 2), (15_000, 0)]); @@ -1685,7 +1694,7 @@ mod tests { .cut(Some(moq_net::Timestamp::from_micros(30_000).unwrap())) .unwrap(); producer.write(frame(40_000, true)).unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); assert_eq!( collect_loc_payloads(consumer).await, @@ -1705,7 +1714,7 @@ mod tests { .cut(Some(moq_net::Timestamp::from_micros(15_000).unwrap())) .unwrap(); producer.write(frame(20_000, true)).unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); assert_eq!( collect_loc_payloads(consumer).await, @@ -1726,7 +1735,7 @@ mod tests { .cut(Some(moq_net::Timestamp::from_micros(15_000).unwrap())) .unwrap(); producer.write(frame(20_000, true)).unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); assert_eq!(collect_groups(consumer).await, vec![2, 1]); } @@ -1760,7 +1769,7 @@ mod tests { producer.write(frame(0, true)).unwrap(); // seq 0 producer.seek(42).unwrap(); producer.write(frame(10_000, true)).unwrap(); // seq 42 - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); assert_eq!(collect_sequences(consumer).await, vec![0, 42]); } @@ -1775,7 +1784,7 @@ mod tests { producer.seek(5).unwrap(); producer.write(frame(0, true)).unwrap(); // seq 5 producer.write(frame(10_000, true)).unwrap(); // seq 6 (auto-incremented) - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); assert_eq!(collect_sequences(consumer).await, vec![5, 6]); } @@ -1809,7 +1818,7 @@ mod tests { producer.write(frame(0, true)).unwrap(); // group 0 opens producer.write(frame(33_000, false)).unwrap(); // buffered producer.write(frame(66_000, true)).unwrap(); // rolls group 0 over -> flush with next = 66ms - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let writes = recording.0.borrow(); let group0 = &writes[0]; @@ -1843,7 +1852,7 @@ mod tests { producer.write(frame_at(3003, false)).unwrap(); producer.write(frame_at(6006, false)).unwrap(); producer.cut(None).unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let writes = recording.0.borrow(); let group = &writes[0]; @@ -1877,7 +1886,7 @@ mod tests { producer.write(frame(0, true)).unwrap(); producer.write(frame(20_000, false)).unwrap(); producer.write(frame(40_000, false)).unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); assert_eq!(collect_payloads(consumer).await[0].last(), Some(&(60_000, 0))); } @@ -1892,7 +1901,7 @@ mod tests { producer.write(frame(160_000, true)).unwrap(); producer.write(frame(200_000, false)).unwrap(); producer.cut(Some(Timestamp::from_micros(240_000).unwrap())).unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let groups = collect_payloads(consumer).await; assert_eq!(groups[0], vec![(0, 2), (120_000, 2), (40_000, 2), (80_000, 2)]); assert_eq!( @@ -1910,7 +1919,7 @@ mod tests { for (index, timestamp) in [0, 16_000, 32_000, 65_000, 98_000].into_iter().enumerate() { producer.write(frame(timestamp, index == 0)).unwrap(); } - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); assert_eq!(collect_payloads(consumer).await[0].last(), Some(&(131_000, 0))); } @@ -1923,7 +1932,7 @@ mod tests { first.duration = Some(Timestamp::from_micros(40_000).unwrap()); producer.write(first).unwrap(); producer.write(frame(40_000, false)).unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); assert_eq!(collect_payloads(consumer).await[0].last(), Some(&(80_000, 0))); } @@ -1978,7 +1987,7 @@ mod tests { producer.write(frame(166_000, false)).unwrap(); producer.write(frame(150_000, true)).unwrap(); producer.write(frame(183_000, false)).unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let groups = collect_payloads(consumer).await; assert_eq!(groups[1][..2], [(150_000, 2), (183_000, 2)]); } @@ -2012,7 +2021,7 @@ mod tests { for (index, timestamp) in [0, 66_000, 33_000].into_iter().enumerate() { producer.write(frame(timestamp, index == 0)).unwrap(); } - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let groups = collect_payloads(consumer).await; assert_eq!(groups[0][..3], [(0, 2), (66_000, 2), (33_000, 2)]); } @@ -2026,7 +2035,7 @@ mod tests { producer.write(frame(33_000, false)).unwrap(); producer.write(frame(66_000, true)).unwrap(); producer.write(frame(50_000, false)).unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let groups = collect_payloads(consumer).await; assert_eq!(groups[1][0], (66_000, 2)); assert_eq!(groups[1][1], (50_000, 2)); @@ -2042,7 +2051,7 @@ mod tests { producer.discontinuity().unwrap(); producer.write(frame(1_020_000, true)).unwrap(); producer.write(frame(1_040_000, false)).unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let groups = collect_payloads(consumer).await; assert_eq!(groups.last().unwrap().last(), Some(&(1_060_000, 0))); } @@ -2060,7 +2069,7 @@ mod tests { producer.discontinuity().unwrap(); // Resumed 10ms later, inside the 40ms cadence measured before the break. producer.write(frame(1_050_000, true)).unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let mut consumer = crate::container::Consumer::new(subscriber, Container::Legacy(crate::container::Kind::Video)); diff --git a/rs/moq-mux/src/container/ts/export.rs b/rs/moq-mux/src/container/ts/export.rs index f2fca18a1f7..5a16c80ccd2 100644 --- a/rs/moq-mux/src/container/ts/export.rs +++ b/rs/moq-mux/src/container/ts/export.rs @@ -5322,7 +5322,7 @@ mod tests { for at in (40..=end).step_by(40) { publisher.write(frame(at)).unwrap(); } - publisher.finish().unwrap(); + publisher.end_with(None).unwrap(); } let waiter = kio::Waiter::noop(); for index in order { diff --git a/rs/moq-mux/src/container/ts/export_test.rs b/rs/moq-mux/src/container/ts/export_test.rs index a1a637f9631..82bb4625665 100644 --- a/rs/moq-mux/src/container/ts/export_test.rs +++ b/rs/moq-mux/src/container/ts/export_test.rs @@ -189,7 +189,7 @@ async fn custom_selected_catalog_drives_export_and_follow() { keyframe: true, }) .unwrap(); - producer.finish().unwrap(); + let producer = producer.finish().unwrap(); producers.push(producer); } let stream = Once { @@ -326,7 +326,7 @@ async fn export_aac_roundtrip() { .unwrap(); producer.cut(None).unwrap(); } - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); // The producers stay alive so the exporter can subscribe to the catalog and // the finished (retained) track; `drain` stops once all frames are emitted. @@ -406,7 +406,7 @@ async fn export_skips_a_disabled_rendition() { .unwrap(); producer.cut(None).unwrap(); } - producer.finish().unwrap(); + let producer = producer.finish().unwrap(); producer }; let _on = rendition(".on", true, &[0x01, 0x02, 0x03]); @@ -467,7 +467,7 @@ async fn export_packs_aac_frames_up_to_their_buffer() { .unwrap(); producer.cut(None).unwrap(); } - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let ts = drain(consumer).await; let mut pes = PesPacketReader::new(TsPacketReader::new(Cursor::new(ts.as_ref()))); @@ -640,8 +640,8 @@ async fn export_lead_audio() -> BytesMut { audio.write(audio_frame(ms)).unwrap(); audio.cut(None).unwrap(); } - video.finish().unwrap(); - audio.finish().unwrap(); + let _finished = video.finish().unwrap(); + let _finished = audio.finish().unwrap(); let exporter = open_export(crate::source::announced(&consumer).await, RECORDING_MAX_AGE) .await @@ -756,7 +756,7 @@ async fn export_avc3_preserves_multiple_pps() { keyframe: true, }) .unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); // Keep the producers alive (see `export_aac_roundtrip`). let ts = drain(consumer).await; @@ -811,7 +811,7 @@ async fn export_avc1_out_of_band_reassembles() { keyframe: true, }) .unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); // Keep the producers alive (see `export_aac_roundtrip`). let ts = drain(consumer).await; @@ -877,7 +877,7 @@ async fn export_import_h265_keeps_suffix_sei_on_its_picture() { }) .unwrap(); } - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); // Keep the producers alive (see `export_aac_roundtrip`). let ts = drain(consumer).await; @@ -895,7 +895,7 @@ async fn export_import_h265_keeps_suffix_sei_on_its_picture() { let import_catalog = crate::catalog::Producer::new(&mut imported, crate::catalog::Config::default()).unwrap(); let mut import = crate::container::ts::Import::new(imported, import_catalog.reserve()); import.decode(&ts).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let snapshot = import_catalog.snapshot(); let (imported_name, video) = snapshot.video.renditions.iter().next().expect("an H.265 rendition"); @@ -922,7 +922,7 @@ async fn export_bframe_video_authors_dts() { let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = crate::container::ts::Import::new(broadcast, catalog.reserve()); import.decode(&BytesMut::from(&data[..])).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); // `import` and `catalog` stay alive: retained tracks the exporter subscribes to. let ts = drain(consumer).await; @@ -1102,7 +1102,7 @@ async fn export_trims_leading_pictures_after_a_late_drop() { .unwrap(); out.extend(poll_frames(&mut export)); } - video.finish().unwrap(); + let _finished = video.finish().unwrap(); out.extend(drain_frames(&mut export).await); let bytes: Vec = out.iter().flat_map(|frame| frame.payload.iter().copied()).collect(); let (pts, _) = collect_pes_pts(&bytes); @@ -1197,8 +1197,8 @@ async fn export_resumes_whole_mpeg2_video_at_a_sequence_header() { .unwrap(); out.extend(poll_frames(&mut export)); } - audio.finish().unwrap(); - video.finish().unwrap(); + let _finished = audio.finish().unwrap(); + let _finished = video.finish().unwrap(); out.extend(drain_frames(&mut export).await); // Each picture's last byte is its index. @@ -1253,7 +1253,7 @@ async fn export_decodes_where_the_source_did() { let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = crate::container::ts::Import::new(broadcast, catalog.reserve()); import.decode(&BytesMut::from(&data[..])).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let ts = drain(consumer).await; let exported = video_pes_timing(&ts); @@ -1434,7 +1434,7 @@ async fn export_pcr_is_a_uniform_ramp() { let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = crate::container::ts::Import::new(broadcast, catalog.reserve()); import.decode(&BytesMut::from(&data[..])).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let ts = drain(consumer).await; assert_packet_aligned(&ts); @@ -1521,7 +1521,7 @@ async fn export_pcr_wraps_below_zero_at_start() { }) .unwrap(); } - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let mut exporter = open_export(crate::source::announced(&consumer).await, RECORDING_MAX_AGE) .await @@ -1621,8 +1621,8 @@ async fn export_pcr_respects_every_rendition() { .unwrap(); } } - a.finish().unwrap(); - b.finish().unwrap(); + let _finished = a.finish().unwrap(); + let _finished = b.finish().unwrap(); let ts = drain(consumer).await; assert_packet_aligned(&ts); @@ -1696,7 +1696,7 @@ async fn export_pcr_backfills_a_coarse_cadence() { }) .unwrap(); } - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let ts = drain(consumer).await; assert_packet_aligned(&ts); @@ -1788,12 +1788,12 @@ async fn export_scte35_roundtrip() { }) .unwrap(); scte_producer.cut(None).unwrap(); - scte_producer.finish().unwrap(); + let _finished = scte_producer.finish().unwrap(); // Now add the real video/audio by importing bbb.ts (this moves `broadcast`). let mut import = crate::container::ts::Import::new(broadcast, reserved); import.decode(&BytesMut::from(&data[..])).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); // `import`, `catalog`, and `scte_producer` stay alive: retained tracks. The // exporter must carry the extension to see the mpegts section. @@ -1841,7 +1841,7 @@ async fn export_scte35_roundtrip() { .unwrap(); let mut import2 = crate::container::ts::Import::new(broadcast2, catalog2.reserve()); import2.decode(&BytesMut::from(ts.as_ref())).unwrap(); - import2.finish().unwrap(); + let _finished = import2.finish().unwrap(); let snapshot = catalog2.snapshot(); let verbatim = snapshot @@ -1928,14 +1928,14 @@ async fn assert_unknown_stream_type(pes: bool) { keyframe: true, }) .unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); // Import real media to supply the program clock without interpreting the opaque track. let mut import = crate::container::ts::Import::new(broadcast, reserved); import .decode(&BytesMut::from(&include_bytes!("test_data/bbb.ts")[..])) .unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let ts = drain_with( open_ts( crate::source::announced(&consumer).await, @@ -2029,12 +2029,12 @@ async fn export_pes_packet_roundtrip() { }) .unwrap(); data_producer.cut(None).unwrap(); - data_producer.finish().unwrap(); + let _finished = data_producer.finish().unwrap(); // Real video/audio supplies the media clock (moves `broadcast`). let mut import = crate::container::ts::Import::new(broadcast, reserved); import.decode(&BytesMut::from(&data[..])).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); // `import`, `catalog`, and `data_producer` stay alive: retained tracks. let ts = drain_with( @@ -2059,7 +2059,7 @@ async fn export_pes_packet_roundtrip() { .unwrap(); let mut import2 = crate::container::ts::Import::new(broadcast2, catalog2.reserve()); import2.decode(&BytesMut::from(ts.as_ref())).unwrap(); - import2.finish().unwrap(); + let _finished = import2.finish().unwrap(); let snapshot = catalog2.snapshot(); let (name, track) = snapshot @@ -2142,7 +2142,7 @@ async fn programs_without_video_roundtrip_opaque_tracks() { .unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); import.decode(&mux.bytes()).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let original = catalog .snapshot() .ext @@ -2220,7 +2220,7 @@ async fn programs_without_video_roundtrip_opaque_tracks() { .unwrap(); let mut import2 = super::Import::new(broadcast2, catalog2.reserve()); import2.decode(&output).unwrap(); - import2.finish().unwrap(); + let _finished = import2.finish().unwrap(); let restored = catalog2 .snapshot() .ext @@ -2306,7 +2306,7 @@ async fn stats_count_what_the_import_writes_and_the_export_reads() { let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = crate::container::ts::Import::new(broadcast, catalog.reserve()); import.decode(&BytesMut::from(&data[..])).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let snapshot = catalog.snapshot(); let sent = catalog.stats(); @@ -2356,7 +2356,7 @@ async fn mp2_kyrion_roundtrip_byte_exact() { let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = crate::container::ts::Import::new(broadcast, catalog.reserve()); import.decode(&BytesMut::from(&data[..])).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let names: Vec = catalog.snapshot().audio.renditions.keys().cloned().collect(); assert_eq!(names.len(), 2, "both Kyrion MP2 programs"); @@ -2392,7 +2392,7 @@ async fn mp2_kyrion_roundtrip_byte_exact() { let catalog2 = crate::catalog::Producer::new(&mut broadcast2, crate::catalog::Config::default()).unwrap(); let mut import2 = crate::container::ts::Import::new(broadcast2, catalog2.reserve()); import2.decode(&BytesMut::from(ts.as_ref())).unwrap(); - import2.finish().unwrap(); + let _finished = import2.finish().unwrap(); let names2: Vec = catalog2.snapshot().audio.renditions.keys().cloned().collect(); assert_eq!(names2.len(), 2, "round-trip lost an MP2 track"); @@ -2425,7 +2425,7 @@ async fn ac3_roundtrip_byte_exact() { let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = crate::container::ts::Import::new(broadcast, catalog.reserve()); import.decode(&BytesMut::from(&data[..])).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let name = catalog .snapshot() @@ -2469,7 +2469,7 @@ async fn ac3_roundtrip_byte_exact() { let catalog2 = crate::catalog::Producer::new(&mut broadcast2, crate::catalog::Config::default()).unwrap(); let mut import2 = crate::container::ts::Import::new(broadcast2, catalog2.reserve()); import2.decode(&BytesMut::from(ts.as_ref())).unwrap(); - import2.finish().unwrap(); + let _finished = import2.finish().unwrap(); let name2 = catalog2 .snapshot() @@ -2503,7 +2503,7 @@ async fn aac_program_config_roundtrip() { let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = crate::container::ts::Import::new(broadcast, catalog.reserve()); import.decode(&BytesMut::from(&data[..])).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let snapshot = catalog.snapshot(); let (name, audio) = snapshot.audio.renditions.iter().next().expect("an AAC track"); @@ -2527,7 +2527,7 @@ async fn aac_program_config_roundtrip() { let catalog2 = crate::catalog::Producer::new(&mut broadcast2, crate::catalog::Config::default()).unwrap(); let mut import2 = crate::container::ts::Import::new(broadcast2, catalog2.reserve()); import2.decode(&BytesMut::from(ts.as_ref())).unwrap(); - import2.finish().unwrap(); + let _finished = import2.finish().unwrap(); let snapshot2 = catalog2.snapshot(); let (name2, audio2) = snapshot2 @@ -2609,7 +2609,7 @@ async fn aac_program_config_follows_each_table() { write(&mut producer, 0..2_000); producer.discontinuity().unwrap(); write(&mut producer, 2_000..3_000); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let frames = drain_frames(&mut export).await; let ts: Vec = frames.iter().flat_map(|f| f.payload.iter().copied()).collect(); assert_packet_aligned(&ts); @@ -2637,7 +2637,7 @@ async fn aac_program_config_follows_each_table() { let joined_catalog = crate::catalog::Producer::new(&mut joined, crate::catalog::Config::default()).unwrap(); let mut import = crate::container::ts::Import::new(joined, joined_catalog.reserve()); import.decode(&BytesMut::from(&ts[pats[1]..])).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let snapshot = joined_catalog.snapshot(); let (name, audio) = snapshot @@ -2691,7 +2691,7 @@ async fn aac_program_config_leads_a_packed_pes() { .unwrap(); producer.cut(None).unwrap(); } - producer.finish().unwrap(); + producer.end_with(None).unwrap(); } let ts = drain(consumer).await; @@ -2784,8 +2784,8 @@ async fn aac_program_config_follows_a_revision() { frames.extend(poll_frames(&mut export)); write(&mut a, 0, 58..96); write(&mut b, 1_250_000, 0..40); - a.finish().unwrap(); - b.finish().unwrap(); + let _finished = a.finish().unwrap(); + let _finished = b.finish().unwrap(); catalog.finish().unwrap(); frames.extend(drain_frames(&mut export).await); let ts: Vec = frames.iter().flat_map(|f| f.payload.iter().copied()).collect(); @@ -2840,7 +2840,7 @@ async fn aac_explicit_sbr_exports_its_lc_core() { let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = crate::container::flv::Import::new(broadcast, catalog.reserve()); import.decode(data).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let snapshot = catalog.snapshot(); let (name, _) = snapshot.audio.renditions.iter().next().expect("an AAC track"); @@ -2890,7 +2890,7 @@ async fn description_less_aac_refuses_raw_payloads() { keyframe: true, }) .unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let mut exporter = open_export(crate::source::announced(&consumer).await, std::time::Duration::ZERO) .await @@ -2919,7 +2919,7 @@ async fn eac3_roundtrip_byte_exact() { let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = crate::container::ts::Import::new(broadcast, catalog.reserve()); import.decode(&BytesMut::from(&data[..])).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let name = catalog .snapshot() @@ -2966,7 +2966,7 @@ async fn eac3_roundtrip_byte_exact() { let catalog2 = crate::catalog::Producer::new(&mut broadcast2, crate::catalog::Config::default()).unwrap(); let mut import2 = crate::container::ts::Import::new(broadcast2, catalog2.reserve()); import2.decode(&BytesMut::from(ts.as_ref())).unwrap(); - import2.finish().unwrap(); + let _finished = import2.finish().unwrap(); let name2 = catalog2 .snapshot() @@ -3006,7 +3006,7 @@ async fn kyrion_ac3_mp2_roundtrip_byte_exact() { let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = crate::container::ts::Import::new(broadcast, catalog.reserve()); import.decode(&BytesMut::from(&data[..])).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let ingested = read_audio_by_codec(&consumer, &catalog).await; assert_eq!( @@ -3054,7 +3054,7 @@ async fn kyrion_ac3_mp2_roundtrip_byte_exact() { let catalog2 = crate::catalog::Producer::new(&mut broadcast2, crate::catalog::Config::default()).unwrap(); let mut import2 = crate::container::ts::Import::new(broadcast2, catalog2.reserve()); import2.decode(&BytesMut::from(ts.as_ref())).unwrap(); - import2.finish().unwrap(); + let _finished = import2.finish().unwrap(); let roundtripped = read_audio_by_codec(&consumer2, &catalog2).await; assert_eq!(roundtripped, ingested, "both audio streams survive byte-for-byte"); @@ -3156,7 +3156,7 @@ async fn scte35_fixtures_survive_roundtrip() { .unwrap(); let mut import = crate::container::ts::Import::new(broadcast, catalog.reserve()); import.decode(&BytesMut::from(&data[..])).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let snap = catalog.snapshot(); assert!(!snap.video.renditions.is_empty(), "{source}: video track from the clip"); @@ -3216,7 +3216,7 @@ async fn scte35_fixtures_survive_roundtrip() { .unwrap(); let mut import2 = crate::container::ts::Import::new(broadcast2, catalog2.reserve()); import2.decode(&BytesMut::from(ts.as_ref())).unwrap(); - import2.finish().unwrap(); + let _finished = import2.finish().unwrap(); let name2 = scte_track(&catalog2.snapshot()).expect("a scte35 track"); let roundtripped = read_cues(&consumer2, &name2).await; @@ -3385,7 +3385,7 @@ async fn service_layer_survives_roundtrip() { .unwrap(); let mut import = crate::container::ts::Import::new(broadcast, catalog.reserve()); import.decode(&BytesMut::from(&input[..])).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let snapshot = catalog.snapshot(); let program = snapshot.ext.mpegts.program.clone().expect("a program record"); @@ -3481,7 +3481,7 @@ async fn service_layer_survives_roundtrip() { .unwrap(); let mut import2 = crate::container::ts::Import::new(broadcast2, catalog2.reserve()); import2.decode(&BytesMut::from(ts.as_ref())).unwrap(); - import2.finish().unwrap(); + let import2 = import2.finish().unwrap(); // Its row, if any, is the SI track's, never an unclaimed PID's. if let Some(nit) = import2.stats().streams.get(&0x0010) { assert_eq!( @@ -3535,7 +3535,7 @@ async fn a_reimport_keeps_the_first_si() { .unwrap(); let mut import = crate::container::ts::Import::new(broadcast, catalog.reserve()); import.decode(&BytesMut::from(&input[..])).unwrap(); - import.finish().unwrap(); + let import = import.finish().unwrap(); let units = |stats: super::stats::Snapshot| { stats .streams @@ -3589,7 +3589,7 @@ async fn a_reimport_keeps_the_first_si() { .unwrap(); let mut import2 = crate::container::ts::Import::new(broadcast2, catalog2.reserve()); import2.decode(&BytesMut::from(ts.as_ref())).unwrap(); - import2.finish().unwrap(); + let import2 = import2.finish().unwrap(); let stats = import2.stats(); for (pid, stream) in &stats.streams { assert_eq!( @@ -3675,7 +3675,7 @@ async fn a_reimport_keeps_the_first_si_across_a_gap() { }) .unwrap(); } - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); // No delay: each frame settles the slots before it, so the gap is skipped at once. let mut export = open_ts( @@ -3721,7 +3721,7 @@ async fn a_reimport_keeps_the_first_si_across_a_gap() { .unwrap(); let mut import = crate::container::ts::Import::new(broadcast2, catalog2.reserve()); import.decode(&BytesMut::from(ts.as_ref())).unwrap(); - import.finish().unwrap(); + let import = import.finish().unwrap(); for (pid, stream) in &import.stats().streams { assert_eq!( stream.unplaced, 0, @@ -3817,7 +3817,7 @@ async fn si_pids_are_re_emitted_on_their_own_interval() { .unwrap(); producer.cut(None).unwrap(); } - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let ts = drain_with( open_ts( @@ -3907,7 +3907,7 @@ async fn inline_si_form_is_re_emitted() { .unwrap(); producer.cut(None).unwrap(); } - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let ts = drain_with( open_ts( @@ -4503,7 +4503,7 @@ async fn multi_packet_si_section_is_captured() { let mut rig = si_rig(); rig.import.decode(&BytesMut::from(&input[..])).unwrap(); - rig.import.finish().unwrap(); + let _finished = rig.import.finish().unwrap(); let si = rig.catalog.snapshot().ext.mpegts.si.clone(); let entry = si @@ -4583,7 +4583,7 @@ async fn torn_transition_is_never_published() { rig.import .decode(&BytesMut::from(&si_packet_cc(0x0011, &sdt(6, 1, 0xbb), 3)[..])) .unwrap(); - rig.import.finish().unwrap(); + let _finished = rig.import.finish().unwrap(); snapshots.push(newest_si_group(&rig.consumer, track).await); for (sequence, frames) in &snapshots { @@ -4622,7 +4622,7 @@ async fn eit_now_next_and_schedule_are_captured() { } let mut rig = si_rig(); rig.import.decode(&BytesMut::from(&input[..])).unwrap(); - rig.import.finish().unwrap(); + let _finished = rig.import.finish().unwrap(); let si = rig.catalog.snapshot().ext.mpegts.si.clone(); let eit = si.get(&0x0012).expect("EIT entries"); @@ -4743,7 +4743,7 @@ async fn a_selected_program_carries_only_its_own_si() { input.extend(two_service_si()); let mut rig = si_rig_selecting(program); rig.import.decode(&BytesMut::from(&input[..])).unwrap(); - rig.import.finish().unwrap(); + let _finished = rig.import.finish().unwrap(); let si = rig.catalog.snapshot().ext.mpegts.si.clone(); let sdt = read_si_sections(&rig.consumer, &si[&0x0011][&0x42].track).await; @@ -4767,7 +4767,7 @@ async fn a_selected_program_carries_only_its_own_si() { async fn an_unselected_import_keeps_every_service() { let mut rig = si_rig(); rig.import.decode(&BytesMut::from(&two_service_si()[..])).unwrap(); - rig.import.finish().unwrap(); + let _finished = rig.import.finish().unwrap(); let si = rig.catalog.snapshot().ext.mpegts.si.clone(); assert_eq!( @@ -4791,7 +4791,7 @@ async fn an_unselected_import_keeps_every_service() { async fn a_selection_missing_from_the_sdt_carries_none() { let mut rig = si_rig_selecting(3); rig.import.decode(&BytesMut::from(&two_service_si()[..])).unwrap(); - rig.import.finish().unwrap(); + let _finished = rig.import.finish().unwrap(); let si = rig.catalog.snapshot().ext.mpegts.si.clone(); assert!(si.contains_key(&0x0010), "the NIT was captured (control)"); @@ -4988,7 +4988,7 @@ async fn a_selected_programs_si_survives_export() { input.extend(two_service_si()); let mut rig = si_rig_selecting(2); rig.import.decode(&BytesMut::from(&input[..])).unwrap(); - rig.import.finish().unwrap(); + let _finished = rig.import.finish().unwrap(); let ts = drain_with( open_ts( @@ -5043,7 +5043,7 @@ async fn sdt_other_networks_do_not_collide() { input.extend_from_slice(&si_packet_cc(0x0011, &net1v2, 2)); let mut rig = si_rig(); rig.import.decode(&BytesMut::from(&input[..])).unwrap(); - rig.import.finish().unwrap(); + let _finished = rig.import.finish().unwrap(); let si = rig.catalog.snapshot().ext.mpegts.si.clone(); let entry = &si[&0x0011][&0x46]; @@ -5070,7 +5070,7 @@ async fn next_version_sections_are_dropped() { input.extend_from_slice(&si_packet_cc(0x0010, &nit, 1)); let mut rig = si_rig(); rig.import.decode(&BytesMut::from(&input[..])).unwrap(); - rig.import.finish().unwrap(); + let _finished = rig.import.finish().unwrap(); let si = rig.catalog.snapshot().ext.mpegts.si.clone(); assert!(si.contains_key(&0x0010), "the current NIT was captured (control)"); @@ -5096,7 +5096,7 @@ async fn tdt_round_trips_as_latest_value() { rig.import .decode(&BytesMut::from(&si_packet_cc(0x0014, &tick(2), 2)[..])) .unwrap(); - rig.import.finish().unwrap(); + let _finished = rig.import.finish().unwrap(); let si = rig.catalog.snapshot().ext.mpegts.si.clone(); assert!(si.contains_key(&0x0011), "the SDT was captured (control)"); @@ -5162,7 +5162,7 @@ async fn short_form_repetition_cuts_no_group() { rig.import .decode(&BytesMut::from(&si_packet_cc(0x0014, &tdt, 2)[..])) .unwrap(); - rig.import.finish().unwrap(); + let _finished = rig.import.finish().unwrap(); let si = rig.catalog.snapshot().ext.mpegts.si.clone(); let (sequence, _) = newest_si_group(&rig.consumer, &si[&0x0014][&0x70].track).await; @@ -5226,7 +5226,7 @@ async fn stale_si_entry_does_not_block_output() { }) .unwrap(); producer.cut(None).unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let mut exporter = open_ts( crate::source::announced(&consumer).await, @@ -5323,7 +5323,7 @@ async fn export_opus_roundtrip() { .unwrap(); producer.cut(None).unwrap(); } - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let ts = drain(consumer).await; assert_packet_aligned(&ts); @@ -5402,7 +5402,7 @@ async fn opus_export_import_roundtrip() { .unwrap(); producer.cut(None).unwrap(); } - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let ts = drain(consumer).await; @@ -5412,7 +5412,7 @@ async fn opus_export_import_roundtrip() { let import_catalog = crate::catalog::Producer::new(&mut imported, crate::catalog::Config::default()).unwrap(); let mut import = crate::container::ts::Import::new(imported, import_catalog.reserve()); import.decode(&ts).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let snapshot = import_catalog.snapshot(); assert_eq!(snapshot.audio.renditions.len(), 1, "expected one Opus track"); @@ -5479,7 +5479,7 @@ async fn export_opus_track(channel_count: u32, description: Option) -> Re keyframe: true, }) .unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let mut exporter = open_export(crate::source::announced(&consumer).await, RECORDING_MAX_AGE) .await @@ -6034,8 +6034,8 @@ impl Interleave { } fn finish(&mut self) { - self.video.finish().unwrap(); - self.audio.finish().unwrap(); + self.video.end_with(None).unwrap(); + self.audio.end_with(None).unwrap(); } } @@ -6481,7 +6481,7 @@ async fn lost_dense_section_recovers_on_the_next_cycle() { input.extend_from_slice(&si_packet_cc(0x0011, &s1, 4)); let mut rig = si_rig(); rig.import.decode(&BytesMut::from(&input[..])).unwrap(); - rig.import.finish().unwrap(); + let _finished = rig.import.finish().unwrap(); let si = rig.catalog.snapshot().ext.mpegts.si.clone(); assert_eq!( @@ -6611,7 +6611,7 @@ async fn repointed_si_entry_resubscribes() { } write_key(&mut producer, 3); write_key(&mut producer, 4); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); while let Ok(res) = kio::clock::timeout(DRAIN, exporter.next()).await { let Some(frame) = res.unwrap() else { break }; after.extend_from_slice(&frame.payload); @@ -6690,7 +6690,7 @@ async fn si_revision_after_final_media_frame_is_flushed() { }) .unwrap(); producer.cut(None).unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let mut exporter = open_ts( crate::source::announced(&consumer).await, @@ -6971,7 +6971,7 @@ async fn clock_revisions_wait_for_their_stamped_grid_slots() { rig.si_track .write_frame(Timestamp::from_millis(6_500).unwrap(), Bytes::from(section(3))) .unwrap(); - rig.producer.finish().unwrap(); + let _finished = rig.producer.finish().unwrap(); let _si_track = rig.si_track.finish().unwrap(); rig.catalog.finish().unwrap(); assert_eq!(count_pid(&drain_frames(&mut rig.exporter).await, 0x0014), 0); @@ -7261,7 +7261,7 @@ async fn contiguous_same_version_commit_replaces_stale_sections() { input.extend_from_slice(&si_packet_cc(0x0011, &b1, 4)); let mut rig = si_rig(); rig.import.decode(&BytesMut::from(&input[..])).unwrap(); - rig.import.finish().unwrap(); + let _finished = rig.import.finish().unwrap(); let si = rig.catalog.snapshot().ext.mpegts.si.clone(); assert_eq!( @@ -7403,7 +7403,7 @@ async fn export_cbr_video() -> Vec { }) .unwrap(); } - video.finish().unwrap(); + let _finished = video.finish().unwrap(); let mut exporter = open_export(crate::source::announced(&consumer).await, RECORDING_MAX_AGE) .await @@ -7468,7 +7468,7 @@ async fn a_frame_on_a_slot_boundary_finishes_before_it_decodes() { }) .unwrap(); } - video.finish().unwrap(); + let _finished = video.finish().unwrap(); let mut exporter = open_export(crate::source::announced(&consumer).await, std::time::Duration::ZERO) .await @@ -7602,7 +7602,7 @@ async fn slices_go_out_on_the_clock() { } kio::clock::sleep(Duration::from_secs(1)).await; } - audio.finish().unwrap(); + let _finished = audio.finish().unwrap(); (broadcast, catalog) }); let mut handed = Vec::new(); @@ -7634,7 +7634,7 @@ async fn pcr_stays_among_the_bytes_across_reordered_tracks() { let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = crate::container::ts::Import::new(broadcast, catalog.reserve()); import.decode(&BytesMut::from(&data[..])).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let mut exporter = open_export(crate::source::announced(&consumer).await, RECORDING_MAX_AGE) .await @@ -7759,7 +7759,7 @@ async fn a_heavy_passage_fills_the_slots_before_it() { }) .unwrap(); } - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let mut export = open_export(crate::source::announced(&consumer).await, Duration::from_millis(500)) .await @@ -7855,7 +7855,7 @@ async fn export_fixture(data: &[u8], mux_rate: Option) -> BytesMut { .unwrap(); let mut import = crate::container::ts::Import::new(broadcast, catalog.reserve()); import.decode(data).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let mut export = export_of(&consumer).await; if let Some(mux_rate) = mux_rate { @@ -7950,7 +7950,7 @@ async fn export_refuses_an_absurd_catalog_mux_rate() { }) .unwrap(); } - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let ts = drain_with(export_of(&consumer).await).await; assert_packet_aligned(&ts); @@ -8004,7 +8004,7 @@ async fn export_pcr_is_its_byte_position_at_the_rate() { }) .unwrap(); } - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let export = open_export(crate::source::announced(&consumer).await, RECORDING_MAX_AGE) .await @@ -8168,8 +8168,8 @@ impl Reordered { } fn finish(&mut self) { - self.video.finish().unwrap(); - self.audio.finish().unwrap(); + self.video.end_with(None).unwrap(); + self.audio.end_with(None).unwrap(); } } @@ -8723,7 +8723,7 @@ async fn a_late_join_starts_at_the_live_edge() { out.extend(poll_frames(export)); } } - audio.finish().unwrap(); + let _finished = audio.finish().unwrap(); let mut export = export.unwrap(); out.extend(drain_frames(&mut export).await); @@ -8858,8 +8858,8 @@ async fn moved_export( frames.extend(drain_frames(&mut export).await); } match broadcast { - None => moving.b.finish().unwrap(), - Some(_) => moving.other.finish().unwrap(), + None => moving.b.end_with(None).unwrap(), + Some(_) => moving.other.end_with(None).unwrap(), } moving.catalog.finish().unwrap(); let (rest, end) = drain_to_end(&mut export).await; @@ -8969,11 +8969,11 @@ async fn a_track_leaving_the_catalog_is_read_to_its_end() { write_aac(&mut kept, ms); write_aac(&mut leaving, ms); } - leaving.finish().unwrap(); + let _finished = leaving.finish().unwrap(); for ms in (300..400).step_by(20) { write_aac(&mut kept, ms); } - kept.finish().unwrap(); + let _finished = kept.finish().unwrap(); catalog.finish().unwrap(); let (rest, end) = drain_to_end(&mut export).await; @@ -9095,7 +9095,7 @@ async fn same_instance_returns(finish: bool) { } let mut frames = drain_frames(&mut export).await; if finish { - track.finish().unwrap(); + track.end_with(None).unwrap(); catalog.finish().unwrap(); } drop((broadcast, catalog, track)); @@ -9125,7 +9125,7 @@ async fn same_instance_returns(finish: bool) { write_aac(&mut track, ms); } frames.extend(drain_frames(&mut export).await); - track.finish().unwrap(); + let _finished = track.finish().unwrap(); catalog.finish().unwrap(); let (rest, end) = drain_to_end(&mut export).await; frames.extend(rest); @@ -9207,7 +9207,7 @@ async fn a_return_keeps_catalog_and_group_history() { 10 + sequence as u64 ); } - track.finish().unwrap(); + let _finished = track.finish().unwrap(); catalog.finish().unwrap(); let (rest, end) = drain_to_end(&mut export).await; frames.extend(rest); @@ -9319,8 +9319,8 @@ async fn another_instance_is_a_full_program_switch() { write_aac(&mut new_audio, ms); } let mut after = drain_frames(&mut export).await; - video.finish().unwrap(); - new_audio.finish().unwrap(); + let _finished = video.finish().unwrap(); + let _finished = new_audio.finish().unwrap(); new_catalog.finish().unwrap(); let (rest, end) = drain_to_end(&mut export).await; after.extend(rest); @@ -9421,7 +9421,7 @@ async fn a_switch_moving_the_pmt_advances_the_pat() { write_aac(&mut new_track, ms); } let mut after = drain_frames(&mut export).await; - new_track.finish().unwrap(); + let _finished = new_track.finish().unwrap(); new_catalog.finish().unwrap(); let (rest, end) = drain_to_end(&mut export).await; after.extend(rest); @@ -9483,7 +9483,7 @@ async fn export_stats_count_only_returned_output() { write_aac(&mut track, ms); } frames.extend(drain_frames(&mut export).await); - track.finish().unwrap(); + let _finished = track.finish().unwrap(); catalog.finish().unwrap(); let (rest, end) = drain_to_end(&mut export).await; frames.extend(rest); @@ -9724,7 +9724,7 @@ async fn large_pmt(streams: u16, mux_rate: Option) -> usize { payload: Bytes::from_static(b"aac"), }) .unwrap(); - producer.finish().unwrap(); + let producer = producer.finish().unwrap(); producers.push(producer); } catalog.modify().unwrap().ext.mpegts = config; @@ -10235,7 +10235,7 @@ async fn a_return_after_an_abandoned_switch_restores_priority() { // The old program ends mid-switch. The replacement goes without serving its catalog, // allowing the older instance to serve again. - old_audio.finish().unwrap(); + let _finished = old_audio.finish().unwrap(); finish((old, old_catalog)); pump(&mut follower).await; drop((replacement, unanswered)); @@ -10278,7 +10278,7 @@ async fn a_follower_follows_a_return_after_a_lost_route() { catalog.abort(moq_net::Error::Unroutable); drop(broadcast); let (broadcast, mut catalog) = publish_bare(&origin, Some(&instance)); - let mut track = aac_rendition(&mut broadcast.broadcast().clone(), &mut catalog, "a.aac"); + let track = aac_rendition(&mut broadcast.broadcast().clone(), &mut catalog, "a.aac"); assert!( kio::clock::timeout(Duration::from_secs(3), follower.next()) .await @@ -10287,7 +10287,7 @@ async fn a_follower_follows_a_return_after_a_lost_route() { ); assert!(follower.export().cataloged(), "the return served its catalog"); - track.finish().unwrap(); + let _finished = track.finish().unwrap(); finish((broadcast, catalog)); let end = follower.next().await; assert!(matches!(end, Ok(None)), "{end:?}"); @@ -10508,7 +10508,7 @@ async fn a_follower_continues_through_a_same_instance_handoff_after_a_gap() { for ms in (resumed..resumed + 200).step_by(20) { write_aac(&mut track, ms); } - track.finish().unwrap(); + let _finished = track.finish().unwrap(); catalog.finish().unwrap(); let (rest, end) = drain_follower(&mut follower).await; frames.extend(rest); @@ -11184,10 +11184,10 @@ async fn export_placed(placed: Placed) -> crate::Result> { .unwrap(); } if let Some(video) = video.as_mut() { - video.finish().unwrap(); + video.end_with(None).unwrap(); } - audio.finish().unwrap(); - data.finish().unwrap(); + let _finished = audio.finish().unwrap(); + let _finished = data.finish().unwrap(); let mut exporter = open_ts( crate::source::announced(&consumer).await, @@ -11543,7 +11543,7 @@ async fn placed_sections_keep_their_source_positions() { .unwrap(); let mut import = crate::container::ts::Import::new(broadcast, catalog.reserve()); import.decode(data).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let name = scte_track(&catalog.snapshot()).unwrap(); let cues = read_cues(&consumer, &name).await; assert_eq!( @@ -11761,7 +11761,7 @@ async fn a_switch_follows_the_last_pmt_out() { write_aac(&mut audio, ms); } frames.extend(drain_frames(&mut export).await); - audio.finish().unwrap(); + let _finished = audio.finish().unwrap(); new_catalog.finish().unwrap(); let (rest, end) = drain_to_end(&mut export).await; frames.extend(rest); @@ -11809,7 +11809,7 @@ async fn a_switch_follows_the_last_pat_out() { write_aac(&mut audio, ms); } frames.extend(drain_frames(&mut export).await); - audio.finish().unwrap(); + let _finished = audio.finish().unwrap(); new_catalog.finish().unwrap(); let (rest, end) = drain_to_end(&mut export).await; frames.extend(rest); @@ -11863,7 +11863,7 @@ async fn a_switch_past_an_instance_that_laid_no_tables_follows_the_last_pmt_out( write_aac(&mut audio, ms); } frames.extend(drain_frames(&mut export).await); - audio.finish().unwrap(); + let _finished = audio.finish().unwrap(); third_catalog.finish().unwrap(); let (rest, end) = drain_to_end(&mut export).await; frames.extend(rest); @@ -12343,7 +12343,7 @@ async fn a_newer_pending_instance_discards_the_unemitted_held_replacement() { .await .unwrap(); let mut export = export.follow(replacement, catalog).unwrap(); - c_audio.finish().unwrap(); + let _finished = c_audio.finish().unwrap(); c_catalog.finish().unwrap(); let (rest, end) = drain_to_end(&mut export).await; out.extend(rest); @@ -12434,10 +12434,10 @@ async fn cold_same_instance_return(pending: bool) { &origin, moq_net::origin::RouteOptions::default().with_instance(next_instance.clone(), 0), ); - let mut video = h264_rendition(&mut cold.broadcast().clone(), &mut catalog, "v.avc1"); - let mut audio = aac_rendition(&mut cold.broadcast().clone(), &mut catalog, "a.aac"); - video.finish().unwrap(); - audio.finish().unwrap(); + let video = h264_rendition(&mut cold.broadcast().clone(), &mut catalog, "v.avc1"); + let audio = aac_rendition(&mut cold.broadcast().clone(), &mut catalog, "a.aac"); + let _finished = video.finish().unwrap(); + let _finished = audio.finish().unwrap(); catalog.finish().unwrap(); let target = origin .consume() @@ -12457,8 +12457,8 @@ async fn cold_same_instance_return(pending: bool) { cold.withdraw(); next_instance } else { - old_video.finish().unwrap(); - old_audio.finish().unwrap(); + let _finished = old_video.finish().unwrap(); + let _finished = old_audio.finish().unwrap(); old_catalog.finish().unwrap(); assert!(export.next().await.unwrap().is_none()); old.withdraw(); @@ -12488,8 +12488,8 @@ async fn cold_same_instance_return(pending: bool) { for ms in (0..400).step_by(20) { write_aac(&mut audio, ms); } - video.finish().unwrap(); - audio.finish().unwrap(); + let _finished = video.finish().unwrap(); + let _finished = audio.finish().unwrap(); catalog.finish().unwrap(); let (out, end) = drain_to_end(&mut export).await; end.unwrap(); @@ -12532,7 +12532,7 @@ async fn fixed_delay_keeps_buffered_pes_ahead_of_the_clock() { let mut all = vec![export.next().await.unwrap().unwrap()]; assert!(export.buffered() > 1, "several PES remain buffered"); - audio.finish().unwrap(); + let _finished = audio.finish().unwrap(); catalog.finish().unwrap(); let (frames, end) = drain_to_end(&mut export).await; end.unwrap(); diff --git a/rs/moq-mux/src/container/ts/export_timing_test.rs b/rs/moq-mux/src/container/ts/export_timing_test.rs index 7a12cbb967d..8eaf6249cca 100644 --- a/rs/moq-mux/src/container/ts/export_timing_test.rs +++ b/rs/moq-mux/src/container/ts/export_timing_test.rs @@ -362,7 +362,7 @@ impl Live { } self.stalled.clear(); drop(self.video.clone().finish().unwrap()); - self.audio.finish().unwrap(); + self.audio.end_with(None).unwrap(); } /// Run the clock to `at`, letting every leg read whenever it would have on its own. diff --git a/rs/moq-mux/src/container/ts/health.rs b/rs/moq-mux/src/container/ts/health.rs index 99e790f92cd..0355e809545 100644 --- a/rs/moq-mux/src/container/ts/health.rs +++ b/rs/moq-mux/src/container/ts/health.rs @@ -1284,7 +1284,7 @@ mod test { for packet in CLIP[half..].chunks(188) { import.decode(packet).unwrap(); } - import.finish().unwrap(); + let import = import.finish().unwrap(); assert_eq!(import.stats().health(), [0; 9]); } @@ -1389,7 +1389,7 @@ mod test { Resume::Stalled => feed.run(100, 200, |t, _| t * MS, |_, part| part != Part::Pcr), } import.decode(&std::mem::take(&mut feed.out)).unwrap(); - import.finish().unwrap(); + let import = import.finish().unwrap(); let stats = import.stats(); let (pat, pts, pcr) = match resume { Resume::Eof | Resume::Delayed => (0, 0, 0), @@ -1427,7 +1427,7 @@ mod test { assert!(import.held() >= 188, "the step is held"); wait(200).await; if eof { - import.finish().unwrap(); + import.finish_in_place().unwrap(); } else { // The streams carry on 200 ms later without a PCR, clearing the step: the PCR // is missing from when the held one arrived. diff --git a/rs/moq-mux/src/container/ts/import.rs b/rs/moq-mux/src/container/ts/import.rs index 641b3e8599b..889b756470e 100644 --- a/rs/moq-mux/src/container/ts/import.rs +++ b/rs/moq-mux/src/container/ts/import.rs @@ -5085,7 +5085,7 @@ pub(super) mod test { }); import.damage(pid, &error).unwrap(); import.damage(pid, &error).unwrap(); - import.finish().unwrap(); + import.finish_in_place().unwrap(); assert_eq!(import.stats().streams[&0x61].damaged, 5); drop(import); logs_assert(|lines| { @@ -5430,7 +5430,7 @@ pub(super) mod test { bytes.extend_from_slice(&pes_packet(PCR_PID, 90_000)); bytes.extend_from_slice(&pcr_packet(PCR_PID, 27_000_000)); import.decode(&bytes).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); assert_eq!( cue_tracks(&catalog.snapshot().ext.mpegts), @@ -5483,7 +5483,7 @@ pub(super) mod test { bytes.extend_from_slice(&pes_packet(PCR_PID, 90_000)); bytes.extend_from_slice(&pcr_packet(PCR_PID, 27_000_000)); import.decode(&bytes).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let first = kio::clock::timeout(Duration::from_secs(10), updates.next()) .await @@ -5523,7 +5523,7 @@ pub(super) mod test { ), "the CUEI PID routes to Ignored" ); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); // SCTE detection takes no lock here (video/audio would still publish later): the old // discarding ScteStream took the lock and republished an empty catalog on this path. assert!( @@ -5598,7 +5598,7 @@ pub(super) mod test { // The importer clears its verbatim entries from the catalog when it drops, so read the // track name while it is still registered. let name = track_on(&catalog.snapshot().ext.mpegts, SECTION_PID); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let track = consumer .track(&name) .unwrap() @@ -5900,7 +5900,7 @@ pub(super) mod test { } } import.decode(&bytes).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let exporter = crate::container::ts::export_test::open_ts( crate::source::announced(&consumer).await, @@ -6086,7 +6086,7 @@ pub(super) mod test { .decode(&audio_pes_packet(PID, ((i + 1) % 16) as u8, pts, &frame)) .unwrap(); } - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let snapshot = catalog.snapshot(); let config = snapshot.audio.renditions.values().next().unwrap(); assert!(config.bitrate.is_some(), "exercise a bitrate refinement"); @@ -6197,7 +6197,7 @@ pub(super) mod test { } import.decode(&bytes).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let snap = catalog.snapshot(); let aac = snap.audio.renditions.values().next().expect("an AAC rendition"); @@ -6252,7 +6252,7 @@ pub(super) mod test { bytes.extend_from_slice(&audio_pes_packet(AAC_PID, 0, 270_000, &aac)); import.decode(&bytes).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let snap = catalog.snapshot(); assert_eq!(snap.audio.renditions.len(), 2, "AAC and MP2 renditions"); @@ -6333,7 +6333,7 @@ pub(super) mod test { looped.ends_with(&restart.remainder) && restart.remainder.len() <= data.len(), "the new instance starts at the wrap" ); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let frames = read_audio_frames(&consumer, &catalog).await; assert!(!frames.is_empty(), "the pass before the wrap published"); @@ -6414,7 +6414,7 @@ pub(super) mod test { import .decode(audio_pes_packet(VIDEO_PID, 14, 450_000, &annexb_au(false)).as_slice()) .unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let frames = read_video_frames(&consumer, &catalog).await; assert!( @@ -6463,7 +6463,7 @@ pub(super) mod test { import .decode(audio_pes_packet(AAC_PID, 13, 270_000, &normal).as_slice()) .unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let frames = read_audio_frames(&consumer, &catalog).await; assert_eq!( @@ -6501,7 +6501,7 @@ pub(super) mod test { import .decode(audio_pes_packet(AAC_PID, 1, 270_000, &rest).as_slice()) .unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let frames = read_audio_frames(&consumer, &catalog).await; assert_eq!(frames.len(), 2, "the split frame is reassembled, not dropped"); @@ -6542,7 +6542,7 @@ pub(super) mod test { import .decode(audio_pes_packet(AAC_PID, 2, 450_000, &adts_frame(40, 0xDD)).as_slice()) .unwrap(); - import.finish().unwrap(); + import.finish_in_place().unwrap(); // The track carries raw AAC, so each published frame is its ADTS body. let frames = read_audio_frames(&consumer, &catalog).await; @@ -6603,7 +6603,7 @@ pub(super) mod test { import .decode(audio_pes_packet(AAC_PID, 2, 450_000, &adts_frame(40, 0xEE)).as_slice()) .unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); // The track carries raw AAC, so each published frame is its ADTS body. let frames = read_audio_frames(&consumer, &catalog).await; @@ -6645,7 +6645,7 @@ pub(super) mod test { import .decode(audio_pes_packet(AAC_PID, 1, 270_000, &split[25..]).as_slice()) .unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let frames = read_audio_frames(&consumer, &catalog).await; assert_eq!( @@ -6701,7 +6701,7 @@ pub(super) mod test { tokio::time::advance(Duration::from_millis(440) / (packets.len() - 1) as u32).await; import.decode(packet).unwrap(); } - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let name = catalog.snapshot().audio.renditions.keys().next().unwrap().clone(); let frames = read_track(&consumer, &name, crate::container::Kind::Audio).await; assert_eq!( @@ -6747,7 +6747,7 @@ pub(super) mod test { .decode(&audio_pes_open(PID, 1, 180_000, rest.len(), &rest[..20])) .unwrap(); import.decode(&ts_continuation(PID, 2, &rest[20..])).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let frames = read_audio_frames(&consumer, &catalog).await; assert_eq!( frames @@ -6784,7 +6784,7 @@ pub(super) mod test { ] { import.decode(&audio_pes_packet(PID, cc, pts, &payload)).unwrap(); } - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let frames = read_audio_frames(&consumer, &catalog).await; assert_eq!( frames @@ -6830,7 +6830,7 @@ pub(super) mod test { .decode(&audio_pes_open(PID, 2, 135_000, 20 + rest.len(), &b[40..60])) .unwrap(); import.decode(&ts_continuation(PID, 3, &rest)).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let frames = read_audio_frames(&consumer, &catalog).await; let step = if stream_type == StreamType::AdtsAac { 21_333 @@ -6878,7 +6878,7 @@ pub(super) mod test { .decode(&ts_continuation(PID, 1, &[junk, recovered].concat())) .unwrap(); } - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let frames = read_audio_frames(&consumer, &catalog).await; assert_eq!(frames.len(), 2, "{stream_type:?} carry={carry}"); let step = if stream_type == StreamType::AdtsAac { @@ -6930,7 +6930,7 @@ pub(super) mod test { let (name, config) = snapshot.audio.renditions.iter().next().unwrap(); assert_ne!(name, &first, "the stereo frames publish under a new name"); assert_eq!(config.channel_count, 2); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let frames = read_audio_frames(&consumer, &catalog).await; assert_eq!(frames[0].payload, stereo(0xCC), "the new track starts at the change"); } @@ -6990,7 +6990,7 @@ pub(super) mod test { vec![name.clone()], "the PID's mpegts entry moves to the new track" ); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let frames = read_audio_frames(&consumer, &catalog).await; assert_eq!( frames[0].timestamp, @@ -7030,7 +7030,7 @@ pub(super) mod test { import .decode(audio_pes_packet(MP2_PID, 13, 270_000, &normal).as_slice()) .unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let frames = read_audio_frames(&consumer, &catalog).await; assert_eq!( @@ -7074,7 +7074,7 @@ pub(super) mod test { import .decode(audio_pes_packet(MP2_PID, 2, 450_000, &mp2_frame(0xDD)).as_slice()) .unwrap(); - import.finish().unwrap(); + import.finish_in_place().unwrap(); let frames = read_audio_frames(&consumer, &catalog).await; assert_eq!( @@ -7134,7 +7134,7 @@ pub(super) mod test { import .decode(audio_pes_packet(MP2_PID, 2, 450_000, &mp2_frame(0xEE)).as_slice()) .unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let frames = read_audio_frames(&consumer, &catalog).await; // The stale tail still carries an intact header claiming 72 bytes, so what gives the @@ -7189,7 +7189,7 @@ pub(super) mod test { import .decode(audio_pes_packet(MP2_PID, 2, 450_000, &mp2_frame(0xEE)).as_slice()) .unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let frames = read_audio_frames(&consumer, &catalog).await; assert_eq!( @@ -7230,7 +7230,7 @@ pub(super) mod test { join.extend_from_slice(&mp2_frame(0xEE)); let err = import .decode(audio_pes_packet(MP2_PID, 1, 89_910, &join).as_slice()) - .and_then(|()| import.finish()) + .and_then(|()| import.finish_in_place()) .expect_err("a PES PTS below the live edge is a restart"); assert!(is_rewind(&err), "{err:?}"); } @@ -7260,7 +7260,7 @@ pub(super) mod test { import .decode(audio_pes_packet(MP2_PID, 1, 270_000, &mp2_frame(0xBB)[40..]).as_slice()) .unwrap(); - import.finish().unwrap(); + import.finish_in_place().unwrap(); let frames = read_audio_frames(&consumer, &catalog).await; assert_eq!( @@ -7326,7 +7326,7 @@ pub(super) mod test { import .decode(audio_pes_packet(MP2_PID, 2, 450_000, &mp2_frame(0xDD)).as_slice()) .unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let frames = read_audio_frames(&consumer, &catalog).await; // Without confirmation the planted header is published as a frame of payload bytes, @@ -7370,7 +7370,7 @@ pub(super) mod test { import .decode(audio_pes_packet(MP2_PID, 1, 270_000, &real).as_slice()) .unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let frames = read_audio_frames(&consumer, &catalog).await; assert!( @@ -7418,7 +7418,7 @@ pub(super) mod test { import .decode(audio_pes_packet(MP2_PID, 2, 450_000, &real).as_slice()) .unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); // Only the four real frames: the fragment the seek landed inside is dropped rather // than spliced onto the front of the first real frame after it. @@ -7460,7 +7460,7 @@ pub(super) mod test { import .decode(audio_pes_packet(MP2_PID, 0, 270_000, &frame[..4]).as_slice()) .unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let frames = read_audio_frames(&consumer, &catalog).await; assert_eq!( @@ -7504,7 +7504,7 @@ pub(super) mod test { import .decode(audio_pes_packet(AAC_PID, 0, 90_000, &aac).as_slice()) .unwrap(); - import.finish().unwrap(); + import.finish_in_place().unwrap(); // The importer is deliberately still alive here, as moq-srt leaves it after finish. let track = consumer @@ -7584,7 +7584,7 @@ pub(super) mod test { let cc = mux.cc(AAC_PID); let late = audio_pes_packet(AAC_PID, cc, 180_000, &[with.as_slice(), &bare, &with, &bare].concat()); import.decode(&late).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let audio = loop { let update = kio::clock::timeout(Duration::from_millis(10), updates.next()) @@ -7647,7 +7647,7 @@ pub(super) mod test { let cc = mux.cc(AAC_PID); mux.out.extend(audio_pes_packet(AAC_PID, cc, 270_000, &bare)); import.decode(&mux.out).unwrap(); - import.finish().unwrap(); + import.finish_in_place().unwrap(); assert_eq!(import.stats().streams[&AAC_PID].damaged, 1); let frames = read_audio_frames(&consumer, &catalog).await; @@ -7733,7 +7733,7 @@ pub(super) mod test { import .decode(audio_pes_packet(MP2_PID, 2, 450_000, &good).as_slice()) .expect("a clean stream after a seek must not inherit the pre-seek budget"); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let frames = read_audio_frames(&consumer, &catalog).await; assert_eq!( @@ -7806,7 +7806,7 @@ pub(super) mod test { import .decode(audio_pes_packet(MP2_PID, 1, 270_000, &second).as_slice()) .unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let frames = read_audio_frames(&consumer, &catalog).await; assert_eq!(frames.len(), 2, "both frames must survive the split"); @@ -7853,7 +7853,7 @@ pub(super) mod test { import .decode(audio_pes_packet(MP2_PID, 1, 900_000, &frame_b[2..]).as_slice()) .unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let frames = read_audio_frames(&consumer, &catalog).await; assert_eq!(frames.len(), 2, "both frames must survive the header split"); @@ -7901,7 +7901,7 @@ pub(super) mod test { bytes.extend_from_slice(&pcr_packet(VIDEO_PID, 29_700_000)); import.decode(&bytes).unwrap(); let expected = 1_000_000 + (at - (2 * 188 + 10)) * 100_000 / (3 * 188); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let name = track_on(&catalog.snapshot().ext.mpegts, 0x21); let track = consumer @@ -8154,7 +8154,7 @@ pub(super) mod test { let anchored = catalog.snapshot().media.clock.expect("a clock"); assert_ne!(anchored, provisional, "the first PES anchors the clock"); assert!(clocks.drain().is_empty(), "rate measurement still holds the catalog"); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let published = clocks.drain(); assert!(!published.is_empty(), "EOF releases a program without PCR samples"); assert!(published.iter().all(|clock| *clock == Some(anchored)), "{published:?}"); @@ -8288,7 +8288,7 @@ pub(super) mod test { let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()).with_program(2); import.decode(&two_programs()).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let snapshot = catalog.snapshot(); let names: Vec<_> = snapshot.audio.renditions.keys().collect(); @@ -8422,7 +8422,7 @@ pub(super) mod test { data.extend(mp2_pes(0x0062, 0, 90_000, [0xCC, 0xDD])); import.decode(&data).unwrap(); - import.finish().unwrap(); + import.finish_in_place().unwrap(); assert_eq!(catalog.snapshot().audio.renditions.len(), 2, "both streams publish"); assert_eq!(import.stats().crc_error, 0); } @@ -8440,7 +8440,7 @@ pub(super) mod test { data.extend(mp2_pes(0x0061, 0, 90_000, [0xAA, 0xBB])); import.decode(&data).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); assert_eq!(catalog.snapshot().audio.renditions.len(), 1); } @@ -8449,7 +8449,7 @@ pub(super) mod test { let (catalog, import) = psi_import(); let mut import = import.with_program(50); import.decode(&fifty_programs()).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); assert_eq!(catalog.snapshot().audio.renditions.len(), 1, "program 50 publishes"); // Unselected, the whole PAT is what refuses the input. @@ -8501,7 +8501,7 @@ pub(super) mod test { let mut data = psi_packets(0x0100, &mut pmt_cc, &[], &pmt_section(1, 2, &two)); data.extend(mp2_pes(0x0062, 0, 180_000, [0xCC, 0xDD])); import.decode(&data).unwrap(); - import.finish().unwrap(); + import.finish_in_place().unwrap(); assert_eq!( catalog.snapshot().audio.renditions.len(), 2, @@ -8524,7 +8524,7 @@ pub(super) mod test { data.extend(mp2_pes(0x0061, 0, 90_000, [0xAA, 0xBB])); import.decode(&data).unwrap(); - import.finish().unwrap(); + import.finish_in_place().unwrap(); assert!(catalog.snapshot().audio.renditions.is_empty()); assert_eq!(import.stats().crc_error, 1); assert!(!import.stats().is_empty(), "a dropped section is something to report"); @@ -8622,7 +8622,7 @@ pub(super) mod test { import.decode(packet(true, 2, 0, &CUE).as_slice()).unwrap(); import.decode(pes_packet(PCR_PID, 90_000).as_slice()).unwrap(); import.decode(pcr_packet(PCR_PID, 27_000_000).as_slice()).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let name = track_on(&catalog.snapshot().ext.mpegts, 0x21); let (frames, breaks) = read_breaks(&consumer, &name, crate::container::Kind::Data).await; assert_eq!(frames.len(), 3); @@ -8662,7 +8662,7 @@ pub(super) mod test { import.last_pts.unwrap(), Timestamp::from_scale((1 << 33) + 9_000, 90_000).unwrap() ); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); } #[test] @@ -8700,10 +8700,10 @@ pub(super) mod test { import.decode(&pcr_packet(PCR_PID, 27_000_000)).unwrap(); import.decode(&packet(true, 0, 0, &CUE)).unwrap(); if clock == "none" { - let err = import.finish().unwrap_err(); + let err = import.finish_in_place().unwrap_err(); assert!(err.to_string().contains("without a media timestamp"), "{clock}: {err}"); } else { - import.finish().unwrap(); + import.finish_in_place().unwrap(); assert!(import.sections[&0x21].held.is_empty()); assert_eq!(import.stats().streams[&0x21].unplaced, 1, "{clock}: no PCR interval"); } @@ -8741,34 +8741,19 @@ pub(super) mod test { let name = catalog.snapshot().ext.mpegts.tracks.keys().next().unwrap().clone(); let mut track = consumer.track(&name).unwrap().subscribe(None).await.unwrap(); - let err = import.finish().unwrap_err(); + let err = import.finish_in_place().unwrap_err(); assert!(err.to_string().contains("without a media timestamp"), "{err}"); let ended = kio::clock::timeout(Duration::from_millis(50), track.recv_group()) .await .expect("the track ends"); assert!(ended.is_err(), "the track aborts rather than finishing"); - let retried = import.finish().unwrap_err(); + let retried = import.finish_in_place().unwrap_err(); assert!(retried.to_string().contains("without a media timestamp"), "{retried}"); let refused = import.decode(&pcr_packet(PCR_PID, 54_000_000)).unwrap_err(); assert!(refused.to_string().contains("without a media timestamp"), "{refused}"); } - /// Input after a finish that succeeded is refused, as every importer refuses it. - #[test] - fn input_after_finish_is_refused() { - let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); - let mut import = super::Import::new(broadcast, catalog.reserve()); - import.finish().unwrap(); - import.finish().unwrap(); - let err = import.decode(&pcr_packet(PCR_PID, 27_000_000)).unwrap_err(); - assert!( - matches!(err.downcast_ref::(), Some(crate::Error::Finished)), - "{err}" - ); - } - #[test] fn unanchored_sections_cannot_cross_a_clock_break() { use crate::catalog::hang::Catalog; @@ -8812,7 +8797,7 @@ pub(super) mod test { .decode(mp2_pes(pid, cc, 270_000, [0xEE, 0xFF]).as_slice()) .unwrap(); } - import.finish().unwrap(); + let _finished = import.finish().unwrap(); for (_, breaks) in read_all_breaks(&consumer, &catalog).await { assert_eq!(breaks, 1, "a retransmitted packet declared another break"); } @@ -8835,7 +8820,7 @@ pub(super) mod test { .decode(mp2_pes(pid, cc, 270_000, [0xEE, 0xFF]).as_slice()) .unwrap(); } - import.finish().unwrap(); + let _finished = import.finish().unwrap(); for (_, breaks) in read_all_breaks(&consumer, &catalog).await { assert_eq!(breaks, 1, "an incomplete frame was counted as publication"); } @@ -8861,7 +8846,7 @@ pub(super) mod test { .decode(mp2_pes(pid, 1, 90_000 + 20 * 90_000, [0xCC, 0xDD]).as_slice()) .unwrap(); } - import.finish().unwrap(); + let _finished = import.finish().unwrap(); for (frames, breaks) in read_all_breaks(&consumer, &catalog).await { assert_eq!(breaks, 1, "the timebase break did not reach this track"); @@ -8901,7 +8886,7 @@ pub(super) mod test { } ); assert_eq!(restart.remainder, after, "the new instance starts at the break"); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); } /// Adaptation stuffing leaves exactly these bytes in a TS payload. @@ -8985,7 +8970,7 @@ pub(super) mod test { 0, ) .unwrap(), - _ => import.finish().unwrap(), + _ => import.finish_in_place().unwrap(), } assert!(!import.headers.contains_key(&pid)); if end == "start" { @@ -9012,7 +8997,7 @@ pub(super) mod test { .decode(&mp2_pes(PCR_PID, i as u8, pts % (1 << 33), [1, 2])) .unwrap(); } - import.finish().unwrap(); + let _finished = import.finish().unwrap(); assert!(!catalog.snapshot().audio.renditions.is_empty()); } @@ -9044,7 +9029,7 @@ pub(super) mod test { import.decode(&data).unwrap(); tokio::time::advance(Duration::from_millis(144)).await; } - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let all = timeline(&consumer, &catalog).await; let step = all[&21] - all[&19]; assert!( @@ -9062,7 +9047,7 @@ pub(super) mod test { import.decode(&source_pes(i, 4_000 + i * STEP)).unwrap(); tokio::time::advance(Duration::from_millis(144)).await; } - import.finish().unwrap(); + import.finish_in_place().unwrap(); assert_eq!(import.stats().streams[&PEER_PID].unplaced, 1); let all = timeline(&consumer, &catalog).await; assert!(!all.contains_key(&0x80), "the peer's PES before the wrap is dropped"); @@ -9207,7 +9192,7 @@ pub(super) mod test { .unwrap(); } if end == "eof" { - import.finish().unwrap(); + import.finish_in_place().unwrap(); } else { let mut header = vec![0, 0, 1, 0xe0, 0, 0, 0x80, 0x80, 5]; header.extend_from_slice(&pes_timestamp(2, 180_000)); @@ -9401,7 +9386,7 @@ pub(super) mod test { import.decode(&data).unwrap(); tokio::time::advance(Duration::from_millis(144)).await; } - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let frames = timeline(&consumer, &catalog).await; let apart = frames[&31] - frames[&0x9e]; @@ -9542,7 +9527,7 @@ pub(super) mod test { .unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); import.decode(data)?; - import.finish()?; + import.finish_in_place()?; Ok((consumer, catalog, import)) } @@ -9896,7 +9881,7 @@ pub(super) mod test { import .decode(&mux.out[resume_at..]) .expect("next keyframe must recover"); - import.finish().unwrap(); + import.finish_in_place().unwrap(); assert_eq!(import.stats().streams[&VIDEO].damaged, u64::from(damage != "clean")); assert_eq!(import.stats().streams[&peer].damaged, 0); for (pid, expected) in [ @@ -10023,7 +10008,7 @@ pub(super) mod test { let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); import.decode(&data).expect("a damaged SPS stays local to its unit"); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let name = catalog.snapshot().video.renditions.keys().next().unwrap().clone(); let frames = read_track(&consumer, &name, crate::container::Kind::Video).await; assert_eq!(frames.len(), 4, "the bare keyframe after the damage recovers"); @@ -10152,7 +10137,7 @@ pub(super) mod test { damaged[start + 6..start + 19].fill(0); data.extend(damaged); import.decode(&data).unwrap(); - import.finish().unwrap(); + import.finish_in_place().unwrap(); assert_eq!(import.stats().streams[&VIDEO].damaged, 1); assert_eq!( import.stats().streams[&VIDEO].units, @@ -10186,7 +10171,7 @@ pub(super) mod test { flagged[3] |= 1; data.extend(flagged); import.decode(&data).unwrap(); - import.finish().unwrap(); + import.finish_in_place().unwrap(); assert_eq!( import.stats().streams[&VIDEO].units, 1, @@ -10214,7 +10199,7 @@ pub(super) mod test { data.extend(packet); import.decode(&data).unwrap(); assert_eq!(import.stats().streams[&VIDEO].damaged, 0, "PES has not ended yet"); - import.finish().unwrap(); + import.finish_in_place().unwrap(); assert_eq!(import.stats().streams[&VIDEO].damaged, 1); assert_eq!(import.stats().streams[&VIDEO].units, 0); assert!(catalog.snapshot().video.renditions.is_empty()); @@ -10247,7 +10232,7 @@ pub(super) mod test { data.extend(video_pes(VIDEO, cc, 90_000 + u64::from(cc) * FRAME, None, payload)); } import.decode(&data).unwrap(); - import.finish().unwrap(); + import.finish_in_place().unwrap(); assert_eq!(import.stats().streams[&VIDEO].damaged, 1); assert_eq!(import.stats().streams[&VIDEO].units, 2); let name = catalog.snapshot().audio.renditions.keys().next().unwrap().clone(); @@ -10339,7 +10324,7 @@ pub(super) mod test { let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); import.decode(&mux.out).unwrap(); - import.finish().unwrap(); + import.finish_in_place().unwrap(); assert_eq!(import.stats().streams[&VIDEO].damaged, 1); // Each group as published, its empty end marker included. @@ -10921,7 +10906,7 @@ pub(super) mod test { import.decode(&pcr_packet(PCR_PID, 0)).unwrap(); let waiter = kio::Waiter::noop(); assert!(updates.poll_next(&waiter).is_pending()); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let std::task::Poll::Ready(Ok(Some(first))) = updates.poll_next(&waiter) else { panic!("short recording finished without a catalog"); }; @@ -10995,7 +10980,7 @@ pub(super) mod test { import .decode(mp2_pes(PCR_PID, 1, 180_000, [0xCC, 0xDD]).as_slice()) .unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); for (_, breaks) in read_all_breaks(&consumer, &catalog).await { assert_eq!(breaks, 0, "a counter jump is not a program timebase reset"); @@ -11015,7 +11000,7 @@ pub(super) mod test { import .decode(mp2_pes(PCR_PID, 7, 180_000, [0xCC, 0xDD]).as_slice()) .unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); for (_, breaks) in read_all_breaks(&consumer, &catalog).await { assert_eq!(breaks, 0, "lost packets are not a signalled clock reset"); @@ -11035,7 +11020,7 @@ pub(super) mod test { .unwrap(); import.decode(mp2_pes(pid, 1, 90_000, [0xCC, 0xDD]).as_slice()).unwrap(); } - import.finish().unwrap(); + let _finished = import.finish().unwrap(); for (frames, breaks) in read_all_breaks(&consumer, &catalog).await { assert_eq!(breaks, 0, "a rollover is not a break"); @@ -11065,7 +11050,7 @@ pub(super) mod test { .decode(mp2_pes(pid, 1, 180_000, [0xCC, 0xDD]).as_slice()) .unwrap(); } - import.finish().unwrap(); + let _finished = import.finish().unwrap(); for (_, breaks) in read_all_breaks(&consumer, &catalog).await { assert_eq!(breaks, 1, "each flagged packet declared its own break"); @@ -11091,7 +11076,7 @@ pub(super) mod test { .decode(mp2_pes(pid, 1, 180_000, [0xCC, 0xDD]).as_slice()) .unwrap(); } - import.finish().unwrap(); + let _finished = import.finish().unwrap(); for (_, breaks) in read_all_breaks(&consumer, &catalog).await { assert_eq!(breaks, 0, "a corrupt packet's adaptation field was trusted"); @@ -11109,7 +11094,7 @@ pub(super) mod test { for pid in [PCR_PID, PEER_PID] { import.decode(mp2_pes(pid, 0, 90_000, [0xAA, 0xBB]).as_slice()).unwrap(); } - import.finish().unwrap(); + let _finished = import.finish().unwrap(); for (frames, breaks) in read_all_breaks(&consumer, &catalog).await { assert_eq!(breaks, 0, "nothing had been published to break from"); @@ -11176,7 +11161,7 @@ pub(super) mod test { let (frames, before_flags) = exported_discontinuities(&mut exporter).await; assert!(frames > 0, "media before the boundary must reach the output"); import.decode(&bytes::BytesMut::from(after)).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let (frames, after_flags) = exported_discontinuities(&mut exporter).await; assert!(frames > 0, "media after the boundary must reach the output"); before_flags + after_flags @@ -11293,7 +11278,7 @@ pub(super) mod test { "the first snapshot carries the anchored clock" ); import.decode(&pes_fragment(PID, false, 1, &body)).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); } fn opus_extension(body: &[u8]) -> Vec { @@ -11415,7 +11400,7 @@ pub(super) mod test { let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); let mut import = super::Import::new(broadcast, catalog.reserve()); import.decode(data).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); catalog.snapshot() } @@ -11485,7 +11470,7 @@ pub(super) mod test { descriptors: Vec::new(), }); import.handle_pmt(pmt).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); logs_assert(|lines| { let streams: Vec<_> = lines .iter() @@ -11549,7 +11534,7 @@ pub(super) mod test { import.handle_pmt(pmt(None, 0x05)).unwrap(); import.handle_pmt(pmt(Some(0x100), 0x0f)).unwrap(); import.handle_pmt(pmt(Some(0x100), 0x0f)).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); logs_assert(|lines| { let streams: Vec<_> = lines .iter() @@ -11651,7 +11636,7 @@ pub(super) mod test { import.handle_pmt(pmt(Some(Pid::new(PCR_PID).unwrap()))).unwrap(); let next = track_on(&catalog.snapshot().ext.mpegts, 0x21); assert_ne!(next, name, "restoring a stream publishes a new track"); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); } } @@ -11752,7 +11737,7 @@ pub(super) mod test { .clone(); assert_ne!(new, old); assert_eq!(language(&after, &new).as_ref(), b"fra\0"); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); } /// The program descriptors follow each PMT revision in place, and adding or removing the @@ -11884,7 +11869,7 @@ pub(super) mod test { mux.out.extend(mp2_pes(0x61, 1, 108_000, [0xAA, 0xBB])); mux.out.extend(mp2_pes(0x62, 0, 108_000, [0xCC, 0xDD])); import.decode(&mux.out).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); assert_eq!(pmt_pid(), Some(0x200)); assert_eq!( catalog.snapshot().audio.renditions.len(), diff --git a/rs/moq-mux/src/container/ts/import_test.rs b/rs/moq-mux/src/container/ts/import_test.rs index 5b0992bfee6..3cf3304abcc 100644 --- a/rs/moq-mux/src/container/ts/import_test.rs +++ b/rs/moq-mux/src/container/ts/import_test.rs @@ -27,7 +27,7 @@ fn import_ts(data: &[u8]) -> crate::catalog::hang::Catalog { let mut import = crate::container::ts::Import::new(broadcast, catalog.reserve()); let buf = BytesMut::from(data); import.decode(&buf).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); catalog.snapshot() } @@ -44,7 +44,7 @@ fn import_ts_ext(data: &[u8]) -> crate::catalog::hang::Catalog Imported { let before = kio::clock::wall(); import.decode(data).unwrap(); let after = kio::clock::wall(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let snapshot = catalog.snapshot(); Imported { diff --git a/rs/moq-mux/src/container/ts/passthrough_test.rs b/rs/moq-mux/src/container/ts/passthrough_test.rs index 0da27d32b75..dc634640afe 100644 --- a/rs/moq-mux/src/container/ts/passthrough_test.rs +++ b/rs/moq-mux/src/container/ts/passthrough_test.rs @@ -84,7 +84,7 @@ async fn import_onto( for chunk in data.chunks(chunk) { passthrough.decode(chunk)?; } - passthrough.finish()?; + let _finished = passthrough.finish()?; let mut track = consumer .track(&name)? @@ -510,7 +510,7 @@ async fn the_section_goes_with_the_importer() { if abort { passthrough.abort(moq_net::Error::Cancel); } else { - passthrough.finish().unwrap(); + let passthrough = passthrough.finish().unwrap(); assert!(catalog.snapshot().m2ts.is_some(), "a finished track is still described"); drop(passthrough); } @@ -844,15 +844,3 @@ async fn an_overriding_clock_paces_on_its_own_pcrs() { ); } -/// Input after finish is refused, as every importer refuses it. -#[test] -fn input_after_finish_is_refused() { - let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = crate::catalog::Producer::new(&mut broadcast, Default::default()).unwrap(); - let mut passthrough = Passthrough::new(broadcast, catalog.reserve()).unwrap(); - passthrough.finish().unwrap(); - passthrough.finish().unwrap(); - - let err = passthrough.decode(KYRION_H264).unwrap_err(); - assert!(matches!(err, crate::Error::Finished), "{err:?}"); -} diff --git a/rs/moq-mux/src/container/ts/pes_packet_test.rs b/rs/moq-mux/src/container/ts/pes_packet_test.rs index a2cf0d12e37..62d46bbd0b5 100644 --- a/rs/moq-mux/src/container/ts/pes_packet_test.rs +++ b/rs/moq-mux/src/container/ts/pes_packet_test.rs @@ -182,7 +182,7 @@ struct Imported { fn import(data: &[u8], captured: bool) -> anyhow::Result { let mut import = start(captured); import.import.decode(data)?; - import.import.finish()?; + import.import.finish_in_place()?; Ok(import) } @@ -576,7 +576,7 @@ async fn a_pes_with_a_field_no_rebase_reaches_refuses_its_stream() { tokio::task::yield_now().await; } imported.import.decode(&feed.bytes()[head.len()..]).unwrap(); - imported.import.finish().unwrap(); + imported.import.finish_in_place().unwrap(); let (frames, err) = reader.await.unwrap(); assert_eq!(frames.len(), 1, "{why}: the PES placed before it stays published"); @@ -838,7 +838,7 @@ async fn a_pmt_naming_or_dropping_the_pcr_reroutes_private_pes() { tokio::task::yield_now().await; } imported.import.decode(&feed.bytes()[head.len()..]).unwrap(); - imported.import.finish().unwrap(); + imported.import.finish_in_place().unwrap(); let (frames, err) = reader.await.unwrap(); assert_eq!(frames.len(), 1, "the PES placed while the PCR was named"); @@ -923,7 +923,7 @@ async fn a_revised_private_stream_publishes_under_a_fresh_name() { assert_eq!(recorded(entry), descriptor_loop(revised)); let name = name.clone(); - imported.import.finish().unwrap(); + imported.import.finish_in_place().unwrap(); let (frames, err) = read(&imported.consumer, &name).await; assert!(err.is_none(), "{err:?}"); assert_eq!(frames.len(), 1); @@ -959,7 +959,7 @@ async fn a_dropped_private_stream_finishes_and_its_reused_pid_opens_a_new_track( assert_eq!(imported.stats().quiet, None, "a PID the PMT dropped is not silent"); imported.import.decode(&feed.bytes()[dropped.len()..]).unwrap(); - imported.import.finish().unwrap(); + imported.import.finish_in_place().unwrap(); let (name, _) = imported.entry().expect("the PID is carried again"); assert_ne!(name, old, "a reused PID gets a new name"); let (frames, err) = read(&imported.consumer, &name).await; diff --git a/rs/moq-mux/src/container/ts/programs.rs b/rs/moq-mux/src/container/ts/programs.rs index 14fbd40f6cd..a233f8f9205 100644 --- a/rs/moq-mux/src/container/ts/programs.rs +++ b/rs/moq-mux/src/container/ts/programs.rs @@ -1065,7 +1065,9 @@ mod test { assert_eq!(back[1].0, 2); assert_ne!(back[1].1, both[1].1, "the returning program is a new instance"); - programs.finish().unwrap(); + let finished = programs.finish().unwrap(); + + let programs = &finished.programs; let stats = programs.stats(); assert_eq!(stats.streams[&0x61].units, 8, "program 1 delivered every frame"); assert_eq!(stats.streams[&0x71].units, 4, "the returning program counts on"); @@ -1091,7 +1093,8 @@ mod test { } let running: Vec<_> = instances(&programs).into_iter().map(|(number, _)| number).collect(); assert_eq!(running, [2]); - programs.finish().unwrap(); + let finished = programs.finish().unwrap(); + let programs = &finished.programs; assert_eq!(programs.stats().streams[&0x71].units, 4, "program 2 read on"); } @@ -1152,7 +1155,8 @@ mod test { assert_eq!(units(&programs), 1, "program 2 still holds the second picture"); programs.decode(&pictures.next().unwrap()).unwrap(); assert_eq!(units(&programs), 2, "the second picture counts once"); - programs.finish().unwrap(); + let finished = programs.finish().unwrap(); + let programs = &finished.programs; assert_eq!(units(&programs), 3); } @@ -1413,7 +1417,7 @@ mod test { mux.packet(0x1fff, Flags::default()); programs.decode(&std::mem::take(&mut mux.out)).unwrap(); assert_eq!(programs.stats().pts_error, 1); - programs.finish().unwrap(); + let _finished = programs.finish().unwrap(); } #[tokio::test(start_paused = true)] @@ -1458,7 +1462,7 @@ mod test { assert_eq!(stats.pmt_error, 2 * u64::from(!continuing), "{stats:?}"); assert_eq!(stats.pts_error, u64::from(!continuing), "{stats:?}"); assert_eq!(stats.streams[&0x61].pts_error, stats.pts_error); - programs.finish().unwrap(); + let _finished = programs.finish().unwrap(); } } } @@ -1522,7 +1526,8 @@ mod test { } } programs.decode(&std::mem::take(&mut mux.out)).unwrap(); - programs.finish().unwrap(); + let finished = programs.finish().unwrap(); + let programs = &finished.programs; let stats = programs.stats(); assert_eq!(stats.streams[&0x61].units, 8, "program 2 delivers every frame"); assert_eq!(stats.pcr_repetition_error, 1, "program 1 grades its clock"); @@ -1559,7 +1564,8 @@ mod test { assert_eq!(units(&programs), 1, "program 2 still holds the second picture"); programs.decode(&pictures.next().unwrap()).unwrap(); assert_eq!(units(&programs), 2, "the second picture counts once"); - programs.finish().unwrap(); + let finished = programs.finish().unwrap(); + let programs = &finished.programs; assert_eq!(units(&programs), 3); } @@ -1589,7 +1595,8 @@ mod test { assert_eq!(programs.programs.len(), 4); step(&mut mux, &[(2, 0x100), (4, 0x100)], 1); programs.decode(&std::mem::take(&mut mux.out)).unwrap(); - programs.finish().unwrap(); + let finished = programs.finish().unwrap(); + let programs = &finished.programs; let running: Vec<_> = instances(&programs).into_iter().map(|(number, _)| number).collect(); assert_eq!(running, [2, 4]); let units = |pid| programs.stats().streams[&pid].units; @@ -1617,7 +1624,7 @@ mod test { mux.pat(&[]); programs.decode(&mux.out).unwrap(); assert!(programs.programs.is_empty()); - programs.finish().unwrap(); + let _finished = programs.finish().unwrap(); } /// A PAT naming another transport stream is refused while a program runs, before it retires @@ -1747,7 +1754,8 @@ mod test { for chunk in input.chunks(100) { programs.decode(chunk).unwrap(); } - programs.finish().unwrap(); + let finished = programs.finish().unwrap(); + let programs = &finished.programs; assert_eq!(programs.programs.len(), 1, "only program 50 has a PMT"); let consumer = origin.consume().request("event/50.hang", None).await.unwrap(); @@ -1812,7 +1820,8 @@ mod test { catalog::Config::default(), ); programs.decode(&two_section_pat()).unwrap(); - programs.finish().unwrap(); + let finished = programs.finish().unwrap(); + let programs = &finished.programs; assert_eq!(programs.programs.len(), 2); assert_eq!(renditions(&origin, "event/1.hang").await, 1); assert_eq!(renditions(&origin, "event/2.hang").await, 1); @@ -1853,7 +1862,8 @@ mod test { data.extend(mp2_pes(pid, 0, 90_000, fills)); } programs.decode(&data).unwrap(); - programs.finish().unwrap(); + let finished = programs.finish().unwrap(); + let programs = &finished.programs; let started: Vec<_> = programs.programs.iter().map(|program| program.number).collect(); assert_eq!(started, [7, 900]); @@ -1885,7 +1895,8 @@ mod test { } data.extend(mp2_pes(0x0061, 0, 90_000, [0xAA, 0xBB])); programs.decode(&data).unwrap(); - programs.finish().unwrap(); + let finished = programs.finish().unwrap(); + let programs = &finished.programs; let started: Vec<_> = programs.programs.iter().map(|program| program.number).collect(); assert_eq!(started, [1]); @@ -1907,7 +1918,7 @@ mod test { data.extend(psi_packets(0x0100, &mut 0, &[], §ions)); data.extend(mp2_pes(0x0061, 0, 90_000, [0xAA, 0xBB])); programs.decode(&data).unwrap(); - programs.finish().unwrap(); + let _finished = programs.finish().unwrap(); assert_eq!(renditions(&origin, "event/1.hang").await, 1); } @@ -1929,7 +1940,7 @@ mod test { data.extend(packets); data.extend(mp2_pes(0x0061, 0, 90_000, [0xAA, 0xBB])); programs.decode(&data).unwrap(); - programs.finish().unwrap(); + let _finished = programs.finish().unwrap(); assert_eq!(renditions(&origin, "event/1.hang").await, 1); } @@ -2066,7 +2077,8 @@ mod test { catalog::Config::default(), ); programs.decode(&two_section_pat()).unwrap(); - programs.finish().unwrap(); + let finished = programs.finish().unwrap(); + let programs = &finished.programs; assert!( programs.programs.iter().all(|program| program.route.is_announced()), "every program announces" @@ -2104,7 +2116,8 @@ mod test { for packet in packets { programs.decode(packet).unwrap(); } - programs.finish().unwrap(); + let finished = programs.finish().unwrap(); + let programs = &finished.programs; assert!( programs.programs.iter().all(|program| program.route.is_announced()), "every program is announced once it has declared its tracks" @@ -2140,7 +2153,8 @@ mod test { let input = two_programs(); programs.decode(&input[..3 * 188]).unwrap(); - let Err(err) = programs.finish() else { + // The finish's own steps, so the programs can be inspected after the failure. + let Err(err) = programs.end() else { panic!("the PMTs have no configured media") }; assert!(err.to_string().contains("declared a track"), "{err:#}"); @@ -2243,7 +2257,7 @@ mod test { "no program is known before the PAT is whole" ); programs.decode(&input[100..]).unwrap(); - programs.finish().unwrap(); + let _finished = programs.finish().unwrap(); let after = kio::clock::wall(); for (path, fills) in [("event/1.hang", [0xAA, 0xBB]), ("event/2.hang", [0xCC, 0xDD])] { diff --git a/rs/moq-mux/src/container/ts/recovery_test.rs b/rs/moq-mux/src/container/ts/recovery_test.rs index a77becf2e72..dd6a911fd82 100644 --- a/rs/moq-mux/src/container/ts/recovery_test.rs +++ b/rs/moq-mux/src/container/ts/recovery_test.rs @@ -481,7 +481,7 @@ async fn record(profile: Profile) -> Recording { let consumer = broadcast.consume(); let mut import = super::Import::new(broadcast.clone(), catalog.reserve()); import.decode(&profile.clip().ts).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let snapshot = catalog.snapshot(); let whole = (snapshot.ext.mpegts.tracks.iter()).filter(|(_, track)| { diff --git a/rs/moq-mux/src/container/ts/release_test.rs b/rs/moq-mux/src/container/ts/release_test.rs index dd8df163874..8f227a7ab1f 100644 --- a/rs/moq-mux/src/container/ts/release_test.rs +++ b/rs/moq-mux/src/container/ts/release_test.rs @@ -27,7 +27,7 @@ const TOLERANCE: Duration = Duration::from_millis(1); /// A capture imported whole, with what keeps its broadcast up. struct Imported { - _passthrough: Passthrough, + _passthrough: crate::container::ts::passthrough::Finished, _catalog: crate::catalog::Producer, _broadcast: moq_net::broadcast::Producer, consumer: moq_net::broadcast::Consumer, @@ -41,7 +41,7 @@ async fn import(data: &[u8]) -> Imported { let mut passthrough = Passthrough::new(broadcast.clone(), catalog.reserve()).unwrap(); let name = passthrough.track().to_string(); passthrough.decode(data).unwrap(); - passthrough.finish().unwrap(); + let passthrough = passthrough.finish().unwrap(); let mut track = consumer .track(&name) diff --git a/rs/moq-mux/src/container/ts/revision_test.rs b/rs/moq-mux/src/container/ts/revision_test.rs index eeaf1a804e0..f1323a7bf9b 100644 --- a/rs/moq-mux/src/container/ts/revision_test.rs +++ b/rs/moq-mux/src/container/ts/revision_test.rs @@ -107,7 +107,7 @@ async fn reexport_frames(spans: &[(u16, &Span<'_>)], intervals: u64) -> Vec {} } @@ -1787,7 +1787,7 @@ async fn the_clock_moves_behind_the_last_packet_of_the_pmt_naming_its_pid() { } settle(&mut export, &mut frames).await; } - other.finish().unwrap(); + let _finished = other.finish().unwrap(); catalog.finish().unwrap(); drain(&mut export, &mut frames).await; @@ -1830,7 +1830,7 @@ async fn a_rate_change_applies_behind_the_revision_it_comes_with() { }]; } live(&mut export, &mut frames, start, &mut [(&mut audio, 0x21)], 3_000, 6_000).await; - audio.finish().unwrap(); + let _finished = audio.finish().unwrap(); catalog.finish().unwrap(); drain(&mut export, &mut frames).await; @@ -1923,8 +1923,8 @@ async fn withdraw_and_return(listed: bool) { back + 1_000, ) .await; - first.finish().unwrap(); - second.finish().unwrap(); + let _finished = first.finish().unwrap(); + let _finished = second.finish().unwrap(); catalog.finish().unwrap(); drain(&mut export, &mut frames).await; diff --git a/rs/moq-mux/src/container/ts/routes_test.rs b/rs/moq-mux/src/container/ts/routes_test.rs index 10398ccb968..9d60ef94eab 100644 --- a/rs/moq-mux/src/container/ts/routes_test.rs +++ b/rs/moq-mux/src/container/ts/routes_test.rs @@ -96,7 +96,7 @@ impl Imported { fn all(data: &[u8]) -> Self { let mut imported = Self::new(); imported.import.decode(data).unwrap(); - imported.import.finish().unwrap(); + imported.import.finish_in_place().unwrap(); imported } diff --git a/rs/moq-mux/src/container/ts/step_test.rs b/rs/moq-mux/src/container/ts/step_test.rs index bc637dab93c..99eccca1324 100644 --- a/rs/moq-mux/src/container/ts/step_test.rs +++ b/rs/moq-mux/src/container/ts/step_test.rs @@ -135,7 +135,7 @@ fn feed(data: &[u8], chunk: usize) -> (Option, Vec) { let mut rest = restart.remainder.to_vec(); rest.extend(data.chunks(chunk).skip(index + 1).flatten()); // The instance before the step ends cleanly. - import.finish().unwrap(); + let _finished = import.finish().unwrap(); return (Some(err), rest); } } diff --git a/rs/moq-mux/src/import/mod.rs b/rs/moq-mux/src/import/mod.rs index e0986d0848b..3324b9f631c 100644 --- a/rs/moq-mux/src/import/mod.rs +++ b/rs/moq-mux/src/import/mod.rs @@ -149,7 +149,7 @@ mod tests { for chunk in data.chunks(chunk) { import.decode(chunk).unwrap(); } - import.finish().unwrap(); + let import = import.finish().unwrap(); Box::new(import) } } @@ -169,7 +169,7 @@ mod tests { for chunk in data.chunks(CHUNK) { import.decode(chunk).unwrap(); } - import.finish().unwrap(); + let import = import.finish().unwrap(); Box::new(import) }); } From e435257d9748c0182d8911515378c88d7f552d67 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 9 Nov 2026 17:57:55 -0800 Subject: [PATCH 05/10] wip: moq-audio Finished handle and downstream callers Co-Authored-By: Claude Opus 5.5 --- rs/moq-audio/benches/batches.rs | 28 +- rs/moq-audio/src/decode/conformance.rs | 6 +- rs/moq-audio/src/decode/consumer.rs | 28 +- rs/moq-audio/src/encode/capture.rs | 41 ++- rs/moq-audio/src/encode/mod.rs | 2 +- rs/moq-audio/src/encode/producer.rs | 320 ++++++------------ rs/moq-audio/tests/integration/import.rs | 2 +- rs/moq-audio/tests/integration/roundtrip.rs | 6 +- rs/moq-c/src/audio.rs | 24 +- rs/moq-c/src/publish.rs | 45 ++- rs/moq-cli/src/hls/record.rs | 8 +- rs/moq-cli/src/play/media.rs | 56 ++- rs/moq-cli/src/play/video.rs | 56 +-- rs/moq-cli/src/publish.rs | 256 +++++++++----- rs/moq-cli/src/report.rs | 5 +- rs/moq-cli/src/report/echo_test.rs | 4 +- rs/moq-cli/tests/integration/export.rs | 2 +- rs/moq-cli/tests/integration/report.rs | 2 +- rs/moq-ffi/src/audio.rs | 11 +- rs/moq-ffi/src/json.rs | 4 +- rs/moq-ffi/src/media.rs | 13 +- rs/moq-ffi/src/test.rs | 4 +- rs/moq-gst/src/sink/pad.rs | 26 +- rs/moq-gst/src/source/imp.rs | 51 +-- rs/moq-hls/src/export/archive_tests.rs | 2 +- rs/moq-hls/src/export/mod.rs | 10 +- rs/moq-hls/src/import.rs | 21 +- rs/moq-hls/src/server/include_tests.rs | 4 +- rs/moq-hls/tests/integration/conformance.rs | 2 +- rs/moq-hls/tests/integration/finish.rs | 3 +- rs/moq-hls/tests/integration/server.rs | 2 +- rs/moq-hls/tests/integration/support.rs | 18 +- rs/moq-mux/src/container/producer.rs | 9 +- rs/moq-net-tests/tests/integration/refresh.rs | 2 +- rs/moq-room/src/chat.rs | 5 +- rs/moq-rtmp/src/dial.rs | 31 +- rs/moq-rtmp/src/server.rs | 24 +- rs/moq-srt/src/server.rs | 2 +- rs/moq-srt/src/ts.rs | 63 +++- rs/moq-video/src/decode/consumer.rs | 10 +- rs/moq-video/src/decode/decoder.rs | 2 +- test/interop/clients/rust/catalog.rs | 2 +- test/interop/clients/rust/hls.rs | 13 +- 43 files changed, 646 insertions(+), 579 deletions(-) diff --git a/rs/moq-audio/benches/batches.rs b/rs/moq-audio/benches/batches.rs index 10f36902de2..5d8f29cc1ea 100644 --- a/rs/moq-audio/benches/batches.rs +++ b/rs/moq-audio/benches/batches.rs @@ -23,7 +23,7 @@ use std::time::{Duration, Instant}; use bytes::Bytes; use criterion::{BatchSize, BenchmarkId, Criterion, Throughput, criterion_group}; -use moq_audio::encode::{Codec, Config, Input, Options, Producer}; +use moq_audio::encode::{Codec, Config, Finished, Input, Options, Producer}; use moq_audio::{Frame, Layout}; use moq_net::Timestamp; @@ -205,7 +205,7 @@ impl Case { } /// Time individual writes only in the separate latency pass, outside instruction and throughput measurements. -fn run(producers: &mut [Producer], input: &[Frame]) -> Duration { +fn run(mut producers: Vec, input: &[Frame]) -> (Duration, Vec) { let mut slowest = Duration::ZERO; for frame in input { for producer in producers.iter_mut() { @@ -216,10 +216,8 @@ fn run(producers: &mut [Producer], input: &[Frame]) -> Durati } } } - for producer in producers.iter_mut() { - producer.finish().unwrap(); - } - slowest + let finished = producers.into_iter().map(|producer| producer.finish().unwrap()).collect(); + (slowest, finished) } fn cases() -> Vec { @@ -269,20 +267,20 @@ fn counts() { ); for case in cases() { let input = case.input(); - let (broadcast, mut producers) = case.producers(); + let (broadcast, producers) = case.producers(); ALLOCATIONS.store(0, Ordering::Relaxed); ALLOCATED.store(0, Ordering::Relaxed); COUNTING.store(true, Ordering::Relaxed); if let Some(counter) = &mut counter { counter.start(); } - run::(&mut producers, &input); + let (_, finished) = run::(producers, &input); let instructions = counter.as_mut().map(instructions::Counter::stop); COUNTING.store(false, Ordering::Relaxed); - drop((producers, broadcast)); - let (broadcast, mut producers) = case.producers(); - let slowest = run::(&mut producers, &input); - drop((producers, broadcast)); + drop((finished, broadcast)); + let (broadcast, producers) = case.producers(); + let (slowest, finished) = run::(producers, &input); + drop((finished, broadcast)); let per_sample = match instructions { Some(instructions) => format!("{:.1}", instructions as f64 / case.samples() as f64), @@ -309,9 +307,9 @@ fn bench_batches(c: &mut Criterion) { group.bench_function(BenchmarkId::from_parameter(case.name()), |b| { b.iter_batched( || case.producers(), - |(broadcast, mut producers)| { - run::(&mut producers, &input); - (broadcast, producers) + |(broadcast, producers)| { + let (_, finished) = run::(producers, &input); + (broadcast, finished) }, BatchSize::PerIteration, ); diff --git a/rs/moq-audio/src/decode/conformance.rs b/rs/moq-audio/src/decode/conformance.rs index 77a2f0ea01d..f367a02fb17 100644 --- a/rs/moq-audio/src/decode/conformance.rs +++ b/rs/moq-audio/src/decode/conformance.rs @@ -149,14 +149,12 @@ fn import( Container::Ts(bytes) => { let mut importer = moq_mux::container::ts::Import::new(broadcast, catalog.reserve()); importer.decode(bytes).unwrap(); - importer.finish().unwrap(); - Box::new(importer) + Box::new(importer.finish().unwrap()) } Container::Cmaf(bytes) => { let mut importer = moq_mux::container::fmp4::Import::new(broadcast, catalog.reserve()); importer.decode(bytes).unwrap(); - importer.finish().unwrap(); - Box::new(importer) + Box::new(importer.finish().unwrap()) } }; let snapshot = catalog.snapshot(); diff --git a/rs/moq-audio/src/decode/consumer.rs b/rs/moq-audio/src/decode/consumer.rs index 754a2856bac..5f426d530a4 100644 --- a/rs/moq-audio/src/decode/consumer.rs +++ b/rs/moq-audio/src/decode/consumer.rs @@ -913,7 +913,7 @@ mod tests { producer.cut(None).unwrap(); } } - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let mut consumer = Consumer::new( &broadcast.consume(), &catalog, @@ -1130,7 +1130,7 @@ mod tests { for packet in 1..=20 { write(packet); } - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let mut stamps = Vec::new(); while let Some(read) = consumer.read().await.unwrap() { @@ -1190,7 +1190,7 @@ mod tests { } producer.cut(None).unwrap(); } - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let mut catalog = hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Pcm, 48_000, 1); catalog.warmup = Some(Duration::from_millis(80)); @@ -1243,7 +1243,7 @@ mod tests { keyframe: true, }) .unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let mut consumer = Consumer::new( &broadcast.consume(), &catalog, @@ -1299,7 +1299,7 @@ mod tests { }) .unwrap(); } - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let mut consumer = Consumer::new( &broadcast.consume(), @@ -1365,7 +1365,7 @@ mod tests { }) .unwrap(); } - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let mut consumer = Consumer::new( &broadcast.consume(), &catalog, @@ -1433,7 +1433,7 @@ mod tests { }) .unwrap(); } - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let mut stamps = Vec::new(); while let Some(frame) = next_frame(&mut consumer).await.unwrap() { stamps.push(frame.timestamp.as_micros()); @@ -1572,7 +1572,7 @@ mod tests { }) .unwrap(); } - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let mut reads = Vec::new(); while let Some(event) = consumer.read().await.unwrap() { reads.push(match event { @@ -1629,7 +1629,7 @@ mod tests { producer.discontinuity().unwrap(); } } - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let mut before = Vec::new(); let mut ended = None; let mut after = Vec::new(); @@ -1766,7 +1766,7 @@ mod tests { keyframe: true, }) .unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let first = next_frame(&mut consumer).await.unwrap().expect("decoded frame"); let first_frames = first.data.len() / size_of::(); @@ -1850,7 +1850,7 @@ mod tests { .unwrap(); producer.cut(None).unwrap(); } - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let expected = first_dtx.expect("silence should enter Opus DTX"); let mut actual = None; @@ -1928,7 +1928,7 @@ mod tests { }) .unwrap(); } - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let mut read = Vec::new(); while let Some(frame) = next_frame(&mut consumer).await.unwrap() { @@ -2079,7 +2079,7 @@ mod tests { // Same group: a new group at 1s would sit below the marker's live edge. write(&mut producer, 3 * 48_000, Bytes::new(), false); write(&mut producer, 48_000, dtx.payload, false); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let frame = next_frame(&mut consumer).await.unwrap().expect("decoded frame"); // The output begins with the first packet's samples, so it is stamped and @@ -2567,7 +2567,7 @@ mod tests { 2, crate::single(encoder.encode_all(&pcm).unwrap()).payload, ); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let first = next_frame(&mut consumer).await.unwrap().expect("first decoded frame"); let _second = next_frame(&mut consumer).await.unwrap().expect("second decoded frame"); diff --git a/rs/moq-audio/src/encode/capture.rs b/rs/moq-audio/src/encode/capture.rs index af802e637fe..b0a9f1352be 100644 --- a/rs/moq-audio/src/encode/capture.rs +++ b/rs/moq-audio/src/encode/capture.rs @@ -285,33 +285,44 @@ impl Driver { let end = self.drive().await; let mut producer = self.producer.take().unwrap(); let now = self.clock.now(); + // Held until the state closes, so the track outlives its terminal status. + let mut finished = None; let result = match end { Ok(End::Failed(failure)) => { // Retention already reports the failure. It stays the cause even when the track // can no longer end cleanly, such as one aborted while it still retained audio. - let ended = producer - .set_active(false, now) - .and_then(|()| producer.fail(moq_net::Error::Transport(failure.to_string()))); - if ended.is_err() { - producer.abort(moq_net::Error::Transport(failure.to_string())); + let cause = moq_net::Error::Transport(failure.to_string()); + match producer.set_active(false, now) { + Ok(()) => finished = producer.fail(cause).ok(), + Err(_) => producer.abort(cause), } Err(failure) } // The track closed, or encoding or publishing failed. - end => match end.and_then(|_| producer.set_active(false, now).and_then(|()| producer.finish())) { - Ok(()) => { - self.update(Status::Ended); - Ok(()) - } - Err(err) => { - self.update(Status::Failed(err.clone())); - producer.abort(moq_net::Error::Transport(err.to_string())); - Err(err) + end => { + let ended = match end.and_then(|_| producer.set_active(false, now)) { + // A finish that fails ends the track with an error itself. + Ok(()) => producer.finish().map(|track| finished = Some(track)), + Err(err) => { + producer.abort(moq_net::Error::Transport(err.to_string())); + Err(err) + } + }; + match ended { + Ok(()) => { + self.update(Status::Ended); + Ok(()) + } + Err(err) => { + self.update(Status::Failed(err.clone())); + Err(err) + } } - }, + } }; self.publish_level(Level::default()); let _ = self.state.close(); + drop(finished); result } diff --git a/rs/moq-audio/src/encode/mod.rs b/rs/moq-audio/src/encode/mod.rs index b4342eb408d..f6c62c8a511 100644 --- a/rs/moq-audio/src/encode/mod.rs +++ b/rs/moq-audio/src/encode/mod.rs @@ -28,7 +28,7 @@ mod capture; pub use encoded::Encoded; pub use encoder::{Codec, Config, Encoder, Finish, Input, Kind}; -pub use producer::{Options, Producer}; +pub use producer::{Finished, Options, Producer}; #[cfg(feature = "capture")] pub use capture::{Capture, Driver, Publication, State, Status, publish_capture}; diff --git a/rs/moq-audio/src/encode/producer.rs b/rs/moq-audio/src/encode/producer.rs index a82d2f061b5..1dd1c1ed996 100644 --- a/rs/moq-audio/src/encode/producer.rs +++ b/rs/moq-audio/src/encode/producer.rs @@ -78,9 +78,10 @@ impl Default for Options { /// a subscriber that opens the catalog before any frames arrive still sees the /// track. pub struct Producer { - /// The codec while input is accepted. Ending keeps the track and catalog rendition, not the - /// codec. - codec: Codec, + encoder: Encoder, + /// A publish that failed after the codec advanced past its packet: every later call reports the + /// cause rather than stamping what follows, and ending fails the track with it. + failed: Option, bitrate: moq_net::bandwidth::Rate, input: Input, /// Converts the input layout to the codec's, when they differ. @@ -113,33 +114,6 @@ pub struct Producer { activity: Activity, } -/// The codec while the producer accepts input, or how it ended. -enum Codec { - Live(Encoder), - /// [`finish`](Producer::finish) published every packet and ended the track. - Finished, - /// Ending failed, aborting the track: every later call reports the cause. - Failed(Error), -} - -impl Codec { - fn get(&self) -> Result<&Encoder, Error> { - match self { - Self::Live(encoder) => Ok(encoder), - Self::Finished => Err(moq_net::Error::Closed.into()), - Self::Failed(err) => Err(err.clone()), - } - } - - fn get_mut(&mut self) -> Result<&mut Encoder, Error> { - match self { - Self::Live(encoder) => Ok(encoder), - Self::Finished => Err(moq_net::Error::Closed.into()), - Self::Failed(err) => Err(err.clone()), - } - } -} - struct Terminal { packets: Vec, end: Timestamp, @@ -249,7 +223,8 @@ impl Reserved { active: true, reset_pending: false, bitrate, - codec: Codec::Live(registered.encoder), + encoder: registered.encoder, + failed: None, input: registered.input, remix: registered.remix, resampler: registered.resampler, @@ -299,6 +274,11 @@ impl Producer { } } + /// The cause of an earlier publish failure, which ended the track. + fn check(&self) -> Result<(), Error> { + self.failed.clone().map_or(Ok(()), Err) + } + /// The name of the published track, which is [`Options::track`] resolved. pub fn track_name(&self) -> &str { self.track.name() @@ -335,7 +315,8 @@ impl Producer { /// Retune the live encoder to `bitrate`. pub fn set_bitrate(&mut self, bitrate: moq_net::bandwidth::Rate) -> Result<(), Error> { - let encoder = self.codec.get_mut()?; + self.check()?; + let encoder = &mut self.encoder; let previous = encoder.bitrate(); encoder.set_bitrate(bitrate)?; // Advertise the configured target in place, so encoded-byte estimates cannot replace it. @@ -350,10 +331,6 @@ impl Producer { Ok(()) } - fn encoder(&self) -> Result<&Encoder, Error> { - self.codec.get() - } - /// Re-anchor the timeline to the next frame's timestamp, dropping any /// partial input frame. Call this when resuming after an idle gap (e.g. a /// released-then-reopened microphone) so the gap appears in the PTS and @@ -362,7 +339,7 @@ impl Producer { /// `reanchor`. If the codec had started, a marker group is published before /// the next packet so subscribers jump the playhead. pub fn reanchor(&mut self) { - if self.codec.get().is_ok_and(Encoder::started) || !self.backlog.is_empty() { + if self.encoder.started() || !self.backlog.is_empty() { self.pending_discontinuity = true; } self.reset_state(); @@ -374,10 +351,8 @@ impl Producer { self.frames_produced = 0; self.pending.clear(); self.reset_pending = true; - if self.backlog.is_empty() - && let Ok(encoder) = self.codec.get_mut() - { - encoder.reset(); + if self.backlog.is_empty() { + self.encoder.reset(); self.in_flight.clear(); } // The resampler holds samples of its own, plus filter state primed by them. @@ -403,11 +378,8 @@ impl Producer { /// /// [`Frame::activity`] is ignored: the encoder classifies what it actually /// produced, which [`activity`](Self::activity) reports. - /// - /// Writes after a successful [`finish`](Self::finish) fail with - /// [`moq_net::Error::Closed`], and after a failed one with its cause. pub fn write(&mut self, frame: &Frame) -> Result<(), Error> { - self.encoder()?; + self.check()?; let timestamp_us = u64::try_from(frame.timestamp.as_micros()) .map_err(|_| Error::Unsupported(format!("frame timestamp {:?} out of range", frame.timestamp)))?; let anchor_us = *self.anchor_us.get_or_insert(timestamp_us); @@ -435,7 +407,7 @@ impl Producer { /// Frames are read in place and the remainder moves to the front once, so a /// large batch costs linear rather than quadratic copying. fn publish_full_frames(&mut self, anchor_us: u64) -> Result<(), Error> { - let frame_samples = self.encoder()?.frame_size() * self.encoder()?.codec_channels() as usize; + let frame_samples = self.encoder.frame_size() * self.encoder.codec_channels() as usize; // Taken so each frame can be borrowed while `self` encodes it; restored below on every path. let mut pending = std::mem::take(&mut self.pending); let mut consumed = 0; @@ -456,11 +428,11 @@ impl Producer { let timestamp = Self::timestamp( anchor_us, self.frames_produced, - self.encoder()?.folded_delay(), - self.encoder()?.codec_rate(), + self.encoder.folded_delay(), + self.encoder.codec_rate(), )?; - self.frames_produced += self.encoder()?.frame_size() as u64; - let end = Self::timestamp(anchor_us, self.frames_produced, 0, self.encoder()?.codec_rate())?; + self.frames_produced += self.encoder.frame_size() as u64; + let end = Self::timestamp(anchor_us, self.frames_produced, 0, self.encoder.codec_rate())?; let reset = std::mem::take(&mut self.reset_pending); let discontinuity = std::mem::take(&mut self.pending_discontinuity); if self.active && self.backlog.is_empty() { @@ -484,13 +456,14 @@ impl Producer { reset: bool, discontinuity: bool, ) -> Result<(), Error> { + self.check()?; // A break before anything was published, such as history expiring before the first // subscriber, has nothing to separate. if discontinuity && !self.decoder_boundary { self.track.discontinuity()?; self.decoder_boundary = true; } - let encoder = self.codec.get_mut()?; + let encoder = &mut self.encoder; if reset { encoder.reset(); self.in_flight.clear(); @@ -503,18 +476,19 @@ impl Producer { /// the oldest held stamp, then `submitted`, the frame just encoded, which is queued only /// when the codec holds its packet, so a codec that holds nothing never fills the queue. fn publish_ready(&mut self, mut submitted: Option) -> Result<(), Error> { - while let Some(packet) = self.codec.get_mut()?.receive()? { + while let Some(packet) = self.encoder.receive()? { let Some(timestamp) = self.in_flight.pop_front().or_else(|| submitted.take()) else { return Err(Error::Codec(format!( "{} returned more packets than it was given frames", - self.encoder()?.name() + self.encoder.name() ))); }; self.activity = packet.activity; // The codec and its timestamp queue already advanced past this packet, so no later // write could stamp what follows: end the producer with the cause. if let Err(err) = self.publish(packet, timestamp) { - return self.close(Err(err)); + self.failed = Some(err.clone()); + return Err(err); } self.decoder_boundary = false; } @@ -556,9 +530,9 @@ impl Producer { if !self.backlog.is_empty() { return Ok(()); } - let channels = self.encoder()?.codec_channels() as usize; + let channels = self.encoder.codec_channels() as usize; let frames = self.frames_produced + (self.pending.len() / channels) as u64; - let end = Self::timestamp(anchor_us, frames, 0, self.encoder()?.codec_rate())?; + let end = Self::timestamp(anchor_us, frames, 0, self.encoder.codec_rate())?; if self.backlog.expired(end, now) { self.reanchor(); } @@ -609,7 +583,7 @@ impl Producer { duration: None, }; self.track.write(mux_frame)?; - self.grouped += self.encoder()?.frame_size() as u64; + self.grouped += self.encoder.frame_size() as u64; // Cut as soon as the group is long enough rather than when the next packet arrives, so // the relay sees it finish without waiting. No boundary to give: the next packet bounds // this one, and codec frames have a deterministic duration anyway. @@ -664,65 +638,45 @@ impl Producer { /// Flush pending samples, resampler output, and codec lookahead, then finalize /// the track. /// - /// Borrows rather than consumes, so a later [`abort`](Self::abort) can still - /// run after a successful finish. The codec and input scratch are released, while the - /// track and its catalog rendition stay published. Writes and bitrate changes then - /// fail with [`moq_net::Error::Closed`]. A finish that fails ends the track with an error - /// instead, and every later call returns its cause. - pub fn finish(&mut self) -> Result<(), Error> { + /// Consumes the producer, releasing the codec and input scratch. The returned [`Finished`] + /// keeps the track and its catalog rendition published, and can still + /// [`abort`](Finished::abort) them. A finish that fails ends the track with an error instead. + pub fn finish(self) -> Result { self.end(None) } /// [`finish`](Self::finish) the track with `err`, for a source that failed: subscribers keep /// every packet published, then get `err` instead of the end of stream. #[cfg(feature = "capture")] - pub(crate) fn fail(&mut self, err: moq_net::Error) -> Result<(), Error> { + pub(crate) fn fail(self, err: moq_net::Error) -> Result { self.end(Some(err)) } - fn end(&mut self, err: Option) -> Result<(), Error> { - match &self.codec { - Codec::Live(_) => {} - Codec::Finished => return Ok(()), - Codec::Failed(cause) => return Err(cause.clone()), - } - let result = self.drain(err); - self.close(result) - } - - /// Settle the codec as finished or failed with `result`, releasing encoding resources. - fn close(&mut self, result: Result<(), Error>) -> Result<(), Error> { - if !matches!(self.codec, Codec::Live(_)) { - return result; + /// Publish everything still held, then end the track with `err` or cleanly. + fn end(mut self, err: Option) -> Result { + if let Err(cause) = self.check().and_then(|()| self.drain()) { + // Every published packet stays readable, then subscribers get an error rather than an + // end that would claim the source was published whole: `err` when the source failed. + let _ = self.track.fail(err.unwrap_or_else(|| moq_net::StreamError::Internal.into())); + return Err(cause); } - self.codec = match &result { - Ok(()) => Codec::Finished, - Err(cause) => { - // Every published packet stays readable, then subscribers get an error rather than an - // end that would claim the source was published whole. - let _ = self.track.fail(moq_net::StreamError::Internal.into()); - Codec::Failed(cause.clone()) - } + let track = match err { + None => self.track.finish()?, + Some(err) => self.track.fail(err)?, }; - self.pending = Vec::new(); - self.in_flight = VecDeque::new(); - self.remix = None; - self.resampler = None; - result + Ok(Finished { track }) } - /// Publish everything still held, then end the track with `err` or cleanly. - fn drain(&mut self, err: Option) -> Result<(), Error> { + /// Publish everything the codec, resampler, and backlog still hold. + fn drain(&mut self) -> Result<(), Error> { // Preserve the final retained history after capture ends, even without live demand. self.active = true; while let Some(raw) = self.backlog.pop() { self.track.history(raw.timestamp); self.encode_frame(&raw.samples, raw.timestamp, raw.reset, raw.discontinuity)?; } - if self.reset_pending - && let Ok(encoder) = self.codec.get_mut() - { - encoder.reset(); + if self.reset_pending { + self.encoder.reset(); self.in_flight.clear(); } // Whatever the resampler still holds belongs to this track: its last partial @@ -738,12 +692,12 @@ impl Producer { self.publish_full_frames(anchor_us)?; // What the codec still holds came from full frames, so it goes out at their stamps, leaving // the terminal drain's packets to start where the source's partial tail does. - self.codec.get_mut()?.flush()?; + self.encoder.flush()?; self.publish_ready(None)?; if !self.in_flight.is_empty() { return Err(Error::Codec(format!( "{} kept {} packet(s) past its flush", - self.encoder()?.name(), + self.encoder.name(), self.in_flight.len() ))); } @@ -754,15 +708,15 @@ impl Producer { self.decoder_boundary = true; } - let frame_size = self.encoder()?.frame_size(); - let codec_rate = self.encoder()?.codec_rate(); - let channels = self.encoder()?.codec_channels() as usize; + let frame_size = self.encoder.frame_size(); + let codec_rate = self.encoder.codec_rate(); + let channels = self.encoder.codec_channels() as usize; let source_frames = self.pending.len() / channels; - let delay = self.encoder()?.folded_delay(); + let delay = self.encoder.folded_delay(); let start = Self::timestamp(anchor_us, self.frames_produced, delay, codec_rate)?; // The source ends where it ends: priming only moves the packets carrying it. let end = Self::timestamp(anchor_us, self.frames_produced + source_frames as u64, 0, codec_rate)?; - let finish = self.codec.get_mut()?.drain(&self.pending)?; + let finish = self.encoder.drain(&self.pending)?; let discard_padding = finish.discard_padding(); let packets = finish.into_packets(); @@ -785,18 +739,44 @@ impl Producer { self.frames_produced += frame_size as u64; } } - - match err { - None => self.track.finish()?, - Some(err) => self.track.fail(err)?, - } Ok(()) } /// Abort the track with `err` instead of finishing it, so subscribers see the /// real cause rather than [`moq_net::Error::Dropped`]. Pending samples are dropped. - /// - /// Consumes the producer. Still callable after [`finish`](Self::finish). + pub fn abort(self, err: moq_net::Error) { + self.track.abort(err); + } +} + +/// An audio track whose [`Producer`] [finished](Producer::finish) it: every packet is published, +/// and it can still be watched or aborted, but not written to. +/// +/// Dropping it releases the track like dropping a [`Producer`]. +/// +/// ```compile_fail,E0599 +/// # fn cannot_write(track: moq_audio::encode::Finished, frame: &moq_audio::Frame) { +/// track.write(frame).unwrap(); +/// # } +/// ``` +#[must_use = "dropping it releases the track and its catalog rendition"] +pub struct Finished { + track: moq_mux::container::Finished, +} + +impl Finished { + /// The name of the published track, which is [`Options::track`] resolved. + pub fn track_name(&self) -> &str { + self.track.name() + } + + /// A watch-only handle to the track's subscriber demand; see [`Producer::demand`]. + pub fn demand(&self) -> moq_net::track::Demand { + self.track.demand() + } + + /// Abort the track with `err`, so subscribers that have not drained it see the real cause; see + /// [`Producer::abort`]. pub fn abort(self, err: moq_net::Error) { self.track.abort(err); } @@ -901,9 +881,7 @@ mod tests { idle.as_secs_f64() * 1e3, catchup.as_secs_f64() * 1e3 ); - for producer in &mut producers { - producer.finish().unwrap(); - } + let _finished: Vec<_> = producers.into_iter().map(|producer| producer.finish().unwrap()).collect(); } } } @@ -939,7 +917,7 @@ mod tests { )) .unwrap(); } - assert!(!producer.encoder().unwrap().started(), "no codec work without demand"); + assert!(!producer.encoder.started(), "no codec work without demand"); let track = consumer .track("audio") .unwrap() @@ -979,7 +957,7 @@ mod tests { .write(&pcm_frame(&vec![0.5; 1920], at(151).as_micros() as u64)) .unwrap(); assert_eq!(packet(&mut reader).await.unwrap().timestamp, at(151)); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); } /// Replayed groups reach the track as fresh arrivals, so only the encoder can tell they are old: @@ -1022,7 +1000,7 @@ mod tests { panic!("expected a replayed frame"); }; assert_eq!(frame.timestamp, at(143)); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); } /// Replay catches up behind the live edge another track already reached; advertising that as @@ -1061,13 +1039,13 @@ mod tests { .write(&pcm_frame(&vec![0.1; 1920], at(index).as_micros() as u64)) .unwrap(); } - producer.finish().unwrap(); + let producer = producer.finish().unwrap(); let delay = producer.track.estimate().delay; assert!( delay.is_none_or(|delay| delay < Duration::from_millis(100)), "{delay:?}" ); - other.finish().unwrap(); + let _finished = other.finish().unwrap(); } /// History expiring right before capture ends still marks the gap before the final partial frame. @@ -1105,7 +1083,7 @@ mod tests { producer .set_active(false, Timestamp::from_micros(31_045_000).unwrap()) .unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let first = reader.read().await.unwrap().unwrap(); assert!(matches!(first, moq_mux::container::Read::Frame { .. }), "{first:?}"); @@ -1150,7 +1128,7 @@ mod tests { producer .set_active(false, Timestamp::from_micros(31_100_000).unwrap()) .unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let first = reader.read().await.unwrap().unwrap(); assert!( @@ -1181,8 +1159,8 @@ mod tests { .set_active(true, Timestamp::from_micros(30_020_000).unwrap()) .unwrap(); assert!(producer.backlog.is_empty()); - assert!(!producer.encoder().unwrap().started()); - producer.finish().unwrap(); + assert!(!producer.encoder.started()); + let _finished = producer.finish().unwrap(); } #[tokio::test] @@ -1194,7 +1172,7 @@ mod tests { track: Some("audio".into()), ..Options::default() }; - let mut producer = Producer::new(&mut broadcast, catalog, Input::default(), &options).unwrap(); + let producer = Producer::new(&mut broadcast, catalog, Input::default(), &options).unwrap(); let mut demand = producer.demand(); assert_eq!(demand.name(), "audio"); @@ -1222,8 +1200,7 @@ mod tests { .unwrap(); assert_eq!(demand.wanted(), None); - producer.finish().unwrap(); - drop(producer); + drop(producer.finish().unwrap()); let closed = kio::clock::timeout(Duration::from_secs(1), demand.closed()) .await .expect("producer closed"); @@ -1255,20 +1232,8 @@ mod tests { producer.write(&pcm_frame(&vec![0.1; 4096], 0)).unwrap(); assert!(producer.pending.capacity() > 0); assert!(producer.remix.is_some()); - let bitrate = producer.bitrate(); - producer.finish().unwrap(); + retired.push(producer.finish().unwrap()); assert_eq!(backend.resources(), 0, "retired {index} codecs"); - assert_eq!(producer.pending.capacity(), 0); - assert_eq!(producer.in_flight.capacity(), 0); - assert!(producer.remix.is_none()); - assert!(producer.resampler.is_none()); - assert_eq!(producer.bitrate(), bitrate); - producer.finish().unwrap(); - assert!(matches!( - producer.set_bitrate(bitrate), - Err(Error::Net(moq_net::Error::Closed)) - )); - retired.push(producer); // Retention still owns both the published track and its catalog rendition. let mut reader = consumer @@ -1337,7 +1302,7 @@ mod tests { pcm[impulse] = 1.0; let data: Vec = pcm.iter().flat_map(|sample| sample.to_le_bytes()).collect(); producer.write(&Frame::new(Bytes::from(data), Timestamp::ZERO)).unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let mut decoded = Vec::new(); while let Some(frame) = decoded_frame(&mut audio).await.unwrap() { @@ -1393,7 +1358,7 @@ mod tests { let at = (index * batch) as u64 * 1_000_000 / input_rate as u64; producer.write(&pcm_frame(chunk, 1_000_000 + at)).unwrap(); } - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let mut published = Vec::new(); while let Some(read) = reader.read().await.unwrap() { @@ -1468,7 +1433,7 @@ mod tests { producer .write(&Frame::new(data.into(), moq_net::Timestamp::ZERO)) .unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let mut packets = 0; while packet(&mut track).await.is_some() { @@ -1477,40 +1442,6 @@ mod tests { assert_eq!(packets, 11); } - /// A failed finish is final: writes are refused with its cause rather than reaching the codec - /// at the input rate, now that the resampler is gone, and a retry reports the cause rather than - /// publishing the terminal packets again. - #[tokio::test] - async fn a_failed_finish_refuses_later_calls() { - let input = Input { - format: Format::F32, - sample_rate: 44_100, - layout: Layout::Mono, - }; - let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); - let options = Options { - track: Some("audio".to_string()), - config: Config::new(48_000, Layout::Mono), - ..Options::default() - }; - let mut producer = Producer::new(&mut broadcast, catalog, input, &options).unwrap(); - let frame = |samples: usize| { - let data: Vec = vec![0.25f32; samples].iter().flat_map(|s| s.to_le_bytes()).collect(); - Frame::new(data.into(), moq_net::Timestamp::ZERO) - }; - producer.write(&frame(4_410)).unwrap(); - - // Another handle ends the track, so finishing cannot publish the tail. - producer.track.track().clone().abort(moq_net::Error::Cancel).unwrap(); - let cause = producer.finish().unwrap_err().to_string(); - - // Short of a codec frame, so nothing but the guard could refuse it. - let err = producer.write(&frame(100)).unwrap_err(); - assert_eq!(err.to_string(), cause); - assert_eq!(producer.finish().unwrap_err().to_string(), cause); - } - /// `reanchor` promises to drop buffered samples, and the resampler buffers /// samples of its own. Leaving those behind let `finish` flush pre-reset audio /// onto the track, stamped at an anchor that no longer exists. @@ -1548,7 +1479,7 @@ mod tests { .unwrap(); producer.reanchor(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); // The reset dropped everything, so the track ends without a packet. assert!(track.read().await.unwrap().is_none()); @@ -1586,7 +1517,7 @@ mod tests { producer.write(&full_frame(1_000_000)).unwrap(); producer.reanchor(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); assert!(packet(&mut track).await.is_some()); assert!(packet(&mut track).await.is_none()); @@ -1637,7 +1568,7 @@ mod tests { producer.reanchor(); producer.write(&full_frame(1_000_000)).unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let mut resumed_frames = 0; while let Some(frame) = decoded_frame(&mut audio).await.unwrap() { @@ -1856,7 +1787,7 @@ mod tests { } assert_eq!(count, packets, "{frame_us} us packets"); } - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); } } @@ -2003,7 +1934,7 @@ mod tests { // The stub holds the third frame, so only `finish` releases it. producer.write(&pcm_frame(&[0.1; 3 * 1024], 1_000_000)).unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let mut published = Vec::new(); while let Some(frame) = packet(&mut reader).await { @@ -2085,33 +2016,6 @@ mod tests { assert_eq!(pts, vec![0, 5_000_000]); } - /// A sub-frame write never reaches the closed track, so the producer must - /// refuse it itself rather than buffering samples that cannot be published. - #[tokio::test] - async fn write_after_finish_is_closed() { - let mut broadcast = moq_net::broadcast::Info::new().produce(); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); - let options = Options { - track: Some("audio".to_string()), - ..Options::default() - }; - let mut producer = Producer::new( - &mut broadcast, - catalog, - Input { - layout: Layout::Mono, - ..Input::default() - }, - &options, - ) - .unwrap(); - - producer.finish().unwrap(); - let err = producer.write(&pcm_frame(&[0.1; 100], 0)).unwrap_err(); - assert!(matches!(err, Error::Net(moq_net::Error::Closed))); - producer.abort(moq_net::Error::Cancel); - } - /// `Options::track = None` derives a codec-suffixed name rather than making /// the caller invent one, mirroring the video side. Pins the exact name the /// docs promise, and that a second producer doesn't collide with the first. diff --git a/rs/moq-audio/tests/integration/import.rs b/rs/moq-audio/tests/integration/import.rs index 413705a061c..36948a62109 100644 --- a/rs/moq-audio/tests/integration/import.rs +++ b/rs/moq-audio/tests/integration/import.rs @@ -17,7 +17,7 @@ async fn ts_surround_opus_decodes() { let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); let mut import = moq_mux::container::ts::Import::new(broadcast, catalog.reserve()); import.decode(&data[..]).unwrap(); - import.finish().unwrap(); + let _finished = import.finish().unwrap(); let snapshot = catalog.snapshot(); let (name, config) = snapshot.audio.renditions.iter().next().expect("an Opus track"); diff --git a/rs/moq-audio/tests/integration/roundtrip.rs b/rs/moq-audio/tests/integration/roundtrip.rs index 706f6d246ba..3baf277fd47 100644 --- a/rs/moq-audio/tests/integration/roundtrip.rs +++ b/rs/moq-audio/tests/integration/roundtrip.rs @@ -94,7 +94,7 @@ async fn opus_roundtrip(case: Case) { let mut consumer = decode::Consumer::new(&source, rendition, "audio", options) .await .unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); // Decoded from the track's first packet, so only the codec's pre-skip is dropped. let mut pcm = Vec::new(); while let Some(frame) = next_frame(&mut consumer).await.unwrap() { @@ -168,7 +168,7 @@ async fn pcm_round_trip_is_lossless() { let mut consumer = decode::Consumer::new(&broadcast_consumer, catalog, "pcm", decode::Options::default()) .await .unwrap(); - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); let frame = next_frame(&mut consumer).await.unwrap().unwrap(); assert_eq!(frame.timestamp, Timestamp::from_micros(123_000).unwrap()); @@ -232,7 +232,7 @@ async fn pcm_five_one_reads_back_at_any_layout() { .unwrap(), ); } - producer.finish().unwrap(); + let _finished = producer.finish().unwrap(); for (mut consumer, (_, layout, frame)) in consumers.into_iter().zip(cases) { assert_eq!(consumer.layout(), layout); diff --git a/rs/moq-c/src/audio.rs b/rs/moq-c/src/audio.rs index 3fb66bbee92..30db9020c99 100644 --- a/rs/moq-c/src/audio.rs +++ b/rs/moq-c/src/audio.rs @@ -540,18 +540,20 @@ pub extern "C" fn moq_encode_audio_finish(producer: u32) -> i32 { let shared = State::lock().audio.producer(id)?; let finished: Result<(), Error> = (|| { let mut guard = shared.lock(); - let AudioEncoder { - mut producer, children, .. - } = guard.take().ok_or(Error::MediaNotFound)?; - if let Err(err) = producer.finish() { - // Readers see the failure rather than `Dropped`, as a failed video drain does. - children.forget(crate::publish::Child::Audio(id)); - producer.abort(moq_net::Error::Transport(err.to_string())); - return Err(err.into()); + let AudioEncoder { producer, children, .. } = guard.take().ok_or(Error::MediaNotFound)?; + match producer.finish() { + // The broadcast keeps the finished track, so its finish still holds it. + Ok(finished) => { + children.keep(crate::publish::Child::Audio(id), finished); + Ok(()) + } + // A failed finish already ended the track with an error, so readers see the + // failure rather than `Dropped`, as a failed video drain does. + Err(err) => { + children.forget(crate::publish::Child::Audio(id)); + Err(err.into()) + } } - // The broadcast keeps the finished track, so its finish still holds it. - children.keep(crate::publish::Child::Audio(id), producer); - Ok(()) })(); // A racing close may have released the id already. let _ = State::lock().audio.remove(id); diff --git a/rs/moq-c/src/publish.rs b/rs/moq-c/src/publish.rs index 9b4e07cdd9d..dd0659c45e5 100644 --- a/rs/moq-c/src/publish.rs +++ b/rs/moq-c/src/publish.rs @@ -718,17 +718,24 @@ impl Publish { pub fn container_finish(&mut self, container: Id) -> Result<(), Error> { let id = container; - let mut container = self.containers.remove(id).ok_or(Error::MediaNotFound)?; - if let Err(err) = container.inner.finish() { - container.children.forget(Child::Container(id)); - let driver = container.driver.clone(); - container.abort(cause(&err)); - driver.check()?; - return Err(err.into()); + let Container { + inner, + driver, + children, + } = self.containers.remove(id).ok_or(Error::MediaNotFound)?; + match inner.finish() { + Ok(finished) => { + driver.check()?; + children.keep(Child::Container(id), finished); + Ok(()) + } + // A failed finish already ended every track, with an error where it could not finish. + Err(err) => { + children.forget(Child::Container(id)); + driver.check()?; + Err(err.into()) + } } - container.driver.check()?; - container.children.keep(Child::Container(id), container.inner); - Ok(()) } /// Insert or replace a caller-authored video rendition in the broadcast's catalog. @@ -1487,15 +1494,17 @@ impl Publish { pub fn json_window_finish(&mut self, window: Id) -> Result<(), Error> { let id = window; let window = self.json_window.remove(id).ok_or(Error::TrackNotFound)?; - let Data { - mut producer, children, .. - } = window.data; - if let Err(err) = producer.finish() { - children.forget(Child::JsonWindow(id)); - return Err(err.into()); + let Data { producer, children, .. } = window.data; + match producer.finish() { + Ok(finished) => { + children.keep(Child::JsonWindow(id), finished); + Ok(()) + } + Err(err) => { + children.forget(Child::JsonWindow(id)); + Err(err.into()) + } } - children.keep(Child::JsonWindow(id), producer); - Ok(()) } /// Write a frame into a raw group, `None` when untimed. diff --git a/rs/moq-cli/src/hls/record.rs b/rs/moq-cli/src/hls/record.rs index f091ede1519..f0b936ad49c 100644 --- a/rs/moq-cli/src/hls/record.rs +++ b/rs/moq-cli/src/hls/record.rs @@ -606,6 +606,7 @@ mod tests { broadcast: moq_net::origin::Route, catalog: moq_mux::catalog::Producer, video: Vec