diff --git a/src/mux/ps.rs b/src/mux/ps.rs index 2a8063dc..a30ba1e2 100644 --- a/src/mux/ps.rs +++ b/src/mux/ps.rs @@ -41,7 +41,8 @@ pub struct PsPacket { /// PES stream ID (0xE0 for video, 0xC0 for audio, 0xBD for private, etc.). pub stream_id: u8, /// Sub-stream ID for private stream 1 (AC3: 0x80-0x87, DTS: 0x88-0x8F, - /// LPCM: 0xA0-0xA7, subtitles: 0x20-0x3F). + /// LPCM: 0xA0-0xA7, E-AC-3: 0xC0-0xCF, subtitles: 0x20-0x3F); for extended stream + /// id 0xFD, the PES `stream_id_extension`. pub sub_stream_id: Option, /// Presentation timestamp in 90kHz ticks. pub pts: Option, diff --git a/src/mux/tsmux.rs b/src/mux/tsmux.rs index f2cd7ef6..cfc719d0 100644 --- a/src/mux/tsmux.rs +++ b/src/mux/tsmux.rs @@ -107,8 +107,8 @@ pub struct TsMuxer { /// the borrow of the converted bytes does not conflict with the `&mut self` /// the writer needs. annex_b: Vec, - /// Per-track arrival-time copy of `params_written` for the pre-keyframe drop guard, - /// which runs before the start-up hold (design §2.3 tick-domain rule 1). + /// Per-track: a video frame has arrived past the pre-keyframe drop guard, which runs + /// before the start-up hold (design §2.3 tick-domain rule 1). arrival_armed: Vec, /// Per video track: the online DTS deriver, built at its first frame. dts: Vec>, @@ -506,8 +506,8 @@ impl TsMuxer { self.write_psi_if_due(pts_90k)?; // NAL video (HEVC/H.264): convert length-prefixed NALUs to Annex B and prepend - // codec_private params on the first keyframe only (arm `params_written` even if - // data is empty, else later non-key frames fail the drop guard). Others pass through. + // codec_private params on the first keyframe only (`params_written` latches even if + // data is empty, so no later keyframe re-prepends them). Others pass through. let mut annex_b = std::mem::take(&mut self.annex_b); let convert = is_video && self.is_nal_video(track); if convert { @@ -715,8 +715,8 @@ impl TsMuxer { c } - /// Flush the underlying writer. BD-TS needs no stream trailer, so this - /// only drains buffering; the muxer remains usable afterwards. + /// End the stream: resolve the DTS start-up windows, drain the hold and flush the + /// writer. BD-TS needs no stream trailer. Call once, after the last frame. /// /// Returns [`Error::MuxEmpty`](crate::error::Error::MuxEmpty) when not a /// single frame was emitted: an `m2ts://` sink that wrote only the FMKV @@ -1047,10 +1047,10 @@ mod tests { } #[test] - fn empty_data_keyframe_arms_params_so_later_frames_survive() { - // An empty-data keyframe must still arm params_written; otherwise - // every subsequent non-key frame would be dropped by the - // pre-keyframe guard and the track would emit no real frames. + fn empty_data_keyframe_arms_the_guard_so_later_frames_survive() { + // An empty-data keyframe must still arm the pre-keyframe guard; otherwise + // every subsequent non-key frame would be dropped and the track would + // emit no real frames. let mut sink: Vec = Vec::new(); { let mut mux = TsMuxer::new(&mut sink, &[VIDEO_PID]); @@ -1350,11 +1350,11 @@ mod tests { ); } - // A non-NAL video track must still arm `params_written`, or every later - // non-keyframe fails the drop guard and silently vanishes — same class of - // bug `empty_data_keyframe_arms_params_so_later_frames_survive` guards. + // A non-NAL video keyframe must still arm the pre-keyframe guard, or every later + // non-keyframe silently vanishes — same class of bug + // `empty_data_keyframe_arms_the_guard_so_later_frames_survive` guards. #[test] - fn non_nal_video_keyframe_arms_params_so_later_frames_survive() { + fn non_nal_video_keyframe_arms_the_guard_so_later_frames_survive() { let key: Vec = vec![0x00, 0x00, 0x01, 0xB3, 0xAA, 0xBB]; let non_key: Vec = vec![0x00, 0x00, 0x01, 0xB6, 0xCC, 0xDD]; @@ -1438,6 +1438,33 @@ mod tests { assert_eq!(pats, 13); } + // The PES stream_id follows the declared stream_type: the MPEG audio types (13818-1 + // Table 2-22) take an audio id, everything else private_stream_1. + #[test] + fn mpeg_audio_stream_types_get_an_audio_stream_id() { + let types = [0x03u8, 0x04, 0x0F, 0x81, 0x06]; + let pids: Vec = (0..types.len() as u16).map(|k| AUDIO_PID + k).collect(); + let mut sink: Vec = Vec::new(); + { + let mut mux = TsMuxer::new(&mut sink, &pids); + mux.set_program(types.to_vec()).unwrap(); + for t in 0..types.len() { + mux.write_frame(t, 0, true, &[0xFF, 0xF1, 0x4C, 0x80]).unwrap(); + } + mux.finish().unwrap(); + } + let packets = parse_bd_ts(&sink); + let ids: Vec = pids + .iter() + .map(|&pid| { + let first = packets.iter().find(|p| p.pid == pid && p.pusi).unwrap(); + assert_eq!(first.payload[..3], [0, 0, 1]); + first.payload[3] + }) + .collect(); + assert_eq!(ids, [0xC0, 0xC0, 0xC0, 0xBD, 0xBD]); + } + // A PES header's 33-bit PTS and DTS (DTS = PTS when absent), 90 kHz. fn pes_pts_dts(payload: &[u8]) -> (u64, u64) { let ts = |b: &[u8]| { diff --git a/src/mux/videomap.rs b/src/mux/videomap.rs index 2cae9084..da7ab36e 100644 --- a/src/mux/videomap.rs +++ b/src/mux/videomap.rs @@ -404,6 +404,27 @@ mod tests { t } + // An explicit aspect wins; SD without one is unknown; HD falls back to the coded size. + #[test] + fn display_aspect_ratio_falls_back_by_resolution() { + let t = video_title( + Codec::Hevc, + Resolution::R1080p, + FrameRate::F24, + ColorSpace::Bt709, + ); + let DiscStream::Video(mut v) = t.streams[0].clone() else { + unreachable!() + }; + assert_eq!(display_aspect_ratio(&v, 1920, 1080), (1920, 1080)); + assert_eq!(display_aspect_ratio(&v, 0, 0), (0, 1)); + v.display_aspect = Some((16, 9)); + assert_eq!(display_aspect_ratio(&v, 1920, 1080), (16, 9)); + v.display_aspect = None; + v.resolution = Resolution::R576i; + assert_eq!(display_aspect_ratio(&v, 720, 576), (0, 1)); + } + fn src(medium: Medium, path: &str, title: usize) -> SourceInfo { SourceInfo { medium,