@@ -229,6 +229,123 @@ async fn broadcast_moq_lite_05_timestamps_webtransport() {
229229 lite05_timestamp_roundtrip ( "https" ) . await ;
230230}
231231
232+ /// Lite05 FETCH round-trip: retrieve a past group by sequence without holding a
233+ /// subscription, exercising the FETCH / FETCH_OK control flow and per-frame
234+ /// timestamp decoding on the fetch stream.
235+ async fn lite05_fetch_roundtrip ( scheme : & str ) {
236+ use moq_native:: moq_net:: { Timescale , Timestamp } ;
237+
238+ let pub_origin = Origin :: random ( ) . produce ( ) ;
239+ let mut broadcast = pub_origin. create_broadcast ( "test" ) . expect ( "failed to create broadcast" ) ;
240+ let mut track = broadcast
241+ . create_track ( "video" , moq_net:: TrackInfo :: default ( ) . with_timescale ( Timescale :: MICRO ) )
242+ . expect ( "failed to create track" ) ;
243+
244+ // A group with a few timestamped frames (middle PTS goes backwards, so the
245+ // fetch stream carries a negative zigzag delta too).
246+ let timestamps_us = [ 10_000u64 , 30_000 , 20_000 ] ;
247+ let mut group = track. append_group ( ) . expect ( "failed to append group" ) ; // seq 0
248+ for & us in & timestamps_us {
249+ let payload = format ! ( "frame@{us}" ) . into_bytes ( ) ;
250+ let frame = moq_native:: moq_net:: Frame {
251+ size : payload. len ( ) as u64 ,
252+ timestamp : Some ( Timestamp :: new ( us, Timescale :: MICRO ) . unwrap ( ) ) ,
253+ } ;
254+ let mut writer = group. create_frame ( frame) . expect ( "failed to create frame" ) ;
255+ writer
256+ . write ( bytes:: Bytes :: from ( payload) )
257+ . expect ( "failed to write frame" ) ;
258+ writer. finish ( ) . expect ( "failed to finish frame" ) ;
259+ }
260+ group. finish ( ) . expect ( "failed to finish group" ) ;
261+
262+ let mut server_config = moq_native:: ServerConfig :: default ( ) ;
263+ server_config. bind = Some ( "[::]:0" . to_string ( ) ) ;
264+ server_config. tls . generate = vec ! [ "localhost" . into( ) ] ;
265+ server_config. version = vec ! [ "moq-lite-05-wip" . parse( ) . unwrap( ) ] ;
266+ let mut server = server_config. init ( ) . expect ( "failed to init server" ) ;
267+ let addr = server. local_addr ( ) . expect ( "failed to get local addr" ) ;
268+
269+ let sub_origin = Origin :: random ( ) . produce ( ) ;
270+ let mut announcements = sub_origin. consume ( ) . announced ( ) ;
271+
272+ let mut client_config = moq_native:: ClientConfig :: default ( ) ;
273+ client_config. tls . disable_verify = Some ( true ) ;
274+ client_config. version = vec ! [ "moq-lite-05-wip" . parse( ) . unwrap( ) ] ;
275+ let client = client_config. init ( ) . expect ( "failed to init client" ) ;
276+ let url: url:: Url = format ! ( "{scheme}://localhost:{}" , addr. port( ) ) . parse ( ) . unwrap ( ) ;
277+
278+ let server_handle = tokio:: spawn ( async move {
279+ let request = server. accept ( ) . await . expect ( "no incoming connection" ) ;
280+ let session = request. with_publisher ( pub_origin. clone ( ) ) . ok ( ) . await ?;
281+ let _broadcast = broadcast;
282+ let _track = track;
283+ let _ = session. closed ( ) . await ;
284+ Ok :: < _ , anyhow:: Error > ( ( ) )
285+ } ) ;
286+
287+ let client = client. with_consumer ( sub_origin) ;
288+ let session = tokio:: time:: timeout ( TIMEOUT , client. connect ( url) )
289+ . await
290+ . expect ( "client connect timed out" )
291+ . expect ( "client connect failed" ) ;
292+
293+ let ( path, bc) = tokio:: time:: timeout ( TIMEOUT , announcements. next ( ) )
294+ . await
295+ . expect ( "announce timed out" )
296+ . expect ( "origin closed" ) ;
297+ assert_eq ! ( path. as_str( ) , "test" ) ;
298+ let bc = bc. broadcast ( ) . expect ( "expected announce, got unannounce" ) ;
299+
300+ // Fetch group 0 directly, without subscribing. No live producer holds the group
301+ // on the client, so this issues a wire FETCH upstream.
302+ let mut group_sub = tokio:: time:: timeout ( TIMEOUT , async {
303+ bc. track ( "video" ) . unwrap ( ) . fetch ( 0 , None ) . unwrap ( ) . await
304+ } )
305+ . await
306+ . expect ( "fetch timed out" )
307+ . expect ( "fetch failed" ) ;
308+ assert_eq ! ( group_sub. sequence, 0 ) ;
309+
310+ for & expected_us in & timestamps_us {
311+ let mut frame_sub = tokio:: time:: timeout ( TIMEOUT , group_sub. next_frame ( ) )
312+ . await
313+ . expect ( "next_frame timed out" )
314+ . expect ( "next_frame failed" )
315+ . expect ( "group closed prematurely" ) ;
316+
317+ let ts = frame_sub
318+ . timestamp
319+ . expect ( "Lite05 fetch must carry per-frame timestamps" ) ;
320+ assert_eq ! ( ts. scale( ) , Timescale :: MICRO ) ;
321+ assert_eq ! ( ts. value( ) , expected_us) ;
322+
323+ let payload = frame_sub. read_all ( ) . await . expect ( "failed to read frame" ) ;
324+ assert_eq ! ( payload, bytes:: Bytes :: from( format!( "frame@{expected_us}" ) ) ) ;
325+ }
326+
327+ // The fetched group ends cleanly (stream FIN → no more frames).
328+ let end = tokio:: time:: timeout ( TIMEOUT , group_sub. next_frame ( ) )
329+ . await
330+ . expect ( "next_frame timed out" )
331+ . expect ( "next_frame failed" ) ;
332+ assert ! ( end. is_none( ) , "group should finish after its frames" ) ;
333+
334+ drop ( session) ;
335+ server_handle
336+ . await
337+ . expect ( "server task panicked" )
338+ . expect ( "server task failed" ) ;
339+ }
340+
341+ #[ tracing_test:: traced_test]
342+ #[ tokio:: test]
343+ async fn broadcast_moq_lite_05_fetch_webtransport ( ) {
344+ // WebTransport only: Lite05Wip isn't advertised over ALPN, so raw QUIC (moqt://)
345+ // can't negotiate it (same reason the other Lite05 tests are https-only).
346+ lite05_fetch_roundtrip ( "https" ) . await ;
347+ }
348+
232349/// On Lite05 a publisher that doesn't advertise a timescale still works:
233350/// SUBSCRIBE_OK carries `timescale = 0` and neither side encodes a
234351/// per-frame timestamp byte. Subscribers receive `frame.timestamp = None`.
0 commit comments