Skip to content

Commit 66ad7f6

Browse files
kixelatedclaude
andauthored
moq-net: serve one subscription and N fetches concurrently per track (#1634)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a812808 commit 66ad7f6

6 files changed

Lines changed: 960 additions & 572 deletions

File tree

rs/moq-native/tests/broadcast.rs

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ async fn lite05_fetch_roundtrip(scheme: &str) {
300300
// Fetch group 0 directly, without subscribing. No live producer holds the group
301301
// on the client, so this issues a wire FETCH upstream.
302302
let mut group_sub = tokio::time::timeout(TIMEOUT, async {
303-
bc.track("video").unwrap().fetch(0, None).unwrap().await
303+
bc.track("video").unwrap().fetch_group(0, None).unwrap().await
304304
})
305305
.await
306306
.expect("fetch timed out")
@@ -346,6 +346,131 @@ async fn broadcast_moq_lite_05_fetch_webtransport() {
346346
lite05_fetch_roundtrip("https").await;
347347
}
348348

349+
/// A fetch must be served while a live subscription is active on the same track.
350+
/// The relay subscribes starting at the latest group, so an older group isn't
351+
/// cached and the fetch has to issue a wire FETCH concurrently with the
352+
/// subscription. Older relays served a subscription OR a fetch, never both, so
353+
/// this fetch would have hung.
354+
async fn lite05_fetch_during_subscribe(scheme: &str) {
355+
use moq_native::moq_net::{Timescale, Timestamp};
356+
357+
fn timestamped_frame(us: u64, payload: &str) -> moq_net::Frame {
358+
moq_net::Frame {
359+
size: payload.len() as u64,
360+
timestamp: Some(Timestamp::new(us, Timescale::MICRO).unwrap()),
361+
}
362+
}
363+
364+
let pub_origin = Origin::random().produce();
365+
let mut broadcast = pub_origin.create_broadcast("test").expect("failed to create broadcast");
366+
let mut track = broadcast
367+
.create_track("video", moq_net::TrackInfo::default().with_timescale(Timescale::MICRO))
368+
.expect("failed to create track");
369+
370+
// Group 0 is the "past" group only reachable via FETCH; group 1 is the latest,
371+
// delivered live over the subscription.
372+
let mut group0 = track.append_group().expect("append group 0"); // seq 0
373+
let mut w = group0.create_frame(timestamped_frame(10_000, "old")).expect("frame 0");
374+
w.write(bytes::Bytes::from_static(b"old")).expect("write 0");
375+
w.finish().expect("finish frame 0");
376+
group0.finish().expect("finish group 0");
377+
378+
let mut group1 = track.append_group().expect("append group 1"); // seq 1
379+
let mut w = group1.create_frame(timestamped_frame(20_000, "new")).expect("frame 1");
380+
w.write(bytes::Bytes::from_static(b"new")).expect("write 1");
381+
w.finish().expect("finish frame 1");
382+
group1.finish().expect("finish group 1");
383+
384+
let mut server_config = moq_native::ServerConfig::default();
385+
server_config.bind = Some("[::]:0".to_string());
386+
server_config.tls.generate = vec!["localhost".into()];
387+
server_config.version = vec!["moq-lite-05-wip".parse().unwrap()];
388+
let mut server = server_config.init().expect("failed to init server");
389+
let addr = server.local_addr().expect("failed to get local addr");
390+
391+
let sub_origin = Origin::random().produce();
392+
let mut announcements = sub_origin.consume().announced();
393+
394+
let mut client_config = moq_native::ClientConfig::default();
395+
client_config.tls.disable_verify = Some(true);
396+
client_config.version = vec!["moq-lite-05-wip".parse().unwrap()];
397+
let client = client_config.init().expect("failed to init client");
398+
let url: url::Url = format!("{scheme}://localhost:{}", addr.port()).parse().unwrap();
399+
400+
let server_handle = tokio::spawn(async move {
401+
let request = server.accept().await.expect("no incoming connection");
402+
let session = request.with_publisher(pub_origin.clone()).ok().await?;
403+
let _broadcast = broadcast;
404+
let _track = track;
405+
let _ = session.closed().await;
406+
Ok::<_, anyhow::Error>(())
407+
});
408+
409+
let client = client.with_consumer(sub_origin);
410+
let session = tokio::time::timeout(TIMEOUT, client.connect(url))
411+
.await
412+
.expect("client connect timed out")
413+
.expect("client connect failed");
414+
415+
let (path, bc) = tokio::time::timeout(TIMEOUT, announcements.next())
416+
.await
417+
.expect("announce timed out")
418+
.expect("origin closed");
419+
assert_eq!(path.as_str(), "test");
420+
let bc = bc.broadcast().expect("expected announce, got unannounce");
421+
422+
// Subscribe (starts at the latest group) and read the live group, which
423+
// establishes the upstream subscription and leaves it active.
424+
let mut track_sub = tokio::time::timeout(TIMEOUT, async {
425+
bc.track("video").unwrap().subscribe(None).unwrap().await
426+
})
427+
.await
428+
.expect("subscribe timed out")
429+
.expect("subscribe failed");
430+
let mut live = tokio::time::timeout(TIMEOUT, track_sub.recv_group())
431+
.await
432+
.expect("recv_group timed out")
433+
.expect("recv_group failed")
434+
.expect("track closed prematurely");
435+
assert_eq!(live.sequence, 1);
436+
let frame = tokio::time::timeout(TIMEOUT, live.read_frame())
437+
.await
438+
.expect("read_frame timed out")
439+
.expect("read_frame failed")
440+
.expect("group closed prematurely");
441+
assert_eq!(&*frame, b"new");
442+
443+
// While the subscription is still held and active, fetch the older group. The
444+
// relay doesn't have it cached (subscription started at the latest), so this
445+
// must issue a wire FETCH concurrently with the live subscription.
446+
let mut fetched = tokio::time::timeout(TIMEOUT, async {
447+
bc.track("video").unwrap().fetch_group(0, None).unwrap().await
448+
})
449+
.await
450+
.expect("fetch timed out")
451+
.expect("fetch failed");
452+
assert_eq!(fetched.sequence, 0);
453+
let frame = tokio::time::timeout(TIMEOUT, fetched.read_frame())
454+
.await
455+
.expect("fetch read_frame timed out")
456+
.expect("fetch read_frame failed")
457+
.expect("fetched group closed prematurely");
458+
assert_eq!(&*frame, b"old");
459+
460+
// The live subscription is unaffected: a freshly published group still arrives.
461+
drop(session);
462+
server_handle
463+
.await
464+
.expect("server task panicked")
465+
.expect("server task failed");
466+
}
467+
468+
#[tracing_test::traced_test]
469+
#[tokio::test]
470+
async fn broadcast_moq_lite_05_fetch_during_subscribe_webtransport() {
471+
lite05_fetch_during_subscribe("https").await;
472+
}
473+
349474
/// On Lite05 a publisher that doesn't advertise a timescale still works:
350475
/// SUBSCRIBE_OK carries `timescale = 0` and neither side encodes a
351476
/// per-frame timestamp byte. Subscribers receive `frame.timestamp = None`.

rs/moq-net/src/lite/publisher.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -582,7 +582,7 @@ impl<S: web_transport_trait::Session> Publisher<S> {
582582

583583
let group = broadcast
584584
.track(&fetch.track)?
585-
.fetch(
585+
.fetch_group(
586586
fetch.group,
587587
crate::Fetch {
588588
priority: fetch.priority,

0 commit comments

Comments
 (0)