Skip to content

Commit da931f7

Browse files
kixelatedclaude
andauthored
fix(moq-mux): keep catalog Consumer Clone + stable FramedFormat discriminants (#1661)
Co-authored-by: Claude <noreply@anthropic.com>
1 parent af71f54 commit da931f7

5 files changed

Lines changed: 59 additions & 10 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rs/moq-json/src/lib.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,20 @@ pub struct Consumer<T> {
224224
_marker: PhantomData<fn() -> T>,
225225
}
226226

227+
// Manual impl so cloning doesn't require `T: Clone`; `T` only lives in PhantomData.
228+
// Cloned readers inherit the current reconstruction state, then advance in parallel.
229+
impl<T> Clone for Consumer<T> {
230+
fn clone(&self) -> Self {
231+
Self {
232+
track: self.track.clone(),
233+
group: self.group.clone(),
234+
current: self.current.clone(),
235+
frames_read: self.frames_read,
236+
_marker: PhantomData,
237+
}
238+
}
239+
}
240+
227241
impl<T: DeserializeOwned> Consumer<T> {
228242
/// Create a consumer reading from the given track subscriber.
229243
pub fn new(track: moq_net::TrackConsumer) -> Self {
@@ -462,4 +476,36 @@ mod test {
462476
}
463477
assert_eq!(last.unwrap(), json!({ "a": 3 }));
464478
}
479+
480+
#[test]
481+
fn cloned_consumer_reconstructs_independently() {
482+
// Deltas share one group, so a clone taken mid-group carries in-progress reconstruction state.
483+
let config = Config {
484+
delta_ratio: Some(100.0),
485+
};
486+
let (mut producer, track) = producer(config);
487+
let mut consumer = Consumer::<Value>::new(track);
488+
let waiter = kio::Waiter::noop();
489+
490+
producer.update(&json!({ "a": 1, "b": 1 })).unwrap(); // snapshot, group 0
491+
match consumer.poll_next(&waiter) {
492+
Poll::Ready(Ok(Some(value))) => assert_eq!(value, json!({ "a": 1, "b": 1 })),
493+
other => panic!("expected snapshot, got {other:?}"),
494+
}
495+
496+
// Clone after the snapshot: the copy inherits `current`/`frames_read` and an independent cursor.
497+
let mut clone = consumer.clone();
498+
499+
producer.update(&json!({ "a": 1, "b": 2 })).unwrap(); // delta in group 0
500+
producer.finish().unwrap();
501+
502+
// Each consumer applies the delta on top of its own reconstruction state.
503+
let expected = json!({ "a": 1, "b": 2 });
504+
for consumer in [&mut consumer, &mut clone] {
505+
match consumer.poll_next(&waiter) {
506+
Poll::Ready(Ok(Some(value))) => assert_eq!(value, expected),
507+
other => panic!("expected delta, got {other:?}"),
508+
}
509+
}
510+
}
465511
}

rs/moq-mux/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ authors = ["Luke Curley <kixelated@gmail.com>"]
55
repository = "https://github.com/moq-dev/moq"
66
license = "MIT OR Apache-2.0"
77

8-
version = "0.5.3"
8+
version = "0.5.4"
99
edition = "2024"
1010
rust-version.workspace = true
1111

rs/moq-mux/src/catalog/hang/consumer.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use crate::Result;
88
///
99
/// This wraps a [`moq_json::Consumer`], reconstructing the JSON catalog from the latest
1010
/// group's snapshot (plus any future deltas) to discover available audio and video tracks.
11+
#[derive(Clone)]
1112
pub struct Consumer {
1213
inner: moq_json::Consumer<Catalog>,
1314
}

rs/moq-mux/src/import.rs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,6 @@ pub enum FramedFormat {
2828
Hev1,
2929
/// AV1 with inline sequence headers
3030
Av01,
31-
/// VP8 (one frame per buffer; not self-delimiting).
32-
Vp8,
33-
/// VP9 (one frame per buffer; not self-delimiting).
34-
Vp9,
3531
/// Raw AAC frames (not ADTS).
3632
Aac,
3733
/// Raw Opus frames (not Ogg).
@@ -40,6 +36,12 @@ pub enum FramedFormat {
4036
Mkv,
4137
/// MPEG-TS (transport stream) container.
4238
Ts,
39+
// New variants go at the end: this enum has no repr, so inserting in the
40+
// middle would shift the implicit discriminants of everything after it.
41+
/// VP8 (one frame per buffer; not self-delimiting).
42+
Vp8,
43+
/// VP9 (one frame per buffer; not self-delimiting).
44+
Vp9,
4345
}
4446

4547
impl FromStr for FramedFormat {
@@ -52,12 +54,12 @@ impl FromStr for FramedFormat {
5254
"hev1" => Ok(FramedFormat::Hev1),
5355
"fmp4" | "cmaf" => Ok(FramedFormat::Fmp4),
5456
"av01" | "av1" | "av1c" | "av1C" => Ok(FramedFormat::Av01),
55-
"vp8" | "vp08" => Ok(FramedFormat::Vp8),
56-
"vp9" | "vp09" => Ok(FramedFormat::Vp9),
5757
"aac" => Ok(FramedFormat::Aac),
5858
"opus" => Ok(FramedFormat::Opus),
5959
"mkv" | "webm" | "matroska" => Ok(FramedFormat::Mkv),
6060
"ts" | "mpegts" | "mpeg2ts" | "m2ts" => Ok(FramedFormat::Ts),
61+
"vp8" | "vp08" => Ok(FramedFormat::Vp8),
62+
"vp9" | "vp09" => Ok(FramedFormat::Vp9),
6163
_ => Err(Error::UnknownFormat(s.to_string())),
6264
}
6365
}
@@ -71,12 +73,12 @@ impl fmt::Display for FramedFormat {
7173
FramedFormat::Fmp4 => write!(f, "fmp4"),
7274
FramedFormat::Hev1 => write!(f, "hev1"),
7375
FramedFormat::Av01 => write!(f, "av01"),
74-
FramedFormat::Vp8 => write!(f, "vp8"),
75-
FramedFormat::Vp9 => write!(f, "vp9"),
7676
FramedFormat::Aac => write!(f, "aac"),
7777
FramedFormat::Opus => write!(f, "opus"),
7878
FramedFormat::Mkv => write!(f, "mkv"),
7979
FramedFormat::Ts => write!(f, "ts"),
80+
FramedFormat::Vp8 => write!(f, "vp8"),
81+
FramedFormat::Vp9 => write!(f, "vp9"),
8082
}
8183
}
8284
}

0 commit comments

Comments
 (0)