11//! MPEG-TS muxer.
22//!
3- //! [`Export`] subscribes to a MoQ broadcast and produces a single MPEG-TS byte
4- //! stream: PAT/PMT program tables followed by one PES packet per media frame,
5- //! packetized into 188-byte TS packets. Video is carried as Annex-B, audio as
6- //! ADTS AAC.
3+ //! [`Export`] subscribes to a MoQ broadcast and produces MPEG-TS, yielding one
4+ //! [`Frame`] per media frame: PAT/PMT program tables followed by one PES packet,
5+ //! packetized into 188-byte TS packets. Each frame keeps its media timestamp so
6+ //! the caller can pace delivery on the media clock. Video is carried as Annex-B,
7+ //! audio as ADTS AAC.
78//!
89//! Video flows through [`ExportSource`], which normalizes every H.264/H.265
910//! source to length-prefixed NALU plus a resolved avcC/hvcC (parsing in-band
@@ -47,9 +48,11 @@ const PSI_INTERVAL: Duration = Duration::from_millis(500);
4748
4849/// Subscribe to a broadcast and produce an MPEG-TS byte stream.
4950///
50- /// Use [`next`](Self::next) to pull byte chunks: the first chunk is PAT+PMT, then
51- /// each subsequent chunk is the TS packets for one media frame (preceded by a
52- /// fresh PAT+PMT at video keyframes). Returns `None` when the broadcast ends.
51+ /// Use [`next`](Self::next) to pull one [`Frame`] per media frame: its `payload`
52+ /// is the TS packets, stamped with the source `timestamp` and `keyframe` flag.
53+ /// The leading PAT/PMT rides on the first frame (so it inherits a real
54+ /// timestamp), and is re-emitted at video keyframes and periodically for
55+ /// mid-stream tune-in. Returns `None` when the broadcast ends.
5356pub struct Export < E : scte35:: Catalog = ( ) > {
5457 broadcast : moq_net:: BroadcastConsumer ,
5558 catalog : Option < crate :: catalog:: Consumer < E > > ,
@@ -159,12 +162,19 @@ impl<E: scte35::Catalog> Export<E> {
159162 self
160163 }
161164
162- /// Get the next byte chunk.
163- pub async fn next ( & mut self ) -> anyhow:: Result < Option < Bytes > > {
165+ /// Get the next muxed frame.
166+ ///
167+ /// Each [`Frame`] carries the TS packets for one media frame in `payload`,
168+ /// stamped with that frame's media `timestamp` and `keyframe` flag so a
169+ /// transport can pace delivery on the media clock. The leading PAT/PMT rides
170+ /// on the first frame (inheriting its timestamp), and is re-emitted at video
171+ /// keyframes and periodically for mid-stream tune-in. Returns `None` when the
172+ /// broadcast ends. `duration` is always `None`: the muxer has no use for it.
173+ pub async fn next ( & mut self ) -> anyhow:: Result < Option < Frame > > {
164174 kio:: wait ( |waiter| self . poll_next ( waiter) ) . await
165175 }
166176
167- pub fn poll_next ( & mut self , waiter : & kio:: Waiter ) -> Poll < anyhow:: Result < Option < Bytes > > > {
177+ pub fn poll_next ( & mut self , waiter : & kio:: Waiter ) -> Poll < anyhow:: Result < Option < Frame > > > {
168178 // 1. Drain catalog updates, discovering the track layout.
169179 while let Some ( catalog) = self . catalog . as_mut ( ) {
170180 match catalog. poll_next ( waiter) ? {
@@ -207,8 +217,10 @@ impl<E: scte35::Catalog> Export<E> {
207217 }
208218 }
209219
210- // 3. Emit the program tables once the layout is resolved and every
211- // track's codec config is ready.
220+ // 3. Build the program tables once the layout is resolved and every
221+ // track's codec config is ready. The tables aren't emitted here: PSI has
222+ // no media time of its own, so `write_frame` prepends them to the first
223+ // frame instead, letting the leading PAT/PMT inherit a real timestamp.
212224 if self . psi . is_none ( ) {
213225 if self . tracks . is_empty ( ) {
214226 // No tracks yet. If the catalog is also done, the broadcast is empty.
@@ -226,15 +238,14 @@ impl<E: scte35::Catalog> Export<E> {
226238 return Poll :: Pending ;
227239 }
228240 self . build_psi ( ) ?;
229- let header = self . write_psi ( ) ?;
230- return Poll :: Ready ( Ok ( Some ( header) ) ) ;
231241 }
232242
233- // 4. Emit the smallest-timestamp pending frame as a PES packet.
243+ // 4. Emit the smallest-timestamp pending frame as a PES packet (the first
244+ // one carries the buffered PAT/PMT).
234245 if let Some ( name) = self . pick_next_track ( ) {
235246 let frame = self . tracks . get_mut ( & name) . unwrap ( ) . pending . take ( ) . unwrap ( ) ;
236- let chunk = self . write_frame ( & name, frame) ?;
237- return Poll :: Ready ( Ok ( Some ( chunk ) ) ) ;
247+ let out = self . write_frame ( & name, frame) ?;
248+ return Poll :: Ready ( Ok ( Some ( out ) ) ) ;
238249 }
239250
240251 // 5. End of stream once every track has drained and the catalog is closed.
@@ -444,18 +455,7 @@ impl<E: scte35::Catalog> Export<E> {
444455 Ok ( ( ) )
445456 }
446457
447- /// Serialize a fresh PAT + PMT into a chunk.
448- fn write_psi ( & mut self ) -> anyhow:: Result < Bytes > {
449- let psi = self . psi . as_ref ( ) . context ( "PSI not built" ) ?;
450- let pat = TsPayload :: Pat ( psi. pat . clone ( ) ) ;
451- let pmt = TsPayload :: Pmt ( psi. pmt . clone ( ) ) ;
452-
453- let mut out = Vec :: with_capacity ( 2 * TsPacket :: SIZE ) ;
454- self . write_packet ( & mut out, Pid :: PAT , None , pat) ?;
455- self . write_packet ( & mut out, PMT_PID , None , pmt) ?;
456- Ok ( Bytes :: from ( out) )
457- }
458-
458+ /// Name of the track whose pending frame has the smallest timestamp.
459459 fn pick_next_track ( & self ) -> Option < String > {
460460 self . tracks
461461 . iter ( )
@@ -464,14 +464,18 @@ impl<E: scte35::Catalog> Export<E> {
464464 . map ( |( n, _) | n)
465465 }
466466
467- /// Packetize one media frame into a chunk, re-emitting PAT/PMT before video
468- /// keyframes (and periodically) so receivers can tune in mid-stream.
469- fn write_frame ( & mut self , name : & str , frame : Frame ) -> anyhow:: Result < Bytes > {
467+ /// Packetize one media frame into an output [`Frame`], re-emitting PAT/PMT
468+ /// before video keyframes (and periodically) so receivers can tune in
469+ /// mid-stream. The returned frame keeps the source `timestamp` and `keyframe`
470+ /// flag so the caller can pace it.
471+ fn write_frame ( & mut self , name : & str , frame : Frame ) -> anyhow:: Result < Frame > {
470472 let track = self . tracks . get ( name) . context ( "missing track" ) ?;
471473 let pid = track. pid ;
472474 let kind = track. kind . clone ( ) ;
473475 let is_pcr = self . psi . as_ref ( ) . is_some_and ( |p| p. pcr_pid == pid) ;
474476 let is_video = matches ! ( kind, Kind :: Video ( _) ) ;
477+ let timestamp = frame. timestamp ;
478+ let keyframe = frame. keyframe ;
475479
476480 // Build the elementary-stream payload for this frame. Video needs the
477481 // resolved avcC/hvcC to rewrite length-prefixed NALs as Annex-B. SCTE-35
@@ -525,7 +529,12 @@ impl<E: scte35::Catalog> Export<E> {
525529 self . write_pes ( & mut out, & unit, & es_payload) ?;
526530 }
527531 }
528- Ok ( Bytes :: from ( out) )
532+ Ok ( Frame {
533+ timestamp,
534+ duration : None ,
535+ payload : Bytes :: from ( out) ,
536+ keyframe,
537+ } )
529538 }
530539
531540 /// Packetize a PES payload into 188-byte TS packets.
0 commit comments