1use anyhow::{Context, Result, bail};
40use codec::frame::{ColorSpace, PixelFormat, StreamInfo};
41
42use crate::ac3_sync::{
43 self, Eac3SyncInfo, SyncInfo, ac3_bit_rate_kbps, channel_count, eac3_sample_rate_hz,
44 eac3_samples_per_frame,
45};
46use crate::demux::{AudioTrack, DemuxResult};
47use crate::mux::{dac3_body_from_sync, dec3_body_from_sync};
48use crate::streaming::{DemuxHeader, Sample, StreamingDemuxer};
49
50const TS_PACKET: usize = 188;
51const TS_SYNC: u8 = 0x47;
52
53const STREAM_TYPE_MPEG2_VIDEO: u8 = 0x02;
54const STREAM_TYPE_H264: u8 = 0x1B;
55const STREAM_TYPE_HEVC: u8 = 0x24;
56const STREAM_TYPE_PES_PRIVATE: u8 = 0x06;
63const STREAM_TYPE_AAC_ADTS: u8 = 0x0F;
68const STREAM_TYPE_AC3: u8 = 0x81;
71const STREAM_TYPE_EAC3: u8 = 0x87;
73
74const DESC_TAG_REGISTRATION: u8 = 0x05;
79const REG_AC3: u32 = 0x41432D33; const REG_EAC3: u32 = 0x45414333; pub(crate) fn demux_ts(data: &[u8]) -> Result<DemuxResult> {
83 let (packets, packet_stride, prefix_len) = detect_packet_layout(data)?;
87 if packets == 0 {
88 bail!("TS: file contains no TS packets");
89 }
90
91 let mut pmt_pid: Option<u16> = None;
100 let mut chosen_video: Option<VideoStreamInfo> = None;
101 let mut chosen_audio: Option<AudioStreamInfo> = None;
102 for i in 0..packets {
103 let start = i * packet_stride + prefix_len;
104 let pkt = &data[start..start + TS_PACKET];
105 if pkt[0] != TS_SYNC {
106 continue;
107 }
108 let pid = (((pkt[1] & 0x1F) as u16) << 8) | pkt[2] as u16;
109 if pmt_pid.is_none() && pid == 0 {
111 if let Some(payload) = ts_psi_payload(pkt)
112 && let Some(p) = parse_pat_first_pmt_pid(payload)
113 {
114 pmt_pid = Some(p);
115 }
116 continue;
117 }
118 if let (Some(pmt), None) = (pmt_pid, chosen_video)
120 && pid == pmt
121 && let Some(payload) = ts_psi_payload(pkt)
122 && let Some((video_streams, audio_streams)) = parse_pmt_streams(payload)
123 {
124 chosen_video = video_streams.into_iter().next();
125 chosen_audio = audio_streams.into_iter().next();
126 if chosen_video.is_some() {
127 break;
128 }
129 }
130 }
131
132 let video = chosen_video.context("TS: no video elementary stream found in PMT")?;
133 let video_pid = video.pid;
134 let codec = match video.stream_type {
135 STREAM_TYPE_MPEG2_VIDEO => "mpeg2",
136 STREAM_TYPE_H264 => "h264",
137 STREAM_TYPE_HEVC => "h265",
138 other => bail!("TS: unsupported stream_type 0x{:02X}", other),
139 }
140 .to_string();
141
142 let mut samples: Vec<Vec<u8>> = Vec::new();
145 let mut pending: Vec<u8> = Vec::new();
146 let mut have_first_start = false;
147 let mut first_pts: Option<u64> = None;
148 let mut last_pts: Option<u64> = None;
149 let mut ptses: Vec<u64> = Vec::new();
154
155 let flush = |pending: &mut Vec<u8>, samples: &mut Vec<Vec<u8>>| {
156 if !pending.is_empty() {
157 samples.push(std::mem::take(pending));
158 }
159 };
160
161 for i in 0..packets {
162 let start = i * packet_stride + prefix_len;
163 let pkt = &data[start..start + TS_PACKET];
164 if pkt[0] != TS_SYNC {
165 continue;
166 }
167 let pid = (((pkt[1] & 0x1F) as u16) << 8) | pkt[2] as u16;
168 if pid != video_pid {
169 continue;
170 }
171 let pusi = pkt[1] & 0x40 != 0;
172 let scramble = (pkt[3] >> 6) & 0x03;
173 if scramble != 0 {
174 continue;
175 } let adaptation = (pkt[3] >> 4) & 0x03;
177 let has_payload = adaptation & 0x01 != 0;
178 let has_adaptation = adaptation & 0x02 != 0;
179 if !has_payload {
180 continue;
181 }
182
183 let mut offset = 4usize;
184 if has_adaptation {
185 if offset >= TS_PACKET {
186 continue;
187 }
188 let adap_len = pkt[offset] as usize;
189 offset += 1 + adap_len;
190 if offset > TS_PACKET {
191 continue;
192 }
193 }
194 if offset >= TS_PACKET {
195 continue;
196 }
197 let payload = &pkt[offset..];
198
199 if pusi {
200 if have_first_start {
204 flush(&mut pending, &mut samples);
205 }
206 have_first_start = true;
207
208 let Some((es_start, pts)) = parse_pes_header(payload) else {
209 have_first_start = false;
211 pending.clear();
212 continue;
213 };
214 if let Some(p) = pts {
215 if first_pts.is_none() {
216 first_pts = Some(p);
217 }
218 last_pts = Some(p);
219 ptses.push(p);
220 }
221 if es_start < payload.len() {
222 pending.extend_from_slice(&payload[es_start..]);
223 }
224 } else if have_first_start {
225 pending.extend_from_slice(payload);
226 }
227 }
228 flush(&mut pending, &mut samples);
229
230 if samples.is_empty() {
231 bail!("TS: reassembled zero video samples from PID {}", video_pid);
232 }
233
234 let duration = match (first_pts, last_pts) {
240 (Some(a), Some(b)) if b >= a => (b - a) as f64 / 90_000.0,
241 _ => 0.0,
242 };
243 let frame_rate = estimate_frame_rate_from_ptses(&ptses)
244 .or_else(|| {
245 if duration > 0.0 && samples.len() > 1 {
246 Some((samples.len() - 1) as f64 / duration)
247 } else {
248 None
249 }
250 })
251 .unwrap_or(30.0);
252
253 let (width, height) = codec::pixel_format::detect_dims(&codec, &samples).unwrap_or((0, 0));
260 if width == 0 || height == 0 {
261 tracing::warn!(
262 codec = codec.as_str(),
263 "TS demux: could not recover width/height from first sample — \
264 downstream encoder may reject the 0×0 config"
265 );
266 }
267
268 let info = StreamInfo {
269 codec: codec.clone(),
270 width,
271 height,
272 frame_rate,
273 duration,
274 pixel_format: PixelFormat::Yuv420p,
275 color_space: ColorSpace::Bt709,
276 total_frames: samples.len() as u64,
277 bitrate: 0,
278 color_metadata: Default::default(),
279 };
280
281 let detected_pf = codec::pixel_format::detect(&codec, &samples);
282 let info = StreamInfo {
283 pixel_format: detected_pf,
284 ..info
285 };
286
287 let audio = chosen_audio.and_then(|info| {
292 match extract_ts_audio(data, packets, packet_stride, prefix_len, info) {
293 Ok(track) => track,
294 Err(e) => {
295 tracing::warn!(
296 audio_pid = info.pid,
297 audio_kind = ?info.kind,
298 error = %e,
299 "TS audio extraction failed; emitting video-only"
300 );
301 None
302 }
303 }
304 });
305
306 Ok(DemuxResult {
307 codec,
308 info,
309 samples,
310 audio,
311 })
312}
313
314fn detect_packet_layout(data: &[u8]) -> Result<(usize, usize, usize)> {
320 if data.len() < TS_PACKET {
321 bail!("TS: file too small");
322 }
323 if data[0] == TS_SYNC && data.len() >= 2 * TS_PACKET && data[TS_PACKET] == TS_SYNC {
325 return Ok((data.len() / TS_PACKET, TS_PACKET, 0));
326 }
327 if data.len() >= 192 + 4 && data[4] == TS_SYNC && data[196] == TS_SYNC {
329 return Ok((data.len() / 192, 192, 4));
330 }
331 bail!("TS: could not locate 0x47 sync pattern at 188- or 192-byte intervals")
332}
333
334fn ts_psi_payload(pkt: &[u8]) -> Option<&[u8]> {
339 let pusi = pkt[1] & 0x40 != 0;
340 let adaptation = (pkt[3] >> 4) & 0x03;
341 let has_payload = adaptation & 0x01 != 0;
342 let has_adaptation = adaptation & 0x02 != 0;
343 if !has_payload {
344 return None;
345 }
346 let mut offset = 4usize;
347 if has_adaptation {
348 if offset >= TS_PACKET {
349 return None;
350 }
351 let adap_len = pkt[offset] as usize;
352 offset += 1 + adap_len;
353 if offset > TS_PACKET {
354 return None;
355 }
356 }
357 if pusi {
362 if offset >= TS_PACKET {
363 return None;
364 }
365 let pointer = pkt[offset] as usize;
366 offset += 1 + pointer;
367 if offset >= TS_PACKET {
368 return None;
369 }
370 }
371 Some(&pkt[offset..])
372}
373
374#[derive(Debug, Clone, Copy, PartialEq, Eq)]
377pub(crate) struct PatProgram {
378 pub program_number: u16,
379 pub pmt_pid: u16,
380}
381
382#[derive(Debug, Clone, Copy, PartialEq, Eq)]
387pub enum AudioCodecKind {
388 AacAdts,
390 Ac3,
392 Eac3,
394}
395
396#[derive(Debug, Clone, Copy, PartialEq, Eq)]
398pub struct VideoStreamInfo {
399 pub pid: u16,
400 pub stream_type: u8,
401}
402
403#[derive(Debug, Clone, Copy, PartialEq, Eq)]
407pub struct AudioStreamInfo {
408 pub pid: u16,
409 pub stream_type: u8,
410 pub kind: AudioCodecKind,
411}
412
413#[derive(Debug, Clone, PartialEq, Eq)]
422pub struct ProgramInfo {
423 pub program_number: u16,
424 pub pmt_pid: u16,
425 pub video_streams: Vec<VideoStreamInfo>,
426 pub audio_streams: Vec<AudioStreamInfo>,
427}
428
429fn parse_pat_all_programs(section: &[u8]) -> Vec<PatProgram> {
433 let mut out = Vec::new();
440 if section.len() < 12 {
441 return out;
442 }
443 if section[0] != 0x00 {
444 return out;
445 }
446 let section_length = (((section[1] & 0x0F) as usize) << 8) | section[2] as usize;
447 let total = 3 + section_length;
448 if total > section.len() {
449 return out;
450 }
451 let loop_start = 8;
452 let loop_end = total - 4;
453 let mut i = loop_start;
454 while i + 4 <= loop_end {
455 let program = u16::from_be_bytes([section[i], section[i + 1]]);
456 let pid = (((section[i + 2] & 0x1F) as u16) << 8) | section[i + 3] as u16;
457 if program != 0 {
458 out.push(PatProgram {
459 program_number: program,
460 pmt_pid: pid,
461 });
462 }
463 i += 4;
464 }
465 out
466}
467
468fn parse_pat_first_pmt_pid(section: &[u8]) -> Option<u16> {
472 parse_pat_all_programs(section).first().map(|p| p.pmt_pid)
473}
474
475fn parse_pmt_streams(section: &[u8]) -> Option<(Vec<VideoStreamInfo>, Vec<AudioStreamInfo>)> {
487 if section.len() < 12 {
497 return None;
498 }
499 if section[0] != 0x02 {
500 return None;
501 }
502 let section_length = (((section[1] & 0x0F) as usize) << 8) | section[2] as usize;
503 let total = 3 + section_length;
504 if total > section.len() {
505 return None;
506 }
507 if section.len() < 12 {
508 return None;
509 }
510 let pil = (((section[10] & 0x0F) as usize) << 8) | section[11] as usize;
511 let mut i = 12 + pil;
512 let loop_end = total - 4; let mut video: Vec<VideoStreamInfo> = Vec::new();
514 let mut audio: Vec<AudioStreamInfo> = Vec::new();
515 while i + 5 <= loop_end {
516 let stype = section[i];
517 let pid = (((section[i + 1] & 0x1F) as u16) << 8) | section[i + 2] as u16;
518 let esi_len = (((section[i + 3] & 0x0F) as usize) << 8) | section[i + 4] as usize;
519 let desc_start = i + 5;
520 let desc_end = (desc_start + esi_len).min(loop_end);
521 let descriptors = if desc_start <= desc_end {
522 §ion[desc_start..desc_end]
523 } else {
524 &[][..]
525 };
526
527 match stype {
528 STREAM_TYPE_MPEG2_VIDEO | STREAM_TYPE_H264 | STREAM_TYPE_HEVC => {
529 video.push(VideoStreamInfo {
530 pid,
531 stream_type: stype,
532 });
533 }
534 STREAM_TYPE_AAC_ADTS => {
535 audio.push(AudioStreamInfo {
536 pid,
537 stream_type: stype,
538 kind: AudioCodecKind::AacAdts,
539 });
540 }
541 STREAM_TYPE_AC3 => {
542 audio.push(AudioStreamInfo {
543 pid,
544 stream_type: stype,
545 kind: AudioCodecKind::Ac3,
546 });
547 }
548 STREAM_TYPE_EAC3 => {
549 audio.push(AudioStreamInfo {
550 pid,
551 stream_type: stype,
552 kind: AudioCodecKind::Eac3,
553 });
554 }
555 STREAM_TYPE_PES_PRIVATE => {
556 if let Some(reg) = find_registration(descriptors) {
560 match reg {
561 REG_AC3 => audio.push(AudioStreamInfo {
562 pid,
563 stream_type: stype,
564 kind: AudioCodecKind::Ac3,
565 }),
566 REG_EAC3 => audio.push(AudioStreamInfo {
567 pid,
568 stream_type: stype,
569 kind: AudioCodecKind::Eac3,
570 }),
571 _ => {}
572 }
573 }
574 }
575 _ => {}
576 }
577 i += 5 + esi_len;
578 }
579 Some((video, audio))
580}
581
582fn find_registration(descriptors: &[u8]) -> Option<u32> {
586 let mut i = 0usize;
587 while i + 2 <= descriptors.len() {
588 let tag = descriptors[i];
589 let len = descriptors[i + 1] as usize;
590 let body_start = i + 2;
591 let body_end = body_start + len;
592 if body_end > descriptors.len() {
593 break;
594 }
595 if tag == DESC_TAG_REGISTRATION && len >= 4 {
596 let id = u32::from_be_bytes([
597 descriptors[body_start],
598 descriptors[body_start + 1],
599 descriptors[body_start + 2],
600 descriptors[body_start + 3],
601 ]);
602 return Some(id);
603 }
604 i = body_end;
605 }
606 None
607}
608
609fn parse_pes_header(payload: &[u8]) -> Option<(usize, Option<u64>)> {
620 if payload.len() < 9 {
621 return None;
622 }
623 if payload[0] != 0 || payload[1] != 0 || payload[2] != 1 {
624 return None;
625 }
626 let stream_id = payload[3];
627 if !(0xE0..=0xEF).contains(&stream_id) {
631 return None;
632 }
633 let flags = payload[7];
635 let pts_dts_flags = (flags >> 6) & 0x03;
636 let header_data_len = payload[8] as usize;
637 let es_start = 9 + header_data_len;
638 if es_start > payload.len() {
639 return None;
640 }
641 let pts = if pts_dts_flags == 0b10 || pts_dts_flags == 0b11 {
642 if payload.len() < 14 {
645 return None;
646 }
647 let p0 = ((payload[9] >> 1) & 0x07) as u64;
648 let p1 = (((payload[10] as u64) << 7) | ((payload[11] as u64) >> 1)) & 0x7FFF;
649 let p2 = (((payload[12] as u64) << 7) | ((payload[13] as u64) >> 1)) & 0x7FFF;
650 Some((p0 << 30) | (p1 << 15) | p2)
651 } else {
652 None
653 };
654 Some((es_start, pts))
655}
656
657pub(crate) struct VideoStreamScan {
661 pub first_au: Option<Vec<u8>>,
662 pub ptses: Vec<u64>,
663}
664
665fn scan_first_video_au(
680 data: &[u8],
681 packets: usize,
682 packet_stride: usize,
683 prefix_len: usize,
684 video_pid: u16,
685 max_pts_samples: usize,
686) -> VideoStreamScan {
687 let mut accumulator: Vec<u8> = Vec::new();
688 let mut first_au: Option<Vec<u8>> = None;
689 let mut ptses: Vec<u64> = Vec::new();
690 let mut au_started = false;
691 let mut au_done = false;
692 for i in 0..packets {
693 let start = i * packet_stride + prefix_len;
694 let pkt = &data[start..start + TS_PACKET];
695 if pkt[0] != TS_SYNC {
696 continue;
697 }
698 let pid = (((pkt[1] & 0x1F) as u16) << 8) | pkt[2] as u16;
699 if pid != video_pid {
700 continue;
701 }
702 let pusi = pkt[1] & 0x40 != 0;
703 let scramble = (pkt[3] >> 6) & 0x03;
704 if scramble != 0 {
705 continue;
706 } let adaptation = (pkt[3] >> 4) & 0x03;
708 let has_payload = adaptation & 0x01 != 0;
709 let has_adaptation = adaptation & 0x02 != 0;
710 if !has_payload {
711 continue;
712 }
713 let mut offset = 4usize;
714 if has_adaptation {
715 if offset >= TS_PACKET {
716 continue;
717 }
718 let adap_len = pkt[offset] as usize;
719 offset += 1 + adap_len;
720 if offset > TS_PACKET {
721 continue;
722 }
723 }
724 if offset >= TS_PACKET {
725 continue;
726 }
727 let payload = &pkt[offset..];
728
729 if pusi {
730 if au_started && !au_done {
732 first_au = Some(std::mem::take(&mut accumulator));
733 au_done = true;
734 }
735 if let Some((es_start, pts)) = parse_pes_header(payload) {
736 if let Some(p) = pts
737 && ptses.len() < max_pts_samples
738 {
739 ptses.push(p);
740 }
741 if !au_done {
742 if es_start < payload.len() {
743 accumulator.extend_from_slice(&payload[es_start..]);
744 }
745 au_started = true;
746 }
747 }
748 } else if au_started && !au_done {
749 accumulator.extend_from_slice(payload);
750 }
751
752 if au_done && ptses.len() >= max_pts_samples {
754 break;
755 }
756 }
757 if first_au.is_none() && au_started && !accumulator.is_empty() {
759 first_au = Some(accumulator);
760 }
761 VideoStreamScan { first_au, ptses }
762}
763
764fn estimate_frame_rate_from_ptses(ptses: &[u64]) -> Option<f64> {
784 if ptses.len() < 2 {
785 return None;
786 }
787 let mut sorted: Vec<u64> = ptses.to_vec();
788 sorted.sort_unstable();
789 let mut deltas: Vec<u64> = sorted.windows(2).map(|w| w[1] - w[0]).collect();
790 deltas.retain(|&d| d > 0);
791 if deltas.is_empty() {
792 return None;
793 }
794 deltas.sort_unstable();
795 let median = deltas[deltas.len() / 2];
796 if median == 0 {
797 return None;
798 }
799 let fps = 90000.0 / median as f64;
800 if !fps.is_finite() || !(1.0..=240.0).contains(&fps) {
801 return None;
802 }
803 Some(fps)
804}
805
806const AAC_SAMPLE_RATES: [u32; 13] = [
825 96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350,
826];
827
828#[derive(Debug, Clone, Copy, PartialEq, Eq)]
832struct AdtsHeader {
833 profile: u8,
836 sampling_frequency_index: u8,
839 channel_configuration: u8,
843 frame_length: usize,
846 header_len: usize,
849}
850
851fn parse_adts_header(buf: &[u8]) -> Option<AdtsHeader> {
856 if buf.len() < 7 {
857 return None;
858 }
859 if buf[0] != 0xFF || (buf[1] & 0xF0) != 0xF0 {
861 return None;
862 }
863 let protection_absent = (buf[1] & 0x01) != 0;
864 let header_len = if protection_absent { 7 } else { 9 };
865 if buf.len() < header_len {
866 return None;
867 }
868 let profile = (buf[2] >> 6) & 0x03;
869 let sampling_frequency_index = (buf[2] >> 2) & 0x0F;
870 let channel_configuration = ((buf[2] & 0x01) << 2) | ((buf[3] >> 6) & 0x03);
874 let frame_length =
879 (((buf[3] & 0x03) as usize) << 11) | ((buf[4] as usize) << 3) | ((buf[5] >> 5) as usize);
880 if frame_length < header_len {
881 return None;
882 }
883 Some(AdtsHeader {
884 profile,
885 sampling_frequency_index,
886 channel_configuration,
887 frame_length,
888 header_len,
889 })
890}
891
892fn decode_sample_rate_index(idx: u8) -> Option<u32> {
898 AAC_SAMPLE_RATES.get(idx as usize).copied()
899}
900
901fn synthesize_asc(adts: &AdtsHeader) -> [u8; 2] {
918 let aot = adts.profile + 1; let sr_idx = adts.sampling_frequency_index;
920 let ch_cfg = adts.channel_configuration;
921 let mut bits: u16 = 0;
925 bits |= ((aot as u16) & 0x1F) << 11;
926 bits |= ((sr_idx as u16) & 0x0F) << 7;
927 bits |= ((ch_cfg as u16) & 0x0F) << 3;
928 bits.to_be_bytes()
930}
931
932fn extract_ts_aac_audio(
943 data: &[u8],
944 packets: usize,
945 packet_stride: usize,
946 prefix_len: usize,
947 audio_pid: u16,
948) -> Result<Option<AudioTrack>> {
949 let es = reassemble_audio_pes(data, packets, packet_stride, prefix_len, audio_pid);
953
954 if es.is_empty() {
955 return Ok(None);
956 }
957
958 let mut cursor = match find_adts_sync(&es, 0) {
960 Some(idx) => idx,
961 None => return Ok(None),
962 };
963 let first = parse_adts_header(&es[cursor..]).context("TS: first ADTS frame failed to parse")?;
964 let sample_rate = decode_sample_rate_index(first.sampling_frequency_index)
965 .context("TS: AAC sampling_frequency_index out of range")?;
966 let channels = first.channel_configuration as u16;
967 if channels == 0 {
968 bail!("TS: AAC channel_configuration=0 (PCE-defined); not supported");
969 }
970 let asc = synthesize_asc(&first).to_vec();
971
972 let mut samples: Vec<Vec<u8>> = Vec::new();
976 let mut durations: Vec<u32> = Vec::new();
977 while cursor < es.len() {
978 let Some(found) = find_adts_sync(&es, cursor) else {
982 break;
983 };
984 cursor = found;
985 let Some(hdr) = parse_adts_header(&es[cursor..]) else {
986 break;
987 };
988 if hdr.sampling_frequency_index != first.sampling_frequency_index
989 || hdr.channel_configuration != first.channel_configuration
990 {
991 tracing::warn!(
992 "TS: AAC ADTS stream switched sr_idx/ch_cfg mid-stream; truncating audio at frame {}",
993 samples.len()
994 );
995 break;
996 }
997 let end = cursor + hdr.frame_length;
998 if end > es.len() {
999 break;
1000 }
1001 let payload_start = cursor + hdr.header_len;
1002 if payload_start > end {
1003 break;
1004 }
1005 samples.push(es[payload_start..end].to_vec());
1006 durations.push(1024);
1007 cursor = end;
1008 }
1009
1010 if samples.is_empty() {
1011 return Ok(None);
1012 }
1013
1014 Ok(Some(AudioTrack {
1015 codec: "aac".into(),
1016 samples,
1017 sample_rate,
1018 channels,
1019 asc,
1020 codec_private: Vec::new(),
1021 timescale: sample_rate,
1022 durations,
1023 }))
1024}
1025
1026fn find_ac3_sync(es: &[u8], from: usize) -> Option<usize> {
1047 let mut i = from;
1048 while i + 1 < es.len() {
1049 if es[i] == 0x0B && es[i + 1] == 0x77 {
1050 return Some(i);
1051 }
1052 i += 1;
1053 }
1054 None
1055}
1056
1057fn ac3_frame_size(brc: u8, fscod: u8, frmsizecod_low_bit: u8) -> Option<usize> {
1075 let kbps = ac3_bit_rate_kbps(brc) as usize;
1076 if kbps == 0 {
1077 return None;
1078 }
1079 let sr = ac3_sync::ac3_sample_rate_hz(fscod) as usize;
1080 if sr == 0 {
1081 return None;
1082 }
1083 let base = (kbps * 1000 * 1536) / (sr * 8);
1084 let extra = if fscod == 1 && frmsizecod_low_bit != 0 {
1088 2
1089 } else {
1090 0
1091 };
1092 Some(base + extra)
1093}
1094
1095fn eac3_frame_size(frmsiz: u16) -> usize {
1099 ((frmsiz as usize) + 1) * 2
1100}
1101
1102fn extract_ts_ac3_audio(
1111 data: &[u8],
1112 packets: usize,
1113 packet_stride: usize,
1114 prefix_len: usize,
1115 audio_pid: u16,
1116) -> Result<Option<AudioTrack>> {
1117 let es = reassemble_audio_pes(data, packets, packet_stride, prefix_len, audio_pid);
1118 if es.is_empty() {
1119 return Ok(None);
1120 }
1121 let mut cursor = match find_ac3_sync(&es, 0) {
1122 Some(idx) => idx,
1123 None => return Ok(None),
1124 };
1125 let first = match ac3_sync::parse_sync_info(&es[cursor..])
1127 .context("TS: first AC-3 frame failed to parse sync header")?
1128 {
1129 SyncInfo::Ac3(s) => s,
1130 SyncInfo::Eac3(_) => bail!("TS: AC-3 PMT entry but bitstream is E-AC-3 (bsid=16)"),
1131 };
1132 let sample_rate = ac3_sync::ac3_sample_rate_hz(first.fscod);
1133 if sample_rate == 0 {
1134 bail!("TS: AC-3 fscod={} reserved", first.fscod);
1135 }
1136 let channels = channel_count(first.acmod, first.lfeon);
1137 let dac3 = dac3_body_from_sync(&first).to_vec();
1138
1139 let mut samples: Vec<Vec<u8>> = Vec::new();
1142 let mut durations: Vec<u32> = Vec::new();
1143 while cursor < es.len() {
1144 let Some(found) = find_ac3_sync(&es, cursor) else {
1145 break;
1146 };
1147 cursor = found;
1148 if cursor + 5 > es.len() {
1151 break;
1152 }
1153 let frmsizecod = es[cursor + 4] & 0x3F;
1154 let bit_rate_code = frmsizecod >> 1;
1155 let low_bit = frmsizecod & 0x01;
1156 let fscod = (es[cursor + 4] >> 6) & 0x03;
1157 let Some(size) = ac3_frame_size(bit_rate_code, fscod, low_bit) else {
1158 break;
1159 };
1160 let end = cursor + size;
1161 if end > es.len() {
1162 break;
1163 }
1164 samples.push(es[cursor..end].to_vec());
1165 durations.push(1536);
1166 cursor = end;
1167 }
1168 if samples.is_empty() {
1169 return Ok(None);
1170 }
1171 Ok(Some(AudioTrack {
1172 codec: "ac3".into(),
1173 samples,
1174 sample_rate,
1175 channels,
1176 asc: Vec::new(),
1177 codec_private: dac3,
1178 timescale: sample_rate,
1179 durations,
1180 }))
1181}
1182
1183fn extract_ts_eac3_audio(
1190 data: &[u8],
1191 packets: usize,
1192 packet_stride: usize,
1193 prefix_len: usize,
1194 audio_pid: u16,
1195) -> Result<Option<AudioTrack>> {
1196 let es = reassemble_audio_pes(data, packets, packet_stride, prefix_len, audio_pid);
1197 if es.is_empty() {
1198 return Ok(None);
1199 }
1200 let mut cursor = match find_ac3_sync(&es, 0) {
1201 Some(idx) => idx,
1202 None => return Ok(None),
1203 };
1204 let first: Eac3SyncInfo = match ac3_sync::parse_sync_info(&es[cursor..])
1205 .context("TS: first E-AC-3 frame failed to parse sync header")?
1206 {
1207 SyncInfo::Eac3(s) => s,
1208 SyncInfo::Ac3(_) => bail!("TS: E-AC-3 PMT entry but bitstream is AC-3 (bsid<=10)"),
1209 };
1210 let sample_rate = eac3_sample_rate_hz(first.fscod, first.fscod2);
1211 if sample_rate == 0 {
1212 bail!(
1213 "TS: E-AC-3 reserved sample rate (fscod={}, fscod2={})",
1214 first.fscod,
1215 first.fscod2
1216 );
1217 }
1218 let channels = channel_count(first.acmod, first.lfeon);
1219 let spf = eac3_samples_per_frame(first.numblkscod) as u64;
1220 let frame_bytes = ((first.frmsiz as u64) + 1) * 2;
1221 let bitrate_kbps = if spf > 0 && sample_rate > 0 {
1222 (frame_bytes * 8 * sample_rate as u64) / spf / 1000
1223 } else {
1224 0
1225 };
1226 let data_rate = bitrate_kbps.div_ceil(2) as u16;
1227 let dec3 = dec3_body_from_sync(&first, data_rate).to_vec();
1228
1229 let mut samples: Vec<Vec<u8>> = Vec::new();
1230 let mut durations: Vec<u32> = Vec::new();
1231 while cursor < es.len() {
1232 let Some(found) = find_ac3_sync(&es, cursor) else {
1233 break;
1234 };
1235 cursor = found;
1236 if cursor + 5 > es.len() {
1237 break;
1238 }
1239 let raw = u16::from_be_bytes([es[cursor + 2], es[cursor + 3]]);
1243 let frmsiz = raw & 0x07FF;
1244 let size = eac3_frame_size(frmsiz);
1245 let end = cursor + size;
1246 if end > es.len() {
1247 break;
1248 }
1249 samples.push(es[cursor..end].to_vec());
1250 durations.push(spf as u32);
1251 cursor = end;
1252 }
1253 if samples.is_empty() {
1254 return Ok(None);
1255 }
1256 Ok(Some(AudioTrack {
1257 codec: "eac3".into(),
1258 samples,
1259 sample_rate,
1260 channels,
1261 asc: Vec::new(),
1262 codec_private: dec3,
1263 timescale: sample_rate,
1264 durations,
1265 }))
1266}
1267
1268fn reassemble_audio_pes(
1273 data: &[u8],
1274 packets: usize,
1275 packet_stride: usize,
1276 prefix_len: usize,
1277 audio_pid: u16,
1278) -> Vec<u8> {
1279 let mut es: Vec<u8> = Vec::new();
1280 let mut have_first_start = false;
1281 for i in 0..packets {
1282 let start = i * packet_stride + prefix_len;
1283 let pkt = &data[start..start + TS_PACKET];
1284 if pkt[0] != TS_SYNC {
1285 continue;
1286 }
1287 let pid = (((pkt[1] & 0x1F) as u16) << 8) | pkt[2] as u16;
1288 if pid != audio_pid {
1289 continue;
1290 }
1291 let pusi = pkt[1] & 0x40 != 0;
1292 let scramble = (pkt[3] >> 6) & 0x03;
1293 if scramble != 0 {
1294 continue;
1295 }
1296 let adaptation = (pkt[3] >> 4) & 0x03;
1297 let has_payload = adaptation & 0x01 != 0;
1298 let has_adaptation = adaptation & 0x02 != 0;
1299 if !has_payload {
1300 continue;
1301 }
1302
1303 let mut offset = 4usize;
1304 if has_adaptation {
1305 if offset >= TS_PACKET {
1306 continue;
1307 }
1308 let adap_len = pkt[offset] as usize;
1309 offset += 1 + adap_len;
1310 if offset > TS_PACKET {
1311 continue;
1312 }
1313 }
1314 if offset >= TS_PACKET {
1315 continue;
1316 }
1317 let payload = &pkt[offset..];
1318
1319 if pusi {
1320 let Some((es_start, _pts)) = parse_pes_header_audio(payload) else {
1321 have_first_start = false;
1322 continue;
1323 };
1324 have_first_start = true;
1325 if es_start < payload.len() {
1326 es.extend_from_slice(&payload[es_start..]);
1327 }
1328 } else if have_first_start {
1329 es.extend_from_slice(payload);
1330 }
1331 }
1332 es
1333}
1334
1335fn extract_ts_audio(
1339 data: &[u8],
1340 packets: usize,
1341 packet_stride: usize,
1342 prefix_len: usize,
1343 info: AudioStreamInfo,
1344) -> Result<Option<AudioTrack>> {
1345 match info.kind {
1346 AudioCodecKind::AacAdts => {
1347 extract_ts_aac_audio(data, packets, packet_stride, prefix_len, info.pid)
1348 }
1349 AudioCodecKind::Ac3 => {
1350 extract_ts_ac3_audio(data, packets, packet_stride, prefix_len, info.pid)
1351 }
1352 AudioCodecKind::Eac3 => {
1353 extract_ts_eac3_audio(data, packets, packet_stride, prefix_len, info.pid)
1354 }
1355 }
1356}
1357
1358fn find_adts_sync(es: &[u8], from: usize) -> Option<usize> {
1361 let mut i = from;
1362 while i + 1 < es.len() {
1363 if es[i] == 0xFF && (es[i + 1] & 0xF0) == 0xF0 {
1364 return Some(i);
1365 }
1366 i += 1;
1367 }
1368 None
1369}
1370
1371fn parse_pes_header_audio(payload: &[u8]) -> Option<(usize, Option<u64>)> {
1375 if payload.len() < 9 {
1376 return None;
1377 }
1378 if payload[0] != 0 || payload[1] != 0 || payload[2] != 1 {
1379 return None;
1380 }
1381 let stream_id = payload[3];
1382 if !(0xC0..=0xDF).contains(&stream_id) {
1384 return None;
1385 }
1386 let flags = payload[7];
1387 let pts_dts_flags = (flags >> 6) & 0x03;
1388 let header_data_len = payload[8] as usize;
1389 let es_start = 9 + header_data_len;
1390 if es_start > payload.len() {
1391 return None;
1392 }
1393 let pts = if pts_dts_flags == 0b10 || pts_dts_flags == 0b11 {
1394 if payload.len() < 14 {
1395 return None;
1396 }
1397 let p0 = ((payload[9] >> 1) & 0x07) as u64;
1398 let p1 = (((payload[10] as u64) << 7) | ((payload[11] as u64) >> 1)) & 0x7FFF;
1399 let p2 = (((payload[12] as u64) << 7) | ((payload[13] as u64) >> 1)) & 0x7FFF;
1400 Some((p0 << 30) | (p1 << 15) | p2)
1401 } else {
1402 None
1403 };
1404 Some((es_start, pts))
1405}
1406
1407pub struct TsStreamingDemuxer {
1427 data: Vec<u8>,
1428 header: DemuxHeader,
1429 audio: Option<AudioTrack>,
1430 packets: usize,
1431 packet_stride: usize,
1432 prefix_len: usize,
1433 programs: Vec<ProgramInfo>,
1438 active_program_idx: usize,
1441 video_pid: u16,
1443 next_pkt: usize,
1445 pending: Vec<u8>,
1447 pending_pts: Option<u64>,
1450 have_first_start: bool,
1453 eof: bool,
1456 pixel_format_detected: bool,
1460 encrypted_drop: bool,
1465}
1466
1467pub(crate) fn demux_ts_streaming_init(data: &[u8]) -> Result<TsStreamingDemuxer> {
1468 let owned = data.to_vec();
1469 let (packets, packet_stride, prefix_len) = detect_packet_layout(&owned)?;
1470 if packets == 0 {
1471 bail!("TS: file contains no TS packets");
1472 }
1473
1474 let mut pat_programs: Vec<PatProgram> = Vec::new();
1476 for i in 0..packets {
1477 let start = i * packet_stride + prefix_len;
1478 let pkt = &owned[start..start + TS_PACKET];
1479 if pkt[0] != TS_SYNC {
1480 continue;
1481 }
1482 let pid = (((pkt[1] & 0x1F) as u16) << 8) | pkt[2] as u16;
1483 if pid == 0
1484 && let Some(payload) = ts_psi_payload(pkt)
1485 {
1486 let progs = parse_pat_all_programs(payload);
1487 if !progs.is_empty() {
1488 pat_programs = progs;
1489 break;
1490 }
1491 }
1492 }
1493 if pat_programs.is_empty() {
1494 bail!("TS: no PAT entries found");
1495 }
1496
1497 let mut programs: Vec<ProgramInfo> = pat_programs
1503 .iter()
1504 .map(|p| ProgramInfo {
1505 program_number: p.program_number,
1506 pmt_pid: p.pmt_pid,
1507 video_streams: Vec::new(),
1508 audio_streams: Vec::new(),
1509 })
1510 .collect();
1511 let mut need: std::collections::HashSet<u16> = pat_programs.iter().map(|p| p.pmt_pid).collect();
1513 for i in 0..packets {
1514 if need.is_empty() {
1515 break;
1516 }
1517 let start = i * packet_stride + prefix_len;
1518 let pkt = &owned[start..start + TS_PACKET];
1519 if pkt[0] != TS_SYNC {
1520 continue;
1521 }
1522 let pid = (((pkt[1] & 0x1F) as u16) << 8) | pkt[2] as u16;
1523 if !need.contains(&pid) {
1524 continue;
1525 }
1526 if let Some(payload) = ts_psi_payload(pkt)
1527 && let Some((video_streams, audio_streams)) = parse_pmt_streams(payload)
1528 {
1529 if let Some(prog) = programs.iter_mut().find(|p| p.pmt_pid == pid) {
1530 prog.video_streams = video_streams;
1531 prog.audio_streams = audio_streams;
1532 }
1533 need.remove(&pid);
1534 }
1535 }
1536
1537 let active_program_idx = programs
1541 .iter()
1542 .position(|p| !p.video_streams.is_empty())
1543 .context("TS: no program advertises a recognised video elementary stream")?;
1544 let active = &programs[active_program_idx];
1545 let video = active.video_streams[0];
1546 let audio = active.audio_streams.first().copied();
1547 let codec = match video.stream_type {
1548 STREAM_TYPE_MPEG2_VIDEO => "mpeg2",
1549 STREAM_TYPE_H264 => "h264",
1550 STREAM_TYPE_HEVC => "h265",
1551 other => bail!("TS: unsupported stream_type 0x{:02X}", other),
1552 }
1553 .to_string();
1554
1555 let scan = scan_first_video_au(&owned, packets, packet_stride, prefix_len, video.pid, 64);
1574 let (width, height) = match &scan.first_au {
1575 Some(au) => codec::pixel_format::detect_dims(&codec, std::slice::from_ref(au))
1576 .unwrap_or_else(|| {
1577 tracing::warn!(
1578 codec = codec.as_str(),
1579 video_pid = video.pid,
1580 "TS streaming demux: first AU SPS parse failed; width/height=0×0"
1581 );
1582 (0, 0)
1583 }),
1584 None => {
1585 tracing::warn!(
1586 codec = codec.as_str(),
1587 video_pid = video.pid,
1588 "TS streaming demux: could not locate first video AU during init; width/height=0×0"
1589 );
1590 (0, 0)
1591 }
1592 };
1593 let frame_rate = estimate_frame_rate_from_ptses(&scan.ptses).unwrap_or_else(|| {
1594 tracing::warn!(
1595 codec = codec.as_str(),
1596 video_pid = video.pid,
1597 pts_samples = scan.ptses.len(),
1598 "TS streaming demux: could not derive frame_rate from PTS window; defaulting to 30.0"
1599 );
1600 30.0
1601 });
1602
1603 let info = StreamInfo {
1604 codec: codec.clone(),
1605 width,
1606 height,
1607 frame_rate,
1608 duration: 0.0,
1609 pixel_format: PixelFormat::Yuv420p,
1610 color_space: ColorSpace::Bt709,
1611 total_frames: 0,
1612 bitrate: 0,
1613 color_metadata: Default::default(),
1614 };
1615
1616 let audio_track = audio.and_then(|info| {
1619 match extract_ts_audio(&owned, packets, packet_stride, prefix_len, info) {
1620 Ok(track) => track,
1621 Err(e) => {
1622 tracing::warn!(
1623 audio_pid = info.pid,
1624 audio_kind = ?info.kind,
1625 error = %e,
1626 "TS audio extraction failed; emitting video-only"
1627 );
1628 None
1629 }
1630 }
1631 });
1632
1633 Ok(TsStreamingDemuxer {
1634 data: owned,
1635 header: DemuxHeader { codec, info },
1636 audio: audio_track,
1637 packets,
1638 packet_stride,
1639 prefix_len,
1640 programs,
1641 active_program_idx,
1642 video_pid: video.pid,
1643 next_pkt: 0,
1644 pending: Vec::new(),
1645 pending_pts: None,
1646 have_first_start: false,
1647 eof: false,
1648 pixel_format_detected: false,
1649 encrypted_drop: false,
1650 })
1651}
1652
1653impl TsStreamingDemuxer {
1654 pub fn programs(&self) -> &[ProgramInfo] {
1659 &self.programs
1660 }
1661
1662 pub fn active_program_index(&self) -> usize {
1664 self.active_program_idx
1665 }
1666
1667 pub fn select_program(&mut self, program_number: u16) -> Result<()> {
1679 let new_idx = self
1680 .programs
1681 .iter()
1682 .position(|p| p.program_number == program_number)
1683 .with_context(|| format!("TS: program_number {} not found in PAT", program_number))?;
1684 if self.programs[new_idx].video_streams.is_empty() {
1685 bail!(
1686 "TS: program {} has no recognised video stream",
1687 program_number
1688 );
1689 }
1690 let video = self.programs[new_idx].video_streams[0];
1691 let audio = self.programs[new_idx].audio_streams.first().copied();
1692 let codec = match video.stream_type {
1693 STREAM_TYPE_MPEG2_VIDEO => "mpeg2",
1694 STREAM_TYPE_H264 => "h264",
1695 STREAM_TYPE_HEVC => "h265",
1696 other => bail!(
1697 "TS: program {} video stream_type 0x{:02X} unsupported",
1698 program_number,
1699 other
1700 ),
1701 }
1702 .to_string();
1703 self.active_program_idx = new_idx;
1704 self.video_pid = video.pid;
1705 self.header.codec = codec.clone();
1708 self.header.info.codec = codec.clone();
1709 self.header.info.pixel_format = PixelFormat::Yuv420p;
1710 self.pixel_format_detected = false;
1711 let scan = scan_first_video_au(
1716 &self.data,
1717 self.packets,
1718 self.packet_stride,
1719 self.prefix_len,
1720 video.pid,
1721 64,
1722 );
1723 let (w, h) = match &scan.first_au {
1724 Some(au) => {
1725 codec::pixel_format::detect_dims(&codec, std::slice::from_ref(au)).unwrap_or((0, 0))
1726 }
1727 None => (0, 0),
1728 };
1729 self.header.info.width = w;
1730 self.header.info.height = h;
1731 self.header.info.frame_rate = estimate_frame_rate_from_ptses(&scan.ptses).unwrap_or(30.0);
1732 self.next_pkt = 0;
1734 self.pending.clear();
1735 self.pending_pts = None;
1736 self.have_first_start = false;
1737 self.eof = false;
1738 self.encrypted_drop = false;
1739 self.audio = audio.and_then(|info| {
1741 match extract_ts_audio(
1742 &self.data,
1743 self.packets,
1744 self.packet_stride,
1745 self.prefix_len,
1746 info,
1747 ) {
1748 Ok(track) => track,
1749 Err(e) => {
1750 tracing::warn!(
1751 audio_pid = info.pid,
1752 audio_kind = ?info.kind,
1753 error = %e,
1754 "TS audio extraction failed on program switch; emitting video-only"
1755 );
1756 None
1757 }
1758 }
1759 });
1760 Ok(())
1761 }
1762
1763 fn yield_sample(&mut self, data: Vec<u8>, pts: Option<u64>) -> Sample {
1767 if !self.pixel_format_detected {
1768 let detected =
1769 codec::pixel_format::detect(&self.header.codec, std::slice::from_ref(&data));
1770 self.header.info.pixel_format = detected;
1771 self.pixel_format_detected = true;
1772 }
1773 Sample {
1774 data,
1775 pts_ticks: pts.map(|p| p as i64).unwrap_or(0),
1776 duration_ticks: 0,
1777 }
1778 }
1779}
1780
1781impl StreamingDemuxer for TsStreamingDemuxer {
1782 fn header(&self) -> &DemuxHeader {
1783 &self.header
1784 }
1785
1786 fn next_video_sample(&mut self) -> Result<Option<Sample>> {
1787 if self.eof || self.encrypted_drop {
1788 return Ok(None);
1789 }
1790 loop {
1791 if self.next_pkt >= self.packets {
1792 self.eof = true;
1794 if !self.pending.is_empty() {
1795 let data = std::mem::take(&mut self.pending);
1796 let pts = self.pending_pts.take();
1797 return Ok(Some(self.yield_sample(data, pts)));
1798 }
1799 return Ok(None);
1800 }
1801
1802 let i = self.next_pkt;
1803 self.next_pkt += 1;
1804 let start = i * self.packet_stride + self.prefix_len;
1805 let pkt = &self.data[start..start + TS_PACKET];
1806 if pkt[0] != TS_SYNC {
1807 continue;
1808 }
1809 let pid = (((pkt[1] & 0x1F) as u16) << 8) | pkt[2] as u16;
1810 if pid != self.video_pid {
1811 continue;
1812 }
1813 let pusi = pkt[1] & 0x40 != 0;
1814 let scramble = (pkt[3] >> 6) & 0x03;
1815 if scramble != 0 {
1816 tracing::warn!(
1823 video_pid = self.video_pid,
1824 transport_scrambling_control = scramble,
1825 error_kind = "encrypted_ts",
1826 "encrypted TS stream; we don't carry CA tables — drop video output"
1827 );
1828 self.encrypted_drop = true;
1829 self.pending.clear();
1830 self.pending_pts = None;
1831 self.have_first_start = false;
1832 self.eof = true;
1833 return Ok(None);
1834 }
1835 let adaptation = (pkt[3] >> 4) & 0x03;
1836 let has_payload = adaptation & 0x01 != 0;
1837 let has_adaptation = adaptation & 0x02 != 0;
1838 if !has_payload {
1839 continue;
1840 }
1841
1842 let mut offset = 4usize;
1843 if has_adaptation {
1844 if offset >= TS_PACKET {
1845 continue;
1846 }
1847 let adap_len = pkt[offset] as usize;
1848 offset += 1 + adap_len;
1849 if offset > TS_PACKET {
1850 continue;
1851 }
1852 }
1853 if offset >= TS_PACKET {
1854 continue;
1855 }
1856 let payload = &pkt[offset..];
1857
1858 if pusi {
1859 let had_pending = self.have_first_start;
1863 let prev_data = if had_pending {
1864 std::mem::take(&mut self.pending)
1865 } else {
1866 Vec::new()
1867 };
1868 let prev_pts = self.pending_pts.take();
1869 self.have_first_start = true;
1870
1871 let Some((es_start, pts)) = parse_pes_header(payload) else {
1872 self.have_first_start = false;
1874 self.pending.clear();
1875 if !prev_data.is_empty() {
1876 return Ok(Some(self.yield_sample(prev_data, prev_pts)));
1877 }
1878 continue;
1879 };
1880 self.pending_pts = pts;
1881 if es_start < payload.len() {
1882 self.pending.extend_from_slice(&payload[es_start..]);
1883 }
1884
1885 if !prev_data.is_empty() {
1886 return Ok(Some(self.yield_sample(prev_data, prev_pts)));
1887 }
1888 } else if self.have_first_start {
1891 self.pending.extend_from_slice(payload);
1892 }
1893 }
1894 }
1895
1896 fn audio(&self) -> Option<&AudioTrack> {
1897 self.audio.as_ref()
1898 }
1899}
1900
1901#[cfg(test)]
1902mod tests {
1903 use super::*;
1904
1905 fn ts_pkt(pid: u16, pusi: bool, adaptation: u8, payload: &[u8]) -> [u8; TS_PACKET] {
1906 let mut p = [0xFFu8; TS_PACKET];
1907 p[0] = TS_SYNC;
1908 p[1] = if pusi { 0x40 } else { 0x00 } | ((pid >> 8) & 0x1F) as u8;
1910 p[2] = (pid & 0xFF) as u8;
1911 p[3] = (adaptation & 0x03) << 4;
1913 let mut off = 4;
1914 let pay_len = payload.len().min(TS_PACKET - off);
1916 p[off..off + pay_len].copy_from_slice(&payload[..pay_len]);
1917 off += pay_len;
1918 let _ = off;
1920 p
1921 }
1922
1923 #[test]
1924 fn estimate_frame_rate_from_uniform_ptses_returns_exact_fps() {
1925 let ptses: Vec<u64> = (0..64).map(|i| i as u64 * 3750).collect();
1927 let fps = estimate_frame_rate_from_ptses(&ptses).expect("24 fps");
1928 assert!((fps - 24.0).abs() < 1e-9, "{} != 24.0", fps);
1929 }
1930
1931 #[test]
1932 fn estimate_frame_rate_from_reordered_ptses_sorts_before_delta() {
1933 let mut ptses: Vec<u64> = (0..32).map(|i| i as u64 * 3750).collect();
1937 ptses.swap(5, 6);
1938 ptses.swap(10, 11);
1939 let fps = estimate_frame_rate_from_ptses(&ptses).expect("24 fps after swap");
1940 assert!((fps - 24.0).abs() < 1e-9, "{} != 24.0", fps);
1941 }
1942
1943 #[test]
1944 fn estimate_frame_rate_from_single_outlier_delta_uses_median() {
1945 let mut ptses: Vec<u64> = (0..24).map(|i| i as u64 * 3750).collect();
1947 ptses.push(24 * 3750 + 37500); let fps = estimate_frame_rate_from_ptses(&ptses).expect("24 fps despite outlier");
1949 assert!((fps - 24.0).abs() < 1e-9);
1950 }
1951
1952 #[test]
1953 fn estimate_frame_rate_returns_none_when_all_ptses_equal() {
1954 let ptses = vec![0u64; 10];
1955 assert!(estimate_frame_rate_from_ptses(&ptses).is_none());
1956 }
1957
1958 #[test]
1959 fn estimate_frame_rate_returns_none_when_fewer_than_two() {
1960 assert!(estimate_frame_rate_from_ptses(&[]).is_none());
1961 assert!(estimate_frame_rate_from_ptses(&[1234]).is_none());
1962 }
1963
1964 #[test]
1965 fn estimate_frame_rate_rejects_out_of_range_values() {
1966 let ptses = vec![0u64, 1];
1968 assert!(estimate_frame_rate_from_ptses(&ptses).is_none());
1969 }
1970
1971 #[test]
1972 fn estimate_frame_rate_handles_29_97_ntsc() {
1973 let ptses: Vec<u64> = (0..32).map(|i| i as u64 * 3003).collect();
1975 let fps = estimate_frame_rate_from_ptses(&ptses).expect("29.97");
1976 assert!((fps - 30.0).abs() < 0.05, "got {}", fps); }
1978
1979 #[test]
1980 fn detects_plain_ts_layout() {
1981 let mut buf = Vec::with_capacity(3 * TS_PACKET);
1982 for _ in 0..3 {
1983 let pkt = ts_pkt(0x1FFF, false, 0b01, &[]);
1984 buf.extend_from_slice(&pkt);
1985 }
1986 let (count, stride, prefix) = detect_packet_layout(&buf).unwrap();
1987 assert_eq!((count, stride, prefix), (3, 188, 0));
1988 }
1989
1990 #[test]
1991 fn parses_minimal_pat_pmt_and_reassembles_one_sample() {
1992 let mut pat = vec![0u8; 0];
1999 pat.push(0x00); let section_length: usize = 5 + 4 + 4; pat.push(0xB0 | ((section_length >> 8) & 0x0F) as u8);
2002 pat.push((section_length & 0xFF) as u8);
2003 pat.extend_from_slice(&[0x00, 0x01, 0xC1, 0x00, 0x00]); pat.extend_from_slice(&[0x00, 0x01]); pat.extend_from_slice(&[0xE1, 0x00]); pat.extend_from_slice(&[0, 0, 0, 0]); let mut pat_payload = vec![0u8];
2010 pat_payload.extend_from_slice(&pat);
2011 let pat_pkt = ts_pkt(0x0000, true, 0b01, &pat_payload);
2012
2013 let mut pmt = vec![0u8; 0];
2015 pmt.push(0x02);
2016 let pmt_sec_len: usize = 9 + 5 + 4; pmt.push(0xB0 | ((pmt_sec_len >> 8) & 0x0F) as u8);
2018 pmt.push((pmt_sec_len & 0xFF) as u8);
2019 pmt.extend_from_slice(&[0x00, 0x01, 0xC1, 0x00, 0x00]); pmt.extend_from_slice(&[0xE2, 0x00]); pmt.extend_from_slice(&[0xF0, 0x00]); pmt.extend_from_slice(&[STREAM_TYPE_MPEG2_VIDEO, 0xE2, 0x00, 0xF0, 0x00]); pmt.extend_from_slice(&[0, 0, 0, 0]); let mut pmt_payload = vec![0u8];
2025 pmt_payload.extend_from_slice(&pmt);
2026 let pmt_pkt = ts_pkt(0x0100, true, 0b01, &pmt_payload);
2027
2028 let make_pes = |byte: u8| {
2034 let mut pes = vec![0u8, 0u8, 1u8]; pes.push(0xE0); pes.extend_from_slice(&[0u8, 0u8]); pes.push(0x80);
2038 pes.push(0x80); pes.push(5); pes.extend_from_slice(&[0x21, 0x00, 0x01, 0x00, 0x01]); pes.extend_from_slice(&[byte; 16]);
2042 pes
2043 };
2044 let pes_pkt_a = ts_pkt(0x0200, true, 0b01, &make_pes(0xAA));
2045 let pes_pkt_b = ts_pkt(0x0200, true, 0b01, &make_pes(0xBB));
2046
2047 let mut buf = Vec::new();
2048 buf.extend_from_slice(&pat_pkt);
2049 buf.extend_from_slice(&pmt_pkt);
2050 buf.extend_from_slice(&pes_pkt_a);
2051 buf.extend_from_slice(&pes_pkt_b);
2052 buf.extend_from_slice(&ts_pkt(0x1FFF, false, 0b01, &[]));
2054
2055 let d = demux_ts(&buf).expect("demux");
2056 assert_eq!(d.codec, "mpeg2");
2057 assert_eq!(d.samples.len(), 2);
2064 assert_eq!(&d.samples[0][..16], &[0xAA; 16]);
2065 assert_eq!(&d.samples[1][..16], &[0xBB; 16]);
2066 }
2067
2068 #[test]
2069 fn rejects_file_with_no_sync() {
2070 let garbage = vec![0u8; TS_PACKET * 3];
2071 assert!(demux_ts(&garbage).is_err());
2072 }
2073
2074 fn build_adts_header_7(profile: u8, sr_idx: u8, ch_cfg: u8, frame_length: usize) -> [u8; 7] {
2079 let mut h = [0u8; 7];
2080 h[0] = 0xFF;
2083 h[1] = 0xF0 | 0x01; h[2] = ((profile & 0x03) << 6) | ((sr_idx & 0x0F) << 2) | ((ch_cfg >> 2) & 0x01);
2086 h[3] = ((ch_cfg & 0x03) << 6) | (((frame_length >> 11) & 0x03) as u8);
2089 h[4] = ((frame_length >> 3) & 0xFF) as u8;
2090 h[5] = (((frame_length & 0x07) << 5) | 0x1F) as u8;
2091 h[6] = 0xFC;
2093 h
2094 }
2095
2096 fn build_adts_header_9(profile: u8, sr_idx: u8, ch_cfg: u8, frame_length: usize) -> [u8; 9] {
2098 let mut h = [0u8; 9];
2099 h[0] = 0xFF;
2100 h[1] = 0xF0; h[2] = ((profile & 0x03) << 6) | ((sr_idx & 0x0F) << 2) | ((ch_cfg >> 2) & 0x01);
2102 h[3] = ((ch_cfg & 0x03) << 6) | (((frame_length >> 11) & 0x03) as u8);
2103 h[4] = ((frame_length >> 3) & 0xFF) as u8;
2104 h[5] = (((frame_length & 0x07) << 5) | 0x1F) as u8;
2105 h[6] = 0xFC;
2106 h
2108 }
2109
2110 #[test]
2111 fn adts_parser_decodes_canonical_lc_stereo_7byte_header() {
2112 let h = build_adts_header_7(1, 3, 2, 107);
2114 let parsed = parse_adts_header(&h).expect("must parse 7-byte ADTS header");
2115 assert_eq!(parsed.profile, 1, "ADTS profile=1 LC");
2116 assert_eq!(parsed.sampling_frequency_index, 3, "sr_idx=3 → 48kHz");
2117 assert_eq!(parsed.channel_configuration, 2, "ch_cfg=2 stereo");
2118 assert_eq!(parsed.frame_length, 107);
2119 assert_eq!(parsed.header_len, 7, "protection_absent=1 → 7-byte header");
2120 assert_eq!(
2121 decode_sample_rate_index(parsed.sampling_frequency_index),
2122 Some(48000)
2123 );
2124 }
2125
2126 #[test]
2127 fn adts_parser_decodes_9byte_header_with_crc() {
2128 let h = build_adts_header_9(1, 4, 2, 109);
2129 let parsed = parse_adts_header(&h).expect("must parse 9-byte ADTS header");
2130 assert_eq!(parsed.profile, 1);
2131 assert_eq!(parsed.sampling_frequency_index, 4, "sr_idx=4 → 44.1kHz");
2132 assert_eq!(parsed.channel_configuration, 2);
2133 assert_eq!(parsed.frame_length, 109);
2134 assert_eq!(
2135 parsed.header_len, 9,
2136 "protection_absent=0 → 9-byte header (incl CRC)"
2137 );
2138 assert_eq!(
2139 decode_sample_rate_index(parsed.sampling_frequency_index),
2140 Some(44100)
2141 );
2142 }
2143
2144 #[test]
2145 fn adts_parser_decodes_aac_profile_bits_full_range() {
2146 for profile in 0u8..=3 {
2154 let h = build_adts_header_7(profile, 3, 2, 32);
2155 let parsed =
2156 parse_adts_header(&h).unwrap_or_else(|| panic!("must parse profile={profile}"));
2157 assert_eq!(parsed.profile, profile);
2158 }
2159 }
2160
2161 #[test]
2162 fn adts_parser_rejects_missing_sync() {
2163 let mut h = build_adts_header_7(1, 3, 2, 32);
2164 h[0] = 0x00;
2165 assert!(parse_adts_header(&h).is_none());
2166 }
2167
2168 #[test]
2169 fn adts_parser_rejects_short_buffer() {
2170 let h = build_adts_header_7(1, 3, 2, 32);
2171 assert!(
2172 parse_adts_header(&h[..6]).is_none(),
2173 "<7 bytes can't carry a complete ADTS header"
2174 );
2175 }
2176
2177 #[test]
2178 fn synthesize_asc_lc_stereo_48k_emits_0x1190() {
2179 let adts = AdtsHeader {
2188 profile: 1,
2189 sampling_frequency_index: 3,
2190 channel_configuration: 2,
2191 frame_length: 0,
2192 header_len: 7,
2193 };
2194 let asc = synthesize_asc(&adts);
2195 assert_eq!(asc, [0x11, 0x90], "LC/48k/stereo → ASC 0x11 0x90");
2196 }
2197
2198 #[test]
2199 fn synthesize_asc_lc_mono_44k() {
2200 let adts = AdtsHeader {
2203 profile: 1,
2204 sampling_frequency_index: 4,
2205 channel_configuration: 1,
2206 frame_length: 0,
2207 header_len: 7,
2208 };
2209 assert_eq!(synthesize_asc(&adts), [0x12, 0x08]);
2210 }
2211
2212 #[test]
2213 fn synthesize_asc_main_aot_at_44k_5p1_rejected_at_channel_layer() {
2214 let adts = AdtsHeader {
2220 profile: 0,
2221 sampling_frequency_index: 4,
2222 channel_configuration: 6,
2223 frame_length: 0,
2224 header_len: 7,
2225 };
2226 assert_eq!(synthesize_asc(&adts), [0x0A, 0x30]);
2227 }
2228
2229 #[test]
2230 fn adts_strip_7byte_header_yields_payload_only() {
2231 let mut frame = Vec::with_capacity(107);
2236 frame.extend_from_slice(&build_adts_header_7(1, 3, 2, 107));
2237 frame.extend_from_slice(&[0x42u8; 100]);
2238 let header = parse_adts_header(&frame).unwrap();
2241 assert_eq!(header.frame_length, 107);
2242 let payload = &frame[header.header_len..header.frame_length];
2243 assert_eq!(payload.len(), 100);
2244 assert!(payload.iter().all(|b| *b == 0x42));
2245 }
2246
2247 #[test]
2248 fn adts_sample_rate_table_covers_documented_indices() {
2249 assert_eq!(decode_sample_rate_index(0), Some(96000));
2251 assert_eq!(decode_sample_rate_index(3), Some(48000));
2252 assert_eq!(decode_sample_rate_index(4), Some(44100));
2253 assert_eq!(decode_sample_rate_index(12), Some(7350));
2254 assert!(decode_sample_rate_index(13).is_none(), "13 is reserved");
2255 assert!(
2256 decode_sample_rate_index(15).is_none(),
2257 "15 (escape) not supported"
2258 );
2259 }
2260
2261 #[test]
2267 fn demux_ts_yields_audio_track_when_pmt_advertises_aac() {
2268 let mut pat = vec![0x00];
2270 let pat_section_len: usize = 5 + 4 + 4;
2271 pat.push(0xB0 | ((pat_section_len >> 8) & 0x0F) as u8);
2272 pat.push((pat_section_len & 0xFF) as u8);
2273 pat.extend_from_slice(&[0x00, 0x01, 0xC1, 0x00, 0x00]);
2274 pat.extend_from_slice(&[0x00, 0x01, 0xE1, 0x00, 0u8, 0u8, 0u8, 0u8]);
2275 let mut pat_payload = vec![0u8];
2276 pat_payload.extend_from_slice(&pat);
2277 let pat_pkt = ts_pkt(0x0000, true, 0b01, &pat_payload);
2278
2279 let mut pmt = vec![0x02];
2282 let pmt_section_len: usize = 9 + 5 + 5 + 4; pmt.push(0xB0 | ((pmt_section_len >> 8) & 0x0F) as u8);
2284 pmt.push((pmt_section_len & 0xFF) as u8);
2285 pmt.extend_from_slice(&[0x00, 0x01, 0xC1, 0x00, 0x00]);
2286 pmt.extend_from_slice(&[0xE2, 0x00]); pmt.extend_from_slice(&[0xF0, 0x00]); pmt.extend_from_slice(&[STREAM_TYPE_MPEG2_VIDEO, 0xE2, 0x00, 0xF0, 0x00]);
2290 pmt.extend_from_slice(&[STREAM_TYPE_AAC_ADTS, 0xE3, 0x00, 0xF0, 0x00]);
2292 pmt.extend_from_slice(&[0u8; 4]); let mut pmt_payload = vec![0u8];
2294 pmt_payload.extend_from_slice(&pmt);
2295 let pmt_pkt = ts_pkt(0x0100, true, 0b01, &pmt_payload);
2296
2297 let video_pes = {
2300 let mut pes = vec![
2301 0u8, 0u8, 1u8, 0xE0, 0u8, 0u8, 0x80, 0x80, 5, 0x21, 0x00, 0x01, 0x00, 0x01,
2302 ];
2303 pes.extend_from_slice(&[0xAAu8; 16]);
2304 pes
2305 };
2306 let video_pkt = ts_pkt(0x0200, true, 0b01, &video_pes);
2307
2308 let mut adts_stream = Vec::new();
2312 for fill in [0xCCu8, 0xDDu8] {
2313 adts_stream.extend_from_slice(&build_adts_header_7(1, 3, 2, 39));
2314 adts_stream.extend_from_slice(&[fill; 32]);
2315 }
2316 let audio_pes = {
2317 let mut pes = vec![
2319 0u8, 0u8, 1u8, 0xC0, 0u8, 0u8, 0x80, 0x80, 5, 0x21, 0x00, 0x01, 0x00, 0x01,
2320 ];
2321 pes.extend_from_slice(&adts_stream);
2322 pes
2323 };
2324 let audio_pkt = ts_pkt(0x0300, true, 0b01, &audio_pes);
2325
2326 let mut buf = Vec::new();
2327 buf.extend_from_slice(&pat_pkt);
2328 buf.extend_from_slice(&pmt_pkt);
2329 buf.extend_from_slice(&video_pkt);
2330 buf.extend_from_slice(&audio_pkt);
2331 buf.extend_from_slice(&ts_pkt(0x1FFF, false, 0b01, &[]));
2332
2333 let d = demux_ts(&buf).expect("demux must succeed");
2334 assert_eq!(d.codec, "mpeg2");
2335 let audio = d.audio.expect("AAC audio track must be surfaced");
2336 assert_eq!(audio.codec, "aac");
2337 assert_eq!(audio.channels, 2, "ch_cfg=2 stereo");
2338 assert_eq!(audio.sample_rate, 48000, "sr_idx=3 → 48k");
2339 assert_eq!(audio.timescale, 48000, "AAC timescale = sample_rate");
2340 assert_eq!(
2341 audio.asc,
2342 vec![0x11, 0x90],
2343 "synthesized ASC for LC/48k/stereo"
2344 );
2345 assert_eq!(audio.samples.len(), 2, "two ADTS frames → two samples");
2346 assert_eq!(
2347 audio.samples[0].len(),
2348 32,
2349 "32-byte payload after 7-byte header strip"
2350 );
2351 assert!(audio.samples[0].iter().all(|b| *b == 0xCC));
2352 assert!(audio.samples[1].iter().all(|b| *b == 0xDD));
2353 assert_eq!(
2354 audio.durations,
2355 vec![1024, 1024],
2356 "AAC-LC frame duration = 1024 ticks @ sample-rate timescale"
2357 );
2358 }
2359
2360 #[test]
2361 fn demux_ts_emits_audio_none_when_no_aac_stream_in_pmt() {
2362 let mut buf = Vec::new();
2364 let mut pat = vec![0x00];
2365 let pat_section_len: usize = 5 + 4 + 4;
2366 pat.push(0xB0 | ((pat_section_len >> 8) & 0x0F) as u8);
2367 pat.push((pat_section_len & 0xFF) as u8);
2368 pat.extend_from_slice(&[0x00, 0x01, 0xC1, 0x00, 0x00]);
2369 pat.extend_from_slice(&[0x00, 0x01, 0xE1, 0x00, 0u8, 0u8, 0u8, 0u8]);
2370 let mut pat_payload = vec![0u8];
2371 pat_payload.extend_from_slice(&pat);
2372 buf.extend_from_slice(&ts_pkt(0x0000, true, 0b01, &pat_payload));
2373
2374 let mut pmt = vec![0x02];
2375 let pmt_section_len: usize = 9 + 5 + 4;
2376 pmt.push(0xB0 | ((pmt_section_len >> 8) & 0x0F) as u8);
2377 pmt.push((pmt_section_len & 0xFF) as u8);
2378 pmt.extend_from_slice(&[0x00, 0x01, 0xC1, 0x00, 0x00]);
2379 pmt.extend_from_slice(&[0xE2, 0x00, 0xF0, 0x00]);
2380 pmt.extend_from_slice(&[STREAM_TYPE_MPEG2_VIDEO, 0xE2, 0x00, 0xF0, 0x00]);
2381 pmt.extend_from_slice(&[0u8; 4]);
2382 let mut pmt_payload = vec![0u8];
2383 pmt_payload.extend_from_slice(&pmt);
2384 buf.extend_from_slice(&ts_pkt(0x0100, true, 0b01, &pmt_payload));
2385
2386 let video_pes = {
2387 let mut pes = vec![
2388 0u8, 0u8, 1u8, 0xE0, 0u8, 0u8, 0x80, 0x80, 5, 0x21, 0x00, 0x01, 0x00, 0x01,
2389 ];
2390 pes.extend_from_slice(&[0xAAu8; 16]);
2391 pes
2392 };
2393 buf.extend_from_slice(&ts_pkt(0x0200, true, 0b01, &video_pes));
2394 buf.extend_from_slice(&ts_pkt(0x1FFF, false, 0b01, &[]));
2395
2396 let d = demux_ts(&buf).expect("demux");
2397 assert!(
2398 d.audio.is_none(),
2399 "PMT without AAC-ADTS stream → no audio track surfaced"
2400 );
2401 }
2402
2403 fn synth_ac3_frame_stereo_48k_128k() -> Vec<u8> {
2411 let mut bw = BitWriter::new();
2412 bw.put(16, 0x0B77); bw.put(16, 0); bw.put(2, 0); bw.put(6, 8 << 1); bw.put(5, 8); bw.put(3, 0); bw.put(3, 2); bw.put(2, 0);
2421 bw.put(1, 0); while bw.bytes.len() < 384 {
2424 bw.put(8, 0);
2425 }
2426 bw.flush()
2427 }
2428
2429 fn synth_eac3_frame_stereo_48k_192bytes() -> Vec<u8> {
2432 let mut bw = BitWriter::new();
2433 bw.put(16, 0x0B77);
2434 bw.put(2, 0); bw.put(3, 0); bw.put(11, 0x5F); bw.put(2, 0); bw.put(2, 3); bw.put(3, 2); bw.put(1, 0); bw.put(5, 16); bw.put(5, 0); bw.put(1, 0); while bw.bytes.len() < 192 {
2445 bw.put(8, 0);
2446 }
2447 bw.flush()
2448 }
2449
2450 struct BitWriter {
2453 bytes: Vec<u8>,
2454 bit_pos: usize,
2455 }
2456 impl BitWriter {
2457 fn new() -> Self {
2458 Self {
2459 bytes: Vec::new(),
2460 bit_pos: 0,
2461 }
2462 }
2463 fn put(&mut self, n: usize, v: u32) {
2464 for i in (0..n).rev() {
2465 let bit = ((v >> i) & 0x01) as u8;
2466 if self.bit_pos % 8 == 0 {
2467 self.bytes.push(0);
2468 }
2469 let byte_idx = self.bit_pos / 8;
2470 let bit_idx = 7 - (self.bit_pos % 8);
2471 self.bytes[byte_idx] |= bit << bit_idx;
2472 self.bit_pos += 1;
2473 }
2474 }
2475 fn flush(self) -> Vec<u8> {
2476 self.bytes
2477 }
2478 }
2479
2480 fn ts_pkt_continuation(pid: u16, payload: &[u8]) -> [u8; TS_PACKET] {
2486 let mut p = [0xFFu8; TS_PACKET];
2487 p[0] = TS_SYNC;
2488 p[1] = ((pid >> 8) & 0x1F) as u8; p[2] = (pid & 0xFF) as u8;
2490 p[3] = 0b01 << 4; let pay_len = payload.len().min(TS_PACKET - 4);
2492 p[4..4 + pay_len].copy_from_slice(&payload[..pay_len]);
2493 p
2494 }
2495
2496 fn build_ts_with_audio(
2506 audio_stream_type: u8,
2507 audio_descriptors: &[u8],
2508 audio_pid: u16,
2509 audio_es: &[u8],
2510 ) -> Vec<u8> {
2511 let mut pat = vec![0x00];
2513 let pat_section_len: usize = 5 + 4 + 4;
2514 pat.push(0xB0 | ((pat_section_len >> 8) & 0x0F) as u8);
2515 pat.push((pat_section_len & 0xFF) as u8);
2516 pat.extend_from_slice(&[0x00, 0x01, 0xC1, 0x00, 0x00]);
2517 pat.extend_from_slice(&[0x00, 0x01, 0xE1, 0x00, 0u8, 0u8, 0u8, 0u8]);
2518 let mut pat_payload = vec![0u8];
2519 pat_payload.extend_from_slice(&pat);
2520 let pat_pkt = ts_pkt(0x0000, true, 0b01, &pat_payload);
2521
2522 let mut pmt = vec![0x02];
2524 let pmt_stream_entries = 5 + 5 + audio_descriptors.len(); let pmt_section_len: usize = 9 + pmt_stream_entries + 4;
2527 pmt.push(0xB0 | ((pmt_section_len >> 8) & 0x0F) as u8);
2528 pmt.push((pmt_section_len & 0xFF) as u8);
2529 pmt.extend_from_slice(&[0x00, 0x01, 0xC1, 0x00, 0x00]);
2530 pmt.extend_from_slice(&[0xE2, 0x00]); pmt.extend_from_slice(&[0xF0, 0x00]); pmt.extend_from_slice(&[STREAM_TYPE_MPEG2_VIDEO, 0xE2, 0x00, 0xF0, 0x00]);
2534 pmt.push(audio_stream_type);
2536 pmt.push(0xE0 | ((audio_pid >> 8) & 0x1F) as u8);
2537 pmt.push((audio_pid & 0xFF) as u8);
2538 let esi_len = audio_descriptors.len() as u16;
2539 pmt.push(0xF0 | ((esi_len >> 8) & 0x0F) as u8);
2540 pmt.push((esi_len & 0xFF) as u8);
2541 pmt.extend_from_slice(audio_descriptors);
2542 pmt.extend_from_slice(&[0u8; 4]); let mut pmt_payload = vec![0u8];
2544 pmt_payload.extend_from_slice(&pmt);
2545 let pmt_pkt = ts_pkt(0x0100, true, 0b01, &pmt_payload);
2546
2547 let video_pes = {
2549 let mut pes = vec![
2550 0u8, 0u8, 1u8, 0xE0, 0u8, 0u8, 0x80, 0x80, 5, 0x21, 0x00, 0x01, 0x00, 0x01,
2551 ];
2552 pes.extend_from_slice(&[0xAAu8; 16]);
2553 pes
2554 };
2555 let video_pkt = ts_pkt(0x0200, true, 0b01, &video_pes);
2556
2557 let mut audio_pes = vec![
2566 0u8, 0u8, 1u8, 0xC0, 0u8, 0u8, 0x80, 0x80, 5, 0x21, 0x00, 0x01, 0x00, 0x01,
2567 ];
2568 audio_pes.extend_from_slice(audio_es);
2569
2570 let first_chunk_max = TS_PACKET - 4; let mut audio_pkts: Vec<[u8; TS_PACKET]> = Vec::new();
2574 let first_len = audio_pes.len().min(first_chunk_max);
2575 audio_pkts.push(ts_pkt(audio_pid, true, 0b01, &audio_pes[..first_len]));
2576 let mut cursor = first_len;
2577 while cursor < audio_pes.len() {
2578 let end = (cursor + first_chunk_max).min(audio_pes.len());
2579 audio_pkts.push(ts_pkt_continuation(audio_pid, &audio_pes[cursor..end]));
2580 cursor = end;
2581 }
2582
2583 let mut buf = Vec::new();
2584 buf.extend_from_slice(&pat_pkt);
2585 buf.extend_from_slice(&pmt_pkt);
2586 buf.extend_from_slice(&video_pkt);
2587 for pkt in &audio_pkts {
2588 buf.extend_from_slice(pkt);
2589 }
2590 buf.extend_from_slice(&ts_pkt(0x1FFF, false, 0b01, &[]));
2591 buf
2592 }
2593
2594 #[test]
2595 fn pmt_walker_classifies_aac_ac3_eac3_stream_types() {
2596 let mut pmt = vec![0x02];
2599 let stream_entries = 5 + 5 + 5 + 5; let pmt_section_len: usize = 9 + stream_entries + 4;
2601 pmt.push(0xB0 | ((pmt_section_len >> 8) & 0x0F) as u8);
2602 pmt.push((pmt_section_len & 0xFF) as u8);
2603 pmt.extend_from_slice(&[0x00, 0x01, 0xC1, 0x00, 0x00]);
2604 pmt.extend_from_slice(&[0xE2, 0x00, 0xF0, 0x00]); pmt.extend_from_slice(&[STREAM_TYPE_MPEG2_VIDEO, 0xE2, 0x00, 0xF0, 0x00]);
2606 pmt.extend_from_slice(&[STREAM_TYPE_AAC_ADTS, 0xE3, 0x00, 0xF0, 0x00]);
2607 pmt.extend_from_slice(&[STREAM_TYPE_AC3, 0xE4, 0x00, 0xF0, 0x00]);
2608 pmt.extend_from_slice(&[STREAM_TYPE_EAC3, 0xE5, 0x00, 0xF0, 0x00]);
2609 pmt.extend_from_slice(&[0u8; 4]);
2610
2611 let (video, audio) = parse_pmt_streams(&pmt).expect("parse");
2612 assert_eq!(video.len(), 1);
2613 assert_eq!(video[0].pid, 0x200);
2614 assert_eq!(audio.len(), 3);
2615 assert_eq!(
2616 (audio[0].pid, audio[0].kind),
2617 (0x300, AudioCodecKind::AacAdts)
2618 );
2619 assert_eq!((audio[1].pid, audio[1].kind), (0x400, AudioCodecKind::Ac3));
2620 assert_eq!((audio[2].pid, audio[2].kind), (0x500, AudioCodecKind::Eac3));
2621 }
2622
2623 #[test]
2624 fn pmt_walker_recognises_dvb_ac3_via_registration_descriptor() {
2625 let mut pmt = vec![0x02];
2628 let descriptors: [u8; 6] = [DESC_TAG_REGISTRATION, 4, b'A', b'C', b'-', b'3'];
2629 let stream_entries = 5 + 5 + descriptors.len();
2630 let pmt_section_len: usize = 9 + stream_entries + 4;
2631 pmt.push(0xB0 | ((pmt_section_len >> 8) & 0x0F) as u8);
2632 pmt.push((pmt_section_len & 0xFF) as u8);
2633 pmt.extend_from_slice(&[0x00, 0x01, 0xC1, 0x00, 0x00]);
2634 pmt.extend_from_slice(&[0xE2, 0x00, 0xF0, 0x00]);
2635 pmt.extend_from_slice(&[STREAM_TYPE_MPEG2_VIDEO, 0xE2, 0x00, 0xF0, 0x00]);
2636 pmt.push(STREAM_TYPE_PES_PRIVATE);
2637 pmt.extend_from_slice(&[0xE3, 0x00]);
2638 let esi_len = descriptors.len() as u16;
2639 pmt.push(0xF0 | ((esi_len >> 8) & 0x0F) as u8);
2640 pmt.push((esi_len & 0xFF) as u8);
2641 pmt.extend_from_slice(&descriptors);
2642 pmt.extend_from_slice(&[0u8; 4]);
2643
2644 let (_, audio) = parse_pmt_streams(&pmt).expect("parse");
2645 assert_eq!(audio.len(), 1);
2646 assert_eq!(audio[0].kind, AudioCodecKind::Ac3);
2647 assert_eq!(audio[0].stream_type, STREAM_TYPE_PES_PRIVATE);
2648 }
2649
2650 #[test]
2651 fn pmt_walker_recognises_dvb_eac3_via_registration_descriptor() {
2652 let mut pmt = vec![0x02];
2653 let descriptors: [u8; 6] = [DESC_TAG_REGISTRATION, 4, b'E', b'A', b'C', b'3'];
2654 let stream_entries = 5 + 5 + descriptors.len();
2655 let pmt_section_len: usize = 9 + stream_entries + 4;
2656 pmt.push(0xB0 | ((pmt_section_len >> 8) & 0x0F) as u8);
2657 pmt.push((pmt_section_len & 0xFF) as u8);
2658 pmt.extend_from_slice(&[0x00, 0x01, 0xC1, 0x00, 0x00]);
2659 pmt.extend_from_slice(&[0xE2, 0x00, 0xF0, 0x00]);
2660 pmt.extend_from_slice(&[STREAM_TYPE_MPEG2_VIDEO, 0xE2, 0x00, 0xF0, 0x00]);
2661 pmt.push(STREAM_TYPE_PES_PRIVATE);
2662 pmt.extend_from_slice(&[0xE3, 0x00]);
2663 let esi_len = descriptors.len() as u16;
2664 pmt.push(0xF0 | ((esi_len >> 8) & 0x0F) as u8);
2665 pmt.push((esi_len & 0xFF) as u8);
2666 pmt.extend_from_slice(&descriptors);
2667 pmt.extend_from_slice(&[0u8; 4]);
2668
2669 let (_, audio) = parse_pmt_streams(&pmt).expect("parse");
2670 assert_eq!(audio.len(), 1);
2671 assert_eq!(audio[0].kind, AudioCodecKind::Eac3);
2672 }
2673
2674 #[test]
2675 fn extract_ac3_frames_from_synthetic_ts_yields_passthrough_track() {
2676 let frame = synth_ac3_frame_stereo_48k_128k();
2678 let mut es = frame.clone();
2680 es.extend_from_slice(&frame);
2681 let buf = build_ts_with_audio(STREAM_TYPE_AC3, &[], 0x300, &es);
2682
2683 let d = demux_ts(&buf).expect("demux");
2684 let audio = d.audio.expect("AC-3 audio surfaced");
2685 assert_eq!(audio.codec, "ac3");
2686 assert_eq!(audio.channels, 2);
2687 assert_eq!(audio.sample_rate, 48_000);
2688 assert_eq!(audio.timescale, 48_000);
2689 assert_eq!(audio.codec_private.len(), 3);
2692 assert!(
2695 audio.samples.len() >= 1,
2696 "at least one AC-3 frame extracted"
2697 );
2698 assert_eq!(
2699 &audio.samples[0][..2],
2700 &[0x0B, 0x77],
2701 "AC-3 frame begins with 0x0B77 sync word verbatim"
2702 );
2703 assert!(
2705 audio.durations.iter().all(|&d| d == 1536),
2706 "AC-3 frames are 1536 samples each"
2707 );
2708 }
2709
2710 #[test]
2711 fn extract_eac3_frames_from_synthetic_ts_yields_passthrough_track() {
2712 let frame = synth_eac3_frame_stereo_48k_192bytes();
2713 let mut es = frame.clone();
2714 es.extend_from_slice(&frame);
2715 let buf = build_ts_with_audio(STREAM_TYPE_EAC3, &[], 0x300, &es);
2716
2717 let d = demux_ts(&buf).expect("demux");
2718 let audio = d.audio.expect("E-AC-3 audio surfaced");
2719 assert_eq!(audio.codec, "eac3");
2720 assert_eq!(audio.channels, 2);
2721 assert_eq!(audio.sample_rate, 48_000);
2722 assert_eq!(audio.codec_private.len(), 5);
2724 assert!(!audio.samples.is_empty());
2725 assert_eq!(
2726 &audio.samples[0][..2],
2727 &[0x0B, 0x77],
2728 "E-AC-3 frame begins with 0x0B77 sync word verbatim"
2729 );
2730 assert!(audio.durations.iter().all(|&d| d == 1536));
2732 }
2733
2734 #[test]
2735 fn extract_ac3_via_pes_private_with_dvb_registration() {
2736 let frame = synth_ac3_frame_stereo_48k_128k();
2739 let descriptors: [u8; 6] = [DESC_TAG_REGISTRATION, 4, b'A', b'C', b'-', b'3'];
2740 let buf = build_ts_with_audio(STREAM_TYPE_PES_PRIVATE, &descriptors, 0x300, &frame);
2741 let d = demux_ts(&buf).expect("demux");
2742 let audio = d.audio.expect("AC-3 audio via DVB registration surfaced");
2743 assert_eq!(audio.codec, "ac3");
2744 assert_eq!(&audio.samples[0][..2], &[0x0B, 0x77]);
2745 }
2746
2747 #[test]
2748 fn dac3_body_synthesized_from_first_ts_frame_matches_sync_header() {
2749 let frame = synth_ac3_frame_stereo_48k_128k();
2754 let buf = build_ts_with_audio(STREAM_TYPE_AC3, &[], 0x300, &frame);
2755 let d = demux_ts(&buf).expect("demux");
2756 let audio = d.audio.expect("AC-3 audio");
2757 let parsed = match crate::ac3_sync::parse_sync_info(&frame).unwrap() {
2758 crate::ac3_sync::SyncInfo::Ac3(s) => s,
2759 _ => panic!("expected AC-3"),
2760 };
2761 let expected = crate::mux::dac3_body_from_sync(&parsed);
2762 assert_eq!(
2763 audio.codec_private,
2764 expected.to_vec(),
2765 "TS-extracted dac3 must match the canonical helper"
2766 );
2767 }
2768
2769 fn build_two_program_ts() -> Vec<u8> {
2774 let mut pat = vec![0x00];
2776 let pat_section_len: usize = 5 + 4 + 4 + 4; pat.push(0xB0 | ((pat_section_len >> 8) & 0x0F) as u8);
2778 pat.push((pat_section_len & 0xFF) as u8);
2779 pat.extend_from_slice(&[0x00, 0x01, 0xC1, 0x00, 0x00]);
2780 pat.extend_from_slice(&[0x00, 0x01, 0xE1, 0x00]); pat.extend_from_slice(&[0x00, 0x02, 0xE1, 0x01]); pat.extend_from_slice(&[0u8; 4]);
2783 let mut pat_payload = vec![0u8];
2784 pat_payload.extend_from_slice(&pat);
2785 let pat_pkt = ts_pkt(0x0000, true, 0b01, &pat_payload);
2786
2787 let mut pmt1 = vec![0x02];
2789 let pmt1_section_len: usize = 9 + 5 + 4;
2790 pmt1.push(0xB0 | ((pmt1_section_len >> 8) & 0x0F) as u8);
2791 pmt1.push((pmt1_section_len & 0xFF) as u8);
2792 pmt1.extend_from_slice(&[0x00, 0x01, 0xC1, 0x00, 0x00]); pmt1.extend_from_slice(&[0xE2, 0x00, 0xF0, 0x00]);
2794 pmt1.extend_from_slice(&[STREAM_TYPE_MPEG2_VIDEO, 0xE2, 0x00, 0xF0, 0x00]);
2795 pmt1.extend_from_slice(&[0u8; 4]);
2796 let mut pmt1_payload = vec![0u8];
2797 pmt1_payload.extend_from_slice(&pmt1);
2798 let pmt1_pkt = ts_pkt(0x0100, true, 0b01, &pmt1_payload);
2799
2800 let mut pmt2 = vec![0x02];
2802 let pmt2_section_len: usize = 9 + 5 + 4;
2803 pmt2.push(0xB0 | ((pmt2_section_len >> 8) & 0x0F) as u8);
2804 pmt2.push((pmt2_section_len & 0xFF) as u8);
2805 pmt2.extend_from_slice(&[0x00, 0x02, 0xC1, 0x00, 0x00]); pmt2.extend_from_slice(&[0xE3, 0x00, 0xF0, 0x00]);
2807 pmt2.extend_from_slice(&[STREAM_TYPE_H264, 0xE3, 0x00, 0xF0, 0x00]);
2808 pmt2.extend_from_slice(&[0u8; 4]);
2809 let mut pmt2_payload = vec![0u8];
2810 pmt2_payload.extend_from_slice(&pmt2);
2811 let pmt2_pkt = ts_pkt(0x0101, true, 0b01, &pmt2_payload);
2812
2813 let make_pes = |fill: u8| {
2816 let mut pes = vec![
2817 0u8, 0u8, 1u8, 0xE0, 0u8, 0u8, 0x80, 0x80, 5, 0x21, 0x00, 0x01, 0x00, 0x01,
2818 ];
2819 pes.extend_from_slice(&[fill; 16]);
2820 pes
2821 };
2822 let p1_pes = ts_pkt(0x0200, true, 0b01, &make_pes(0xAA));
2823 let p2_pes = ts_pkt(0x0300, true, 0b01, &make_pes(0xBB));
2824
2825 let mut buf = Vec::new();
2826 buf.extend_from_slice(&pat_pkt);
2827 buf.extend_from_slice(&pmt1_pkt);
2828 buf.extend_from_slice(&pmt2_pkt);
2829 buf.extend_from_slice(&p1_pes);
2831 buf.extend_from_slice(&p2_pes);
2832 buf.extend_from_slice(&ts_pkt(0x0200, true, 0b01, &make_pes(0xAA)));
2833 buf.extend_from_slice(&ts_pkt(0x0300, true, 0b01, &make_pes(0xBB)));
2834 buf.extend_from_slice(&ts_pkt(0x1FFF, false, 0b01, &[]));
2835 buf
2836 }
2837
2838 #[test]
2839 fn streaming_demuxer_lists_all_pat_programs() {
2840 let buf = build_two_program_ts();
2841 let dem = demux_ts_streaming_init(&buf).expect("init");
2842 let progs = dem.programs();
2843 assert_eq!(progs.len(), 2, "PAT advertised 2 programs");
2844 let nums: Vec<u16> = progs.iter().map(|p| p.program_number).collect();
2845 assert_eq!(nums, vec![1, 2]);
2846 assert_eq!(progs[0].pmt_pid, 0x100);
2847 assert_eq!(progs[1].pmt_pid, 0x101);
2848 assert_eq!(progs[0].video_streams[0].pid, 0x200);
2850 assert_eq!(
2851 progs[0].video_streams[0].stream_type,
2852 STREAM_TYPE_MPEG2_VIDEO
2853 );
2854 assert_eq!(progs[1].video_streams[0].pid, 0x300);
2855 assert_eq!(progs[1].video_streams[0].stream_type, STREAM_TYPE_H264);
2856 }
2857
2858 #[test]
2859 fn streaming_demuxer_default_picks_first_program() {
2860 let buf = build_two_program_ts();
2861 let mut dem = demux_ts_streaming_init(&buf).expect("init");
2862 assert_eq!(dem.active_program_index(), 0);
2863 assert_eq!(dem.header().codec, "mpeg2", "program 1 is MPEG-2");
2864 let s = dem.next_video_sample().expect("sample").expect("some");
2866 assert!(
2867 s.data.iter().any(|&b| b == 0xAA),
2868 "program 1 sample should carry 0xAA"
2869 );
2870 assert!(
2871 !s.data.iter().any(|&b| b == 0xBB),
2872 "program 1 sample must not carry program 2's 0xBB"
2873 );
2874 }
2875
2876 #[test]
2877 fn streaming_demuxer_select_program_switches_active_streams() {
2878 let buf = build_two_program_ts();
2879 let mut dem = demux_ts_streaming_init(&buf).expect("init");
2880 dem.select_program(2).expect("switch to program 2");
2881 assert_eq!(dem.active_program_index(), 1);
2882 assert_eq!(dem.header().codec, "h264", "program 2 is H.264");
2883 let s = dem.next_video_sample().expect("sample").expect("some");
2884 assert!(
2885 s.data.iter().any(|&b| b == 0xBB),
2886 "program 2 sample should carry 0xBB"
2887 );
2888 assert!(
2889 !s.data.iter().any(|&b| b == 0xAA),
2890 "program 2 sample must not carry program 1's 0xAA"
2891 );
2892 }
2893
2894 #[test]
2895 fn streaming_demuxer_select_program_rejects_unknown_number() {
2896 let buf = build_two_program_ts();
2897 let mut dem = demux_ts_streaming_init(&buf).expect("init");
2898 assert!(
2899 dem.select_program(99).is_err(),
2900 "unknown program_number must error rather than silently no-op"
2901 );
2902 }
2903
2904 fn build_encrypted_ts() -> Vec<u8> {
2909 let mut pat = vec![0x00];
2911 let pat_section_len: usize = 5 + 4 + 4;
2912 pat.push(0xB0 | ((pat_section_len >> 8) & 0x0F) as u8);
2913 pat.push((pat_section_len & 0xFF) as u8);
2914 pat.extend_from_slice(&[0x00, 0x01, 0xC1, 0x00, 0x00]);
2915 pat.extend_from_slice(&[0x00, 0x01, 0xE1, 0x00, 0u8, 0u8, 0u8, 0u8]);
2916 let mut pat_payload = vec![0u8];
2917 pat_payload.extend_from_slice(&pat);
2918 let pat_pkt = ts_pkt(0x0000, true, 0b01, &pat_payload);
2919
2920 let mut pmt = vec![0x02];
2921 let pmt_section_len: usize = 9 + 5 + 4;
2922 pmt.push(0xB0 | ((pmt_section_len >> 8) & 0x0F) as u8);
2923 pmt.push((pmt_section_len & 0xFF) as u8);
2924 pmt.extend_from_slice(&[0x00, 0x01, 0xC1, 0x00, 0x00]);
2925 pmt.extend_from_slice(&[0xE2, 0x00, 0xF0, 0x00]);
2926 pmt.extend_from_slice(&[STREAM_TYPE_MPEG2_VIDEO, 0xE2, 0x00, 0xF0, 0x00]);
2927 pmt.extend_from_slice(&[0u8; 4]);
2928 let mut pmt_payload = vec![0u8];
2929 pmt_payload.extend_from_slice(&pmt);
2930 let pmt_pkt = ts_pkt(0x0100, true, 0b01, &pmt_payload);
2931
2932 let video_pes = {
2935 let mut pes = vec![
2936 0u8, 0u8, 1u8, 0xE0, 0u8, 0u8, 0x80, 0x80, 5, 0x21, 0x00, 0x01, 0x00, 0x01,
2937 ];
2938 pes.extend_from_slice(&[0xAAu8; 16]);
2939 pes
2940 };
2941 let mut video_pkt = ts_pkt(0x0200, true, 0b01, &video_pes);
2942 video_pkt[3] = (video_pkt[3] & 0x3F) | (0x01 << 6);
2944
2945 let mut buf = Vec::new();
2946 buf.extend_from_slice(&pat_pkt);
2947 buf.extend_from_slice(&pmt_pkt);
2948 buf.extend_from_slice(&video_pkt);
2949 buf.extend_from_slice(&ts_pkt(0x1FFF, false, 0b01, &[]));
2950 buf
2951 }
2952
2953 #[test]
2954 fn streaming_demuxer_drops_video_when_active_pid_is_scrambled() {
2955 let buf = build_encrypted_ts();
2956 let mut dem = demux_ts_streaming_init(&buf).expect("init");
2957 let s = dem.next_video_sample().expect("call must not error");
2960 assert!(
2961 s.is_none(),
2962 "encrypted TS → next_video_sample returns None on first call"
2963 );
2964 let s2 = dem.next_video_sample().expect("call must not error");
2966 assert!(
2967 s2.is_none(),
2968 "encrypted TS → guard remains latched on subsequent calls"
2969 );
2970 }
2971}