1use anyhow::{Context, Result};
2use bytes::Bytes;
3use codec::encode::EncodedPacket;
4use codec::frame::ColorMetadata;
5use std::fs::File;
6use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write};
7use std::path::Path;
8use tempfile::NamedTempFile;
9
10use crate::AudioInfo;
11
12pub struct Av1Mp4Muxer {
38 width: u32,
39 height: u32,
40 frame_rate: f64,
41 mdat_tmp: NamedTempFile,
42 mdat_writer: BufWriter<File>,
43 sample_sizes: Vec<u32>,
44 keyframe_indices: Vec<u32>,
45 first_packet_header: Option<Vec<u8>>,
46 packet_count: u32,
47 mdat_payload_bytes: u64,
48 audio: Option<AudioTrackState>,
49 color_metadata: ColorMetadata,
55 #[doc(hidden)]
66 force_largesize_mdat: bool,
67}
68
69struct AudioTrackState {
72 info: AudioInfo,
73 audio_tmp: NamedTempFile,
74 audio_writer: BufWriter<File>,
75 sample_sizes: Vec<u32>,
76 durations: Vec<u32>,
77 total_duration_ticks: u64,
78 mdat_payload_bytes: u64,
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85enum AudioCodecKind {
86 Aac,
87 Opus,
88 Ac3,
89 Eac3,
90}
91
92impl AudioCodecKind {
93 fn from_codec_tag(codec: &str) -> Option<Self> {
94 if codec.eq_ignore_ascii_case("aac") {
95 Some(Self::Aac)
96 } else if codec.eq_ignore_ascii_case("opus") {
97 Some(Self::Opus)
98 } else if codec.eq_ignore_ascii_case("ac3") || codec.eq_ignore_ascii_case("ac-3") {
99 Some(Self::Ac3)
100 } else if codec.eq_ignore_ascii_case("eac3") || codec.eq_ignore_ascii_case("e-ac-3") {
101 Some(Self::Eac3)
102 } else {
103 None
104 }
105 }
106}
107
108impl Av1Mp4Muxer {
109 pub fn new(width: u32, height: u32, frame_rate: f64) -> Result<Self> {
110 let mdat_tmp = NamedTempFile::new().context("creating mdat tempfile")?;
111 let handle = mdat_tmp
112 .reopen()
113 .context("reopening mdat tempfile for write")?;
114 let mdat_writer = BufWriter::new(handle);
115 Ok(Self {
116 width,
117 height,
118 frame_rate,
119 mdat_tmp,
120 mdat_writer,
121 sample_sizes: Vec::new(),
122 keyframe_indices: Vec::new(),
123 first_packet_header: None,
124 packet_count: 0,
125 mdat_payload_bytes: 0,
126 audio: None,
127 color_metadata: ColorMetadata::default(),
128 force_largesize_mdat: false,
129 })
130 }
131
132 #[doc(hidden)]
137 pub fn force_largesize_mdat_for_test(&mut self) -> &mut Self {
138 self.force_largesize_mdat = true;
139 self
140 }
141
142 pub fn set_color_metadata(&mut self, color_metadata: ColorMetadata) -> &mut Self {
150 self.color_metadata = color_metadata;
151 self
152 }
153
154 pub fn add_packet(&mut self, packet: EncodedPacket) -> Result<()> {
155 if self.first_packet_header.is_none() {
158 self.first_packet_header = Some(packet.data.to_vec());
159 }
160 let size = packet.data.len() as u32;
161 self.mdat_writer
162 .write_all(&packet.data)
163 .context("writing packet to mdat tempfile")?;
164 self.sample_sizes.push(size);
165 self.packet_count = self
166 .packet_count
167 .checked_add(1)
168 .context("packet count overflow")?;
169 if packet.is_keyframe {
170 self.keyframe_indices.push(self.packet_count);
171 }
172 self.mdat_payload_bytes = self
173 .mdat_payload_bytes
174 .checked_add(size as u64)
175 .context("mdat payload overflow")?;
176 Ok(())
177 }
178
179 pub fn with_audio(&mut self, info: AudioInfo) -> Result<&mut Self> {
219 let codec_kind = AudioCodecKind::from_codec_tag(&info.codec).ok_or_else(|| {
224 anyhow::anyhow!(
225 "audio mux: only AAC-LC, Opus, AC-3, E-AC-3 are supported; got codec '{}'",
226 info.codec
227 )
228 })?;
229 match codec_kind {
241 AudioCodecKind::Aac => {
242 if !matches!(info.channels, 1 | 2 | 6 | 7) {
243 anyhow::bail!(
244 "audio mux: AAC supports mono/stereo/5.1(channels=6)/7.1(channels=7) layouts; \
245 got {} channels — extended Atmos / object layouts are not supported",
246 info.channels
247 );
248 }
249 }
250 AudioCodecKind::Opus => {
251 if info.channels < 1 || info.channels > 8 {
252 anyhow::bail!(
253 "audio mux: Opus supports 1..=8 channels; got {}",
254 info.channels
255 );
256 }
257 }
258 AudioCodecKind::Ac3 | AudioCodecKind::Eac3 => {
259 if !(1..=6).contains(&info.channels) {
260 anyhow::bail!(
261 "audio mux: AC-3 / E-AC-3 channel count must be 1..=6 (mono..5.1); got {}",
262 info.channels
263 );
264 }
265 }
266 }
267 if info.sample_rate == 0 {
268 anyhow::bail!("audio mux: sample_rate must be > 0");
269 }
270 if info.timescale == 0 {
271 anyhow::bail!("audio mux: timescale must be > 0");
272 }
273 match codec_kind {
274 AudioCodecKind::Aac => {
275 if info.asc_bytes.is_empty() {
276 anyhow::bail!("audio mux: AudioSpecificConfig bytes missing");
277 }
278 let parsed = crate::aac_asc::parse_aac_asc(&info.asc_bytes)
283 .with_context(|| "audio mux: failed to parse AudioSpecificConfig")?;
284 use crate::aac_asc::AscSignaling;
285 match parsed.signaling {
286 AscSignaling::ImplicitMaybe => {
287 anyhow::bail!(
288 "audio mux: ASC uses implicit HE-AAC signaling (AOT=2 core at \
289 {} Hz with no SBR/PS layer in the ASC). Apple players silently \
290 downgrade to mono 22.05 kHz core. Caller must upgrade with \
291 aac_asc::upgrade_to_explicit_signaling before muxing.",
292 parsed.sample_rate
293 );
294 }
295 AscSignaling::NoExtension
296 | AscSignaling::ExplicitSbr
297 | AscSignaling::ExplicitPs => {
298 let core_aot = parsed.aot;
303 if !matches!(core_aot, 2 | 42) {
304 anyhow::bail!(
305 "audio mux: only AAC-LC (AOT=2) and xHE-AAC USAC (AOT=42) \
306 cores are supported; ASC core AOT={}",
307 core_aot
308 );
309 }
310 }
311 }
312 }
313 AudioCodecKind::Opus => {
314 if info.codec_private.len() < 11 {
320 anyhow::bail!(
321 "audio mux: Opus codec_private must be ≥11 bytes (RFC 7845 §5.1 \
322 minimum body for ChannelMappingFamily=0); got {} bytes",
323 info.codec_private.len()
324 );
325 }
326 if info.timescale != 48_000 {
332 anyhow::bail!(
333 "audio mux: Opus mdhd timescale must be 48000 (RFC 7845 §3); \
334 got timescale={}",
335 info.timescale
336 );
337 }
338 let cmf = info.codec_private[10];
346 match cmf {
347 0 => {
348 if info.channels > 2 {
351 anyhow::bail!(
352 "audio mux: Opus ChannelMappingFamily=0 only supports 1..=2 channels; got {}",
353 info.channels
354 );
355 }
356 }
357 1 => {
358 let n = info.channels as usize;
362 let needed = 11 + 2 + n;
363 if info.codec_private.len() < needed {
364 anyhow::bail!(
365 "audio mux: Opus family=1 codec_private must be ≥{needed} bytes \
366 (11 preamble + 2 stream/coupled + {n} mapping); got {}",
367 info.codec_private.len()
368 );
369 }
370 let stream_count = info.codec_private[11];
371 let coupled_count = info.codec_private[12];
372 if stream_count < 1 {
380 anyhow::bail!(
381 "audio mux: Opus family=1 StreamCount must be >= 1; got {stream_count}"
382 );
383 }
384 if coupled_count > stream_count {
385 anyhow::bail!(
386 "audio mux: Opus family=1 CoupledCount ({coupled_count}) > StreamCount ({stream_count})"
387 );
388 }
389 if (stream_count as u16) + (coupled_count as u16) > info.channels {
390 anyhow::bail!(
391 "audio mux: Opus family=1 StreamCount ({stream_count}) + CoupledCount ({coupled_count}) > channels ({})",
392 info.channels
393 );
394 }
395 let mapping_max = stream_count + coupled_count;
398 for i in 0..n {
399 let m = info.codec_private[13 + i];
400 if m >= mapping_max {
401 anyhow::bail!(
402 "audio mux: Opus family=1 ChannelMapping[{i}]={m} \
403 exceeds streams+coupled ({mapping_max})"
404 );
405 }
406 }
407 }
408 other => {
409 anyhow::bail!(
410 "audio mux: only Opus ChannelMappingFamily 0 (mono/stereo) and 1 (surround 1..=8) supported; \
411 got family={other}"
412 );
413 }
414 }
415 }
416 AudioCodecKind::Ac3 => {
417 if info.codec_private.len() != 3 {
421 anyhow::bail!(
422 "audio mux: AC-3 codec_private (dac3 body) must be exactly 3 bytes \
423 per ETSI TS 102 366 §F.4; got {} bytes",
424 info.codec_private.len()
425 );
426 }
427 match info.sample_rate {
429 32_000 | 44_100 | 48_000 => {}
430 other => anyhow::bail!(
431 "audio mux: AC-3 sample_rate must be 32000 / 44100 / 48000; got {}",
432 other
433 ),
434 }
435 }
436 AudioCodecKind::Eac3 => {
437 if info.codec_private.len() < 5 {
443 anyhow::bail!(
444 "audio mux: E-AC-3 codec_private (dec3 body) must be ≥5 bytes \
445 per ETSI TS 102 366 §F.6; got {} bytes",
446 info.codec_private.len()
447 );
448 }
449 match info.sample_rate {
452 16_000 | 22_050 | 24_000 | 32_000 | 44_100 | 48_000 => {}
453 other => anyhow::bail!(
454 "audio mux: E-AC-3 sample_rate must be 16000 / 22050 / 24000 / 32000 / \
455 44100 / 48000; got {}",
456 other
457 ),
458 }
459 }
460 }
461 if self.audio.is_some() {
462 anyhow::bail!("audio mux: with_audio called twice");
463 }
464 let audio_tmp = NamedTempFile::new().context("creating audio mdat tempfile")?;
465 let handle = audio_tmp
466 .reopen()
467 .context("reopening audio tempfile for write")?;
468 let audio_writer = BufWriter::new(handle);
469 self.audio = Some(AudioTrackState {
470 info,
471 audio_tmp,
472 audio_writer,
473 sample_sizes: Vec::new(),
474 durations: Vec::new(),
475 total_duration_ticks: 0,
476 mdat_payload_bytes: 0,
477 });
478 Ok(self)
479 }
480
481 pub fn add_audio_sample(
488 &mut self,
489 sample: &[u8],
490 _pts_ticks: u64,
491 duration_ticks: u32,
492 ) -> Result<()> {
493 let audio = self
494 .audio
495 .as_mut()
496 .context("audio mux: add_audio_sample called before with_audio")?;
497 if sample.is_empty() {
498 anyhow::bail!("audio mux: refusing to add empty audio access unit");
499 }
500 audio
501 .audio_writer
502 .write_all(sample)
503 .context("writing audio sample to tempfile")?;
504 audio.sample_sizes.push(sample.len() as u32);
505 let dur = if duration_ticks == 0 {
506 match AudioCodecKind::from_codec_tag(&audio.info.codec) {
515 Some(AudioCodecKind::Aac) => 1024,
516 Some(AudioCodecKind::Opus) => 960,
517 Some(AudioCodecKind::Ac3) | Some(AudioCodecKind::Eac3) => 1536,
518 None => 1024, }
520 } else {
521 duration_ticks
522 };
523 audio.durations.push(dur);
524 audio.total_duration_ticks = audio
525 .total_duration_ticks
526 .checked_add(dur as u64)
527 .context("audio total duration overflow")?;
528 audio.mdat_payload_bytes = audio
529 .mdat_payload_bytes
530 .checked_add(sample.len() as u64)
531 .context("audio mdat payload overflow")?;
532 Ok(())
533 }
534
535 pub fn finalize_to_file(mut self, output_path: &Path) -> Result<()> {
543 if self.packet_count == 0 {
544 anyhow::bail!("cannot finalize MP4 with zero packets");
545 }
546 self.mdat_writer.flush().context("flushing mdat tempfile")?;
547 if let Some(ref mut audio) = self.audio {
548 audio
549 .audio_writer
550 .flush()
551 .context("flushing audio mdat tempfile")?;
552 if audio.sample_sizes.is_empty() {
553 tracing::warn!(
557 "audio mux: with_audio called but no samples pushed; dropping audio"
558 );
559 self.audio = None;
560 }
561 }
562
563 let video_timescale: u32 = 90_000;
566 let frame_duration: u32 = ((video_timescale as f64) / self.frame_rate)
567 .round()
568 .max(1.0) as u32;
569 let total_video_duration: u64 = frame_duration as u64 * self.packet_count as u64;
570
571 let first_packet = self
572 .first_packet_header
573 .as_ref()
574 .context("first packet header missing; add_packet never called?")?;
575 let config_obus = extract_sequence_header(first_packet)
576 .context("extracting AV1 sequence header OBU from first packet")?;
577
578 let ftyp = build_ftyp();
579
580 let video_spc: u32 = (self.frame_rate.round() as u32).max(1).min(120);
584
585 let movie_timescale: u32 = video_timescale;
594
595 let audio_plan: Option<AudioBuildPlan> = self.audio.as_ref().map(|a| {
596 let frames_per_sec = match AudioCodecKind::from_codec_tag(&a.info.codec) {
606 Some(AudioCodecKind::Opus) => (a.info.timescale as f64) / 960.0,
607 Some(AudioCodecKind::Ac3) | Some(AudioCodecKind::Eac3) => {
609 (a.info.timescale as f64) / 1536.0
610 }
611 Some(AudioCodecKind::Aac) | None => (a.info.timescale as f64) / 1024.0,
612 };
613 let audio_spc = (frames_per_sec.round() as u32).max(1).min(200);
614 let audio_duration_movie: u64 =
615 ((a.total_duration_ticks as u128) * movie_timescale as u128
616 / a.info.timescale.max(1) as u128) as u64;
617 AudioBuildPlan {
618 info: a.info.clone(),
619 sample_sizes: a.sample_sizes.clone(),
620 durations: a.durations.clone(),
621 total_duration_in_own_ts: a.total_duration_ticks,
622 total_duration_in_movie_ts: audio_duration_movie,
623 samples_per_chunk: audio_spc,
624 }
625 });
626
627 let video_duration_movie: u64 = total_video_duration; let movie_duration: u64 = match audio_plan.as_ref() {
629 Some(p) => video_duration_movie.max(p.total_duration_in_movie_ts),
630 None => video_duration_movie,
631 };
632
633 let video_payload_bytes = self.mdat_payload_bytes;
635 let audio_payload_bytes = audio_plan
636 .as_ref()
637 .map(|p| p.sample_sizes.iter().map(|&s| s as u64).sum::<u64>())
638 .unwrap_or(0);
639 let mdat_payload_total = video_payload_bytes
640 .checked_add(audio_payload_bytes)
641 .context("combined mdat payload overflow")?;
642
643 let mdat_payload_plus_short_header = 8u64
652 .checked_add(mdat_payload_total)
653 .context("mdat short-header size overflow")?;
654 let use_largesize_mdat =
658 mdat_payload_plus_short_header > u32::MAX as u64 || self.force_largesize_mdat;
659 let mdat_header_len: u64 = if use_largesize_mdat { 16 } else { 8 };
660 let mdat_box_size: u64 = mdat_header_len
661 .checked_add(mdat_payload_total)
662 .context("mdat box size overflow")?;
663
664 let video_chunk_count = chunk_count_of(self.sample_sizes.len(), video_spc);
668 let audio_chunk_count = audio_plan
669 .as_ref()
670 .map(|p| chunk_count_of(p.sample_sizes.len(), p.samples_per_chunk))
671 .unwrap_or(0);
672 let video_zero_offsets: Vec<u64> = vec![0; video_chunk_count];
673 let audio_zero_offsets: Vec<u64> = vec![0; audio_chunk_count];
674
675 let moov_co64_size = build_moov_any(
676 self.width,
677 self.height,
678 video_timescale,
679 movie_timescale,
680 movie_duration,
681 total_video_duration,
682 frame_duration,
683 &self.sample_sizes,
684 &self.keyframe_indices,
685 &config_obus,
686 &video_zero_offsets,
687 video_spc,
688 audio_plan.as_ref(),
689 &audio_zero_offsets,
690 true,
691 &self.color_metadata,
692 )
693 .len() as u64;
694
695 let upper_bound: u64 = (ftyp.len() as u64)
696 .checked_add(moov_co64_size)
697 .context("moov size overflow")?
698 .checked_add(mdat_header_len)
699 .context("mdat header overflow")?
700 .checked_add(mdat_payload_total)
701 .context("mdat payload overflow")?;
702 let use_co64 = upper_bound > u32::MAX as u64;
703
704 let moov_without_offsets = build_moov_any(
705 self.width,
706 self.height,
707 video_timescale,
708 movie_timescale,
709 movie_duration,
710 total_video_duration,
711 frame_duration,
712 &self.sample_sizes,
713 &self.keyframe_indices,
714 &config_obus,
715 &video_zero_offsets,
716 video_spc,
717 audio_plan.as_ref(),
718 &audio_zero_offsets,
719 use_co64,
720 &self.color_metadata,
721 );
722
723 let mdat_offset_in_file = (ftyp.len() + moov_without_offsets.len()) as u64;
724 let first_sample_file_offset = mdat_offset_in_file + mdat_header_len;
725 if !use_co64 && first_sample_file_offset > u32::MAX as u64 {
726 anyhow::bail!(
727 "internal: chose stco but first_sample_file_offset {} exceeds u32",
728 first_sample_file_offset
729 );
730 }
731
732 let (video_chunk_offsets, audio_chunk_offsets, interleave_plan) = plan_interleaved_layout(
736 first_sample_file_offset,
737 &self.sample_sizes,
738 video_spc,
739 audio_plan.as_ref(),
740 );
741 debug_assert_eq!(video_chunk_offsets.len(), video_chunk_count);
742 debug_assert_eq!(audio_chunk_offsets.len(), audio_chunk_count);
743
744 let moov = build_moov_any(
745 self.width,
746 self.height,
747 video_timescale,
748 movie_timescale,
749 movie_duration,
750 total_video_duration,
751 frame_duration,
752 &self.sample_sizes,
753 &self.keyframe_indices,
754 &config_obus,
755 &video_chunk_offsets,
756 video_spc,
757 audio_plan.as_ref(),
758 &audio_chunk_offsets,
759 use_co64,
760 &self.color_metadata,
761 );
762
763 assert_eq!(
764 moov.len(),
765 moov_without_offsets.len(),
766 "moov size must be stable across rebuild"
767 );
768
769 let out_file = File::create(output_path)
771 .with_context(|| format!("creating output file {}", output_path.display()))?;
772 let mut out = BufWriter::new(out_file);
773 out.write_all(&ftyp).context("writing ftyp")?;
774 out.write_all(&moov).context("writing moov")?;
775 if use_largesize_mdat {
776 out.write_all(&1u32.to_be_bytes())
778 .context("writing mdat largesize sentinel")?;
779 out.write_all(b"mdat").context("writing mdat type")?;
780 out.write_all(&mdat_box_size.to_be_bytes())
781 .context("writing mdat largesize")?;
782 } else {
783 let mdat_size_u32 = mdat_box_size as u32;
784 out.write_all(&mdat_size_u32.to_be_bytes())
785 .context("writing mdat size")?;
786 out.write_all(b"mdat").context("writing mdat type")?;
787 }
788
789 let video_payload_handle = self
794 .mdat_tmp
795 .reopen()
796 .context("reopening mdat tempfile for read")?;
797 let mut video_payload = BufReader::new(video_payload_handle);
798 video_payload
799 .seek(SeekFrom::Start(0))
800 .context("rewinding mdat tempfile")?;
801
802 let mut audio_payload: Option<BufReader<File>> = match self.audio.as_ref() {
803 Some(a) => {
804 let h = a
805 .audio_tmp
806 .reopen()
807 .context("reopening audio mdat tempfile for read")?;
808 let mut r = BufReader::new(h);
809 r.seek(SeekFrom::Start(0))
810 .context("rewinding audio mdat tempfile")?;
811 Some(r)
812 }
813 None => None,
814 };
815
816 let mut video_copied: u64 = 0;
817 let mut audio_copied: u64 = 0;
818 for step in &interleave_plan {
819 match step.track {
820 InterleaveTrack::Video => {
821 let copied =
822 std::io::copy(&mut (&mut video_payload).take(step.bytes), &mut out)
823 .context("copying video chunk into mdat")?;
824 if copied != step.bytes {
825 anyhow::bail!(
826 "video chunk short read: wanted {}, got {}",
827 step.bytes,
828 copied
829 );
830 }
831 video_copied += copied;
832 }
833 InterleaveTrack::Audio => {
834 let audio_r = audio_payload.as_mut().context(
835 "internal: interleave plan has audio step but no audio tempfile",
836 )?;
837 let copied = std::io::copy(&mut audio_r.take(step.bytes), &mut out)
838 .context("copying audio chunk into mdat")?;
839 if copied != step.bytes {
840 anyhow::bail!(
841 "audio chunk short read: wanted {}, got {}",
842 step.bytes,
843 copied
844 );
845 }
846 audio_copied += copied;
847 }
848 }
849 }
850 if video_copied != video_payload_bytes {
851 anyhow::bail!(
852 "video mdat payload length mismatch: expected {}, copied {}",
853 video_payload_bytes,
854 video_copied
855 );
856 }
857 if audio_copied != audio_payload_bytes {
858 anyhow::bail!(
859 "audio mdat payload length mismatch: expected {}, copied {}",
860 audio_payload_bytes,
861 audio_copied
862 );
863 }
864 out.flush().context("flushing output")?;
865
866 Ok(())
867 }
868
869 pub fn finalize(self) -> Result<Bytes> {
873 let tmp = NamedTempFile::new().context("creating finalize buffer tempfile")?;
874 let path = tmp.path().to_path_buf();
875 self.finalize_to_file(&path)?;
876 let mut f = File::open(&path).context("reopening finalize buffer tempfile")?;
877 let mut buf = Vec::new();
878 f.read_to_end(&mut buf).context("reading finalize buffer")?;
879 Ok(Bytes::from(buf))
880 }
881}
882
883struct AudioBuildPlan {
886 info: AudioInfo,
887 sample_sizes: Vec<u32>,
888 durations: Vec<u32>,
889 total_duration_in_own_ts: u64,
890 total_duration_in_movie_ts: u64,
891 samples_per_chunk: u32,
892}
893
894#[derive(Debug, Clone, Copy)]
898struct InterleaveStep {
899 track: InterleaveTrack,
900 bytes: u64,
901}
902
903#[derive(Debug, Clone, Copy, PartialEq, Eq)]
904enum InterleaveTrack {
905 Video,
906 Audio,
907}
908
909fn chunk_count_of(sample_count: usize, spc: u32) -> usize {
910 if sample_count == 0 {
911 return 0;
912 }
913 let spc = spc.max(1) as usize;
914 sample_count.div_ceil(spc)
915}
916
917fn chunk_byte_sizes(sample_sizes: &[u32], spc: u32) -> Vec<u64> {
920 let spc = spc.max(1) as usize;
921 let mut out = Vec::new();
922 let mut i = 0usize;
923 while i < sample_sizes.len() {
924 let end = (i + spc).min(sample_sizes.len());
925 let mut total: u64 = 0;
926 for &s in &sample_sizes[i..end] {
927 total += s as u64;
928 }
929 out.push(total);
930 i = end;
931 }
932 out
933}
934
935fn plan_interleaved_layout(
941 first_sample_file_offset: u64,
942 video_sample_sizes: &[u32],
943 video_spc: u32,
944 audio_plan: Option<&AudioBuildPlan>,
945) -> (Vec<u64>, Vec<u64>, Vec<InterleaveStep>) {
946 let video_chunks = chunk_byte_sizes(video_sample_sizes, video_spc);
947 let audio_chunks = match audio_plan {
948 Some(p) => chunk_byte_sizes(&p.sample_sizes, p.samples_per_chunk),
949 None => Vec::new(),
950 };
951
952 let mut video_offsets: Vec<u64> = Vec::with_capacity(video_chunks.len());
953 let mut audio_offsets: Vec<u64> = Vec::with_capacity(audio_chunks.len());
954 let mut plan: Vec<InterleaveStep> = Vec::with_capacity(video_chunks.len() + audio_chunks.len());
955
956 let mut cursor = first_sample_file_offset;
957 let mut vi = 0usize;
958 let mut ai = 0usize;
959 loop {
960 if vi < video_chunks.len() {
961 video_offsets.push(cursor);
962 let size = video_chunks[vi];
963 plan.push(InterleaveStep {
964 track: InterleaveTrack::Video,
965 bytes: size,
966 });
967 cursor = cursor.saturating_add(size);
968 vi += 1;
969 }
970 if ai < audio_chunks.len() {
971 audio_offsets.push(cursor);
972 let size = audio_chunks[ai];
973 plan.push(InterleaveStep {
974 track: InterleaveTrack::Audio,
975 bytes: size,
976 });
977 cursor = cursor.saturating_add(size);
978 ai += 1;
979 }
980 if vi >= video_chunks.len() && ai >= audio_chunks.len() {
981 break;
982 }
983 }
984
985 (video_offsets, audio_offsets, plan)
986}
987
988fn build_ftyp() -> Vec<u8> {
1004 let mut b = BoxBuilder::new(b"ftyp");
1005 b.extend(b"iso6"); b.u32(512); b.extend(b"iso6"); b.extend(b"iso2"); b.extend(b"av01"); b.extend(b"mp41"); b.extend(b"mp42"); b.finish()
1013}
1014
1015#[cfg(test)]
1019fn build_moov(
1020 width: u32,
1021 height: u32,
1022 timescale: u32,
1023 duration: u64,
1024 frame_duration: u32,
1025 sample_sizes: &[u32],
1026 keyframe_indices: &[u32],
1027 config_obus: &[u8],
1028 chunk_offsets: &[u64],
1029 samples_per_chunk: u32,
1030 use_co64: bool,
1031) -> Vec<u8> {
1032 build_moov_any(
1033 width,
1034 height,
1035 timescale,
1036 timescale,
1037 duration,
1038 duration,
1039 frame_duration,
1040 sample_sizes,
1041 keyframe_indices,
1042 config_obus,
1043 chunk_offsets,
1044 samples_per_chunk,
1045 None,
1046 &[],
1047 use_co64,
1048 &ColorMetadata::default(),
1049 )
1050}
1051
1052fn build_moov_any(
1057 width: u32,
1058 height: u32,
1059 video_timescale: u32,
1060 movie_timescale: u32,
1061 movie_duration: u64,
1062 video_duration_in_video_ts: u64,
1063 frame_duration: u32,
1064 sample_sizes: &[u32],
1065 keyframe_indices: &[u32],
1066 config_obus: &[u8],
1067 video_chunk_offsets: &[u64],
1068 video_spc: u32,
1069 audio_plan: Option<&AudioBuildPlan>,
1070 audio_chunk_offsets: &[u64],
1071 use_co64: bool,
1072 color_metadata: &ColorMetadata,
1073) -> Vec<u8> {
1074 let next_track_id: u32 = if audio_plan.is_some() { 3 } else { 2 };
1076 let mvhd = build_mvhd_v2(movie_timescale, movie_duration, next_track_id);
1077 let video_duration_movie: u64 = if video_timescale == movie_timescale {
1079 video_duration_in_video_ts
1080 } else {
1081 ((video_duration_in_video_ts as u128) * movie_timescale as u128
1082 / video_timescale.max(1) as u128) as u64
1083 };
1084 let video_trak = build_video_trak(
1085 width,
1086 height,
1087 video_timescale,
1088 video_duration_movie,
1089 video_duration_in_video_ts,
1090 frame_duration,
1091 sample_sizes,
1092 keyframe_indices,
1093 config_obus,
1094 video_chunk_offsets,
1095 video_spc,
1096 use_co64,
1097 color_metadata,
1098 );
1099
1100 let mut b = BoxBuilder::new(b"moov");
1101 b.extend(&mvhd);
1102 b.extend(&video_trak);
1103 if let Some(plan) = audio_plan {
1104 let audio_trak = build_audio_trak(
1105 plan,
1106 plan.total_duration_in_movie_ts,
1107 audio_chunk_offsets,
1108 use_co64,
1109 );
1110 b.extend(&audio_trak);
1111 }
1112 b.finish()
1113}
1114
1115fn build_mvhd_v2(timescale: u32, duration: u64, next_track_id: u32) -> Vec<u8> {
1119 let mut b = BoxBuilder::new(b"mvhd");
1120 b.u8(0); b.extend(&[0, 0, 0]); b.u32(0); b.u32(0); b.u32(timescale);
1125 b.u32(duration as u32);
1126 b.u32(0x00010000); b.u16(0x0100); b.u16(0); b.u32(0); b.u32(0);
1131 write_unity_matrix(&mut b);
1132 for _ in 0..6 {
1133 b.u32(0);
1134 } b.u32(next_track_id);
1136 b.finish()
1137}
1138
1139fn build_video_trak(
1145 width: u32,
1146 height: u32,
1147 mdhd_timescale: u32,
1148 duration_in_movie_ts: u64,
1149 duration_in_mdhd_ts: u64,
1150 frame_duration: u32,
1151 sample_sizes: &[u32],
1152 keyframe_indices: &[u32],
1153 config_obus: &[u8],
1154 chunk_offsets: &[u64],
1155 samples_per_chunk: u32,
1156 use_co64: bool,
1157 color_metadata: &ColorMetadata,
1158) -> Vec<u8> {
1159 let tkhd = build_video_tkhd(width, height, duration_in_movie_ts);
1160 let mdia = build_video_mdia(
1161 width,
1162 height,
1163 mdhd_timescale,
1164 duration_in_mdhd_ts,
1165 frame_duration,
1166 sample_sizes,
1167 keyframe_indices,
1168 config_obus,
1169 chunk_offsets,
1170 samples_per_chunk,
1171 use_co64,
1172 color_metadata,
1173 );
1174
1175 let mut b = BoxBuilder::new(b"trak");
1176 b.extend(&tkhd);
1177 b.extend(&mdia);
1178 b.finish()
1179}
1180
1181fn build_video_tkhd(width: u32, height: u32, duration: u64) -> Vec<u8> {
1182 let mut b = BoxBuilder::new(b"tkhd");
1183 b.u8(0); b.extend(&[0, 0, 0x03]); b.u32(0); b.u32(0); b.u32(1); b.u32(0); b.u32(duration as u32);
1190 b.u32(0); b.u32(0);
1192 b.u16(0); b.u16(0); b.u16(0); b.u16(0); write_unity_matrix(&mut b);
1197 b.u32(width << 16); b.u32(height << 16);
1199 b.finish()
1200}
1201
1202fn build_video_mdia(
1203 width: u32,
1204 height: u32,
1205 timescale: u32,
1206 duration: u64,
1207 frame_duration: u32,
1208 sample_sizes: &[u32],
1209 keyframe_indices: &[u32],
1210 config_obus: &[u8],
1211 chunk_offsets: &[u64],
1212 samples_per_chunk: u32,
1213 use_co64: bool,
1214 color_metadata: &ColorMetadata,
1215) -> Vec<u8> {
1216 let mdhd = build_mdhd(timescale, duration);
1217 let hdlr = build_video_hdlr();
1218 let minf = build_minf(
1219 width,
1220 height,
1221 frame_duration,
1222 sample_sizes,
1223 keyframe_indices,
1224 config_obus,
1225 chunk_offsets,
1226 samples_per_chunk,
1227 use_co64,
1228 color_metadata,
1229 );
1230
1231 let mut b = BoxBuilder::new(b"mdia");
1232 b.extend(&mdhd);
1233 b.extend(&hdlr);
1234 b.extend(&minf);
1235 b.finish()
1236}
1237
1238fn build_mdhd(timescale: u32, duration: u64) -> Vec<u8> {
1239 let mut b = BoxBuilder::new(b"mdhd");
1240 b.u8(0); b.extend(&[0, 0, 0]); b.u32(0); b.u32(0); b.u32(timescale);
1245 b.u32(duration as u32);
1246 b.u16(0x55c4); b.u16(0); b.finish()
1249}
1250
1251fn build_video_hdlr() -> Vec<u8> {
1252 let mut b = BoxBuilder::new(b"hdlr");
1253 b.u8(0); b.extend(&[0, 0, 0]); b.u32(0); b.extend(b"vide"); b.u32(0); b.u32(0); b.u32(0); b.extend(b"VideoHandler\0");
1261 b.finish()
1262}
1263
1264fn build_audio_trak(
1270 plan: &AudioBuildPlan,
1271 duration_in_movie_ts: u64,
1272 chunk_offsets: &[u64],
1273 use_co64: bool,
1274) -> Vec<u8> {
1275 let tkhd = build_audio_tkhd(duration_in_movie_ts);
1276 let mdia = build_audio_mdia(plan, chunk_offsets, use_co64);
1277
1278 let mut b = BoxBuilder::new(b"trak");
1279 b.extend(&tkhd);
1280 b.extend(&mdia);
1281 b.finish()
1282}
1283
1284fn build_audio_tkhd(duration_in_movie_ts: u64) -> Vec<u8> {
1285 let mut b = BoxBuilder::new(b"tkhd");
1286 b.u8(0); b.extend(&[0, 0, 0x03]); b.u32(0); b.u32(0); b.u32(2); b.u32(0); b.u32(duration_in_movie_ts as u32);
1293 b.u32(0); b.u32(0);
1295 b.u16(0); b.u16(0x0001); b.u16(0x0100); b.u16(0); write_unity_matrix(&mut b);
1300 b.u32(0); b.u32(0); b.finish()
1303}
1304
1305fn build_audio_mdia(plan: &AudioBuildPlan, chunk_offsets: &[u64], use_co64: bool) -> Vec<u8> {
1306 let mdhd = build_mdhd(plan.info.timescale, plan.total_duration_in_own_ts);
1307 let hdlr = build_audio_hdlr();
1308 let minf = build_audio_minf(plan, chunk_offsets, use_co64);
1309
1310 let mut b = BoxBuilder::new(b"mdia");
1311 b.extend(&mdhd);
1312 b.extend(&hdlr);
1313 b.extend(&minf);
1314 b.finish()
1315}
1316
1317fn build_audio_hdlr() -> Vec<u8> {
1318 let mut b = BoxBuilder::new(b"hdlr");
1319 b.u8(0);
1320 b.extend(&[0, 0, 0]);
1321 b.u32(0); b.extend(b"soun"); b.u32(0);
1324 b.u32(0);
1325 b.u32(0);
1326 b.extend(b"SoundHandler\0");
1327 b.finish()
1328}
1329
1330fn build_audio_minf(plan: &AudioBuildPlan, chunk_offsets: &[u64], use_co64: bool) -> Vec<u8> {
1331 let smhd = build_smhd();
1332 let dinf = build_dinf();
1333 let stbl = build_audio_stbl(plan, chunk_offsets, use_co64);
1334
1335 let mut b = BoxBuilder::new(b"minf");
1336 b.extend(&smhd);
1337 b.extend(&dinf);
1338 b.extend(&stbl);
1339 b.finish()
1340}
1341
1342fn build_smhd() -> Vec<u8> {
1343 let mut b = BoxBuilder::new(b"smhd");
1344 b.u8(0);
1345 b.extend(&[0, 0, 0]); b.u16(0); b.u16(0); b.finish()
1349}
1350
1351fn build_audio_stbl(plan: &AudioBuildPlan, chunk_offsets: &[u64], use_co64: bool) -> Vec<u8> {
1352 let stsd = build_audio_stsd(&plan.info);
1353 let stts = build_audio_stts(&plan.durations);
1354 let stsc = build_stsc(plan.sample_sizes.len() as u32, plan.samples_per_chunk);
1355 let stsz = build_stsz(&plan.sample_sizes);
1356 let chunk_offset_box = if use_co64 {
1357 build_co64(chunk_offsets)
1358 } else {
1359 build_stco(chunk_offsets)
1360 };
1361
1362 let mut b = BoxBuilder::new(b"stbl");
1363 b.extend(&stsd);
1364 b.extend(&stts);
1365 b.extend(&stsc);
1366 b.extend(&stsz);
1367 b.extend(&chunk_offset_box);
1368 b.finish()
1369}
1370
1371pub(crate) fn build_audio_stsd(info: &AudioInfo) -> Vec<u8> {
1372 let kind = AudioCodecKind::from_codec_tag(&info.codec)
1378 .expect("with_audio gate already validated codec tag");
1379 let entry = match kind {
1380 AudioCodecKind::Aac => build_mp4a(info),
1381 AudioCodecKind::Opus => build_opus_sample_entry(info),
1382 AudioCodecKind::Ac3 => build_ac3_sample_entry(info),
1383 AudioCodecKind::Eac3 => build_ec3_sample_entry(info),
1384 };
1385 let mut b = BoxBuilder::new(b"stsd");
1386 b.u8(0);
1387 b.extend(&[0, 0, 0]);
1388 b.u32(1); b.extend(&entry);
1390 b.finish()
1391}
1392
1393fn build_mp4a(info: &AudioInfo) -> Vec<u8> {
1404 let mut b = BoxBuilder::new(b"mp4a");
1405 for _ in 0..6 {
1407 b.u8(0);
1408 } b.u16(1); b.u32(0); b.u32(0); b.u16(info.channels); b.u16(16); b.u16(0); b.u16(0); b.u32(info.sample_rate << 16); b.extend(&build_esds(info));
1420 if let Some(chan) = build_chan_box(info.channels) {
1424 b.extend(&chan);
1425 }
1426 b.finish()
1427}
1428
1429pub(crate) fn build_chan_box(channels: u16) -> Option<Vec<u8>> {
1459 let tag: u32 = match channels {
1460 1 | 2 => return None, 6 => (114u32 << 16) | 6, 7 => (127u32 << 16) | 8, _ => return None, };
1465 let mut b = BoxBuilder::new(b"chan");
1466 b.u32(tag); b.u32(0); b.u32(0); Some(b.finish())
1470}
1471
1472fn build_opus_sample_entry(info: &AudioInfo) -> Vec<u8> {
1490 let mut b = BoxBuilder::new(b"Opus");
1492 for _ in 0..6 {
1494 b.u8(0);
1495 } b.u16(1); b.u32(0); b.u32(0); b.u16(info.channels); b.u16(16); b.u16(0); b.u16(0); b.u32(48_000u32 << 16); b.extend(&build_dops(info));
1509 b.finish()
1510}
1511
1512fn build_dops(info: &AudioInfo) -> Vec<u8> {
1536 let p = &info.codec_private;
1537 debug_assert!(
1538 p.len() >= 11,
1539 "with_audio gate must enforce dOps minimum size"
1540 );
1541
1542 let output_channels = p[1];
1556 let pre_skip = u16::from_le_bytes([p[2], p[3]]);
1557 let input_sample_rate = u32::from_le_bytes([p[4], p[5], p[6], p[7]]);
1558 let output_gain = i16::from_le_bytes([p[8], p[9]]);
1559 let channel_mapping_family = p[10];
1560
1561 let mut b = BoxBuilder::new(b"dOps");
1562 b.u8(0); b.u8(output_channels); b.u16(pre_skip); b.u32(input_sample_rate); b.u16(output_gain as u16); b.u8(channel_mapping_family); if channel_mapping_family != 0 {
1578 let trailer_len = 2 + output_channels as usize;
1582 debug_assert!(
1583 p.len() >= 11 + trailer_len,
1584 "family={channel_mapping_family} requires {trailer_len} more bytes after the 11-byte preamble; codec_private has {}",
1585 p.len()
1586 );
1587 b.u8(p[11]); b.u8(p[12]); for i in 0..output_channels as usize {
1590 b.u8(p[13 + i]); }
1592 }
1593
1594 b.finish()
1595}
1596
1597fn build_ac3_sample_entry(info: &AudioInfo) -> Vec<u8> {
1635 let mut b = BoxBuilder::new(b"ac-3");
1636 for _ in 0..6 {
1638 b.u8(0);
1639 } b.u16(1); b.u32(0); b.u32(0); b.u16(info.channels); b.u16(16); b.u16(0); b.u16(0); b.u32(info.sample_rate << 16); b.extend(&build_dac3(info)); b.finish()
1651}
1652
1653fn build_ec3_sample_entry(info: &AudioInfo) -> Vec<u8> {
1656 let mut b = BoxBuilder::new(b"ec-3");
1657 for _ in 0..6 {
1658 b.u8(0);
1659 }
1660 b.u16(1);
1661 b.u32(0);
1662 b.u32(0);
1663 b.u16(info.channels);
1664 b.u16(16);
1665 b.u16(0);
1666 b.u16(0);
1667 b.u32(info.sample_rate << 16);
1668 b.extend(&build_dec3(info));
1669 b.finish()
1670}
1671
1672fn build_dac3(info: &AudioInfo) -> Vec<u8> {
1690 debug_assert_eq!(
1691 info.codec_private.len(),
1692 3,
1693 "with_audio gate must enforce dac3 body == 3 bytes"
1694 );
1695 let mut b = BoxBuilder::new(b"dac3");
1696 b.extend(&info.codec_private);
1697 b.finish()
1698}
1699
1700fn build_dec3(info: &AudioInfo) -> Vec<u8> {
1725 debug_assert!(
1726 info.codec_private.len() >= 5,
1727 "with_audio gate must enforce dec3 body >= 5 bytes"
1728 );
1729 let mut b = BoxBuilder::new(b"dec3");
1730 b.extend(&info.codec_private);
1731 b.finish()
1732}
1733
1734pub fn dac3_body_from_sync(s: &crate::ac3_sync::Ac3SyncInfo) -> [u8; 3] {
1740 let mut bw = MsbBitWriter::new();
1741 bw.put(2, s.fscod as u32);
1742 bw.put(5, s.bsid as u32);
1743 bw.put(3, s.bsmod as u32);
1744 bw.put(3, s.acmod as u32);
1745 bw.put(1, if s.lfeon { 1 } else { 0 });
1746 bw.put(5, s.bit_rate_code as u32);
1747 bw.put(5, 0); let bytes = bw.finish();
1749 [bytes[0], bytes[1], bytes[2]]
1751}
1752
1753pub fn dec3_body_from_sync(s: &crate::ac3_sync::Eac3SyncInfo, data_rate_div2_kbps: u16) -> [u8; 5] {
1762 let mut bw = MsbBitWriter::new();
1763 bw.put(13, (data_rate_div2_kbps & 0x1FFF) as u32);
1766 bw.put(3, 0); bw.put(2, s.fscod as u32);
1769 bw.put(5, 16); bw.put(1, 0); bw.put(1, 0); bw.put(3, s.bsmod as u32);
1773 bw.put(3, s.acmod as u32);
1774 bw.put(1, if s.lfeon { 1 } else { 0 });
1775 bw.put(3, 0); bw.put(4, 0); let bytes = bw.finish();
1778 debug_assert_eq!(bytes.len(), 5, "dec3 single-substream body must be 5 bytes");
1779 [bytes[0], bytes[1], bytes[2], bytes[3], bytes[4]]
1780}
1781
1782struct MsbBitWriter {
1786 bytes: Vec<u8>,
1787 bit_pos: usize,
1788}
1789
1790impl MsbBitWriter {
1791 fn new() -> Self {
1792 Self {
1793 bytes: Vec::new(),
1794 bit_pos: 0,
1795 }
1796 }
1797 fn put(&mut self, n: usize, v: u32) {
1798 debug_assert!(n <= 24);
1799 for i in (0..n).rev() {
1800 let bit = ((v >> i) & 0x01) as u8;
1801 if self.bit_pos.is_multiple_of(8) {
1802 self.bytes.push(0);
1803 }
1804 let byte_idx = self.bit_pos / 8;
1805 let bit_idx = 7 - (self.bit_pos % 8);
1806 self.bytes[byte_idx] |= bit << bit_idx;
1807 self.bit_pos += 1;
1808 }
1809 }
1810 fn finish(self) -> Vec<u8> {
1811 self.bytes
1812 }
1813}
1814
1815fn build_esds(info: &AudioInfo) -> Vec<u8> {
1820 let asc_len = info.asc_bytes.len() as u32;
1822 let mut dsi = Vec::new();
1823 dsi.push(0x05u8);
1824 write_descriptor_length(&mut dsi, asc_len);
1825 dsi.extend_from_slice(&info.asc_bytes);
1826
1827 let mut dcd_payload = Vec::new();
1835 dcd_payload.push(0x40); dcd_payload.push((0x05 << 2) | 0x01); dcd_payload.extend_from_slice(&[0, 0, 0]); dcd_payload.extend_from_slice(&0u32.to_be_bytes()); dcd_payload.extend_from_slice(&0u32.to_be_bytes()); dcd_payload.extend_from_slice(&dsi);
1841 let mut dcd = Vec::new();
1842 dcd.push(0x04);
1843 write_descriptor_length(&mut dcd, dcd_payload.len() as u32);
1844 dcd.extend_from_slice(&dcd_payload);
1845
1846 let mut slc = Vec::new();
1848 slc.push(0x06);
1849 write_descriptor_length(&mut slc, 1);
1850 slc.push(0x02);
1851
1852 let mut es_payload = Vec::new();
1854 es_payload.extend_from_slice(&0u16.to_be_bytes()); es_payload.push(0); es_payload.extend_from_slice(&dcd);
1857 es_payload.extend_from_slice(&slc);
1858 let mut es = Vec::new();
1859 es.push(0x03);
1860 write_descriptor_length(&mut es, es_payload.len() as u32);
1861 es.extend_from_slice(&es_payload);
1862
1863 let mut b = BoxBuilder::new(b"esds");
1865 b.u8(0);
1866 b.extend(&[0, 0, 0]);
1867 b.extend(&es);
1868 b.finish()
1869}
1870
1871fn write_descriptor_length(buf: &mut Vec<u8>, len: u32) {
1881 if len < 128 {
1882 buf.push(len as u8);
1883 return;
1884 }
1885 buf.push(((len >> 21) & 0x7F) as u8 | 0x80);
1886 buf.push(((len >> 14) & 0x7F) as u8 | 0x80);
1887 buf.push(((len >> 7) & 0x7F) as u8 | 0x80);
1888 buf.push((len & 0x7F) as u8);
1889}
1890
1891fn build_audio_stts(durations: &[u32]) -> Vec<u8> {
1896 let mut b = BoxBuilder::new(b"stts");
1897 b.u8(0);
1898 b.extend(&[0, 0, 0]);
1899 let mut runs: Vec<(u32, u32)> = Vec::new();
1901 for &d in durations {
1902 if let Some(last) = runs.last_mut()
1903 && last.1 == d
1904 {
1905 last.0 += 1;
1906 continue;
1907 }
1908 runs.push((1, d));
1909 }
1910 b.u32(runs.len() as u32);
1911 for (count, delta) in runs {
1912 b.u32(count);
1913 b.u32(delta);
1914 }
1915 b.finish()
1916}
1917
1918fn build_minf(
1919 width: u32,
1920 height: u32,
1921 frame_duration: u32,
1922 sample_sizes: &[u32],
1923 keyframe_indices: &[u32],
1924 config_obus: &[u8],
1925 chunk_offsets: &[u64],
1926 samples_per_chunk: u32,
1927 use_co64: bool,
1928 color_metadata: &ColorMetadata,
1929) -> Vec<u8> {
1930 let vmhd = build_vmhd();
1931 let dinf = build_dinf();
1932 let stbl = build_stbl(
1933 width,
1934 height,
1935 frame_duration,
1936 sample_sizes,
1937 keyframe_indices,
1938 config_obus,
1939 chunk_offsets,
1940 samples_per_chunk,
1941 use_co64,
1942 color_metadata,
1943 );
1944
1945 let mut b = BoxBuilder::new(b"minf");
1946 b.extend(&vmhd);
1947 b.extend(&dinf);
1948 b.extend(&stbl);
1949 b.finish()
1950}
1951
1952fn build_vmhd() -> Vec<u8> {
1953 let mut b = BoxBuilder::new(b"vmhd");
1954 b.u8(0);
1955 b.extend(&[0, 0, 0x01]); b.u16(0); b.u16(0);
1958 b.u16(0);
1959 b.u16(0); b.finish()
1961}
1962
1963fn build_dinf() -> Vec<u8> {
1964 let mut dref = BoxBuilder::new(b"dref");
1965 dref.u8(0);
1966 dref.extend(&[0, 0, 0]);
1967 dref.u32(1); let mut url = BoxBuilder::new(b"url ");
1969 url.u8(0);
1970 url.extend(&[0, 0, 0x01]); dref.extend(&url.finish());
1972
1973 let mut b = BoxBuilder::new(b"dinf");
1974 b.extend(&dref.finish());
1975 b.finish()
1976}
1977
1978fn build_stbl(
1979 width: u32,
1980 height: u32,
1981 frame_duration: u32,
1982 sample_sizes: &[u32],
1983 keyframe_indices: &[u32],
1984 config_obus: &[u8],
1985 chunk_offsets: &[u64],
1986 samples_per_chunk: u32,
1987 use_co64: bool,
1988 color_metadata: &ColorMetadata,
1989) -> Vec<u8> {
1990 let stsd = build_stsd(width, height, config_obus, color_metadata);
1991 let stts = build_stts(sample_sizes.len() as u32, frame_duration);
1992 let stsc = build_stsc(sample_sizes.len() as u32, samples_per_chunk);
1993 let stsz = build_stsz(sample_sizes);
1994 let chunk_offset_box = if use_co64 {
1995 build_co64(chunk_offsets)
1996 } else {
1997 build_stco(chunk_offsets)
1998 };
1999 let stss_box = if !keyframe_indices.is_empty() && keyframe_indices.len() < sample_sizes.len() {
2000 Some(build_stss(keyframe_indices))
2001 } else {
2002 None
2003 };
2004
2005 let mut b = BoxBuilder::new(b"stbl");
2006 b.extend(&stsd);
2007 b.extend(&stts);
2008 if let Some(ss) = &stss_box {
2009 b.extend(ss);
2010 }
2011 b.extend(&stsc);
2012 b.extend(&stsz);
2013 b.extend(&chunk_offset_box);
2014 b.finish()
2015}
2016
2017fn build_stsd(
2018 width: u32,
2019 height: u32,
2020 config_obus: &[u8],
2021 color_metadata: &ColorMetadata,
2022) -> Vec<u8> {
2023 let av01 = build_av01(width, height, config_obus, color_metadata);
2024 let mut b = BoxBuilder::new(b"stsd");
2025 b.u8(0);
2026 b.extend(&[0, 0, 0]); b.u32(1); b.extend(&av01);
2029 b.finish()
2030}
2031
2032pub(crate) fn build_av01(
2051 width: u32,
2052 height: u32,
2053 config_obus: &[u8],
2054 color_metadata: &ColorMetadata,
2055) -> Vec<u8> {
2056 let av1c = build_av1c(config_obus);
2057 let colr = build_colr_nclx(color_metadata);
2058 let mdcv = color_metadata.mastering_display.as_ref().map(build_mdcv);
2059 let clli = color_metadata.content_light_level.as_ref().map(build_clli);
2060 let mut b = BoxBuilder::new(b"av01");
2061 for _ in 0..6 {
2063 b.u8(0);
2064 } b.u16(1); b.u16(0); b.u16(0); for _ in 0..3 {
2069 b.u32(0);
2070 } b.u16(width as u16);
2072 b.u16(height as u16);
2073 b.u32(0x00480000); b.u32(0x00480000); b.u32(0); b.u16(1); b.u8(0);
2079 for _ in 0..31 {
2080 b.u8(0);
2081 }
2082 b.u16(0x0018); b.u16(0xFFFF); b.extend(&av1c);
2085 b.extend(&colr);
2086 if let Some(mdcv) = &mdcv {
2087 b.extend(mdcv);
2088 }
2089 if let Some(clli) = &clli {
2090 b.extend(clli);
2091 }
2092 b.finish()
2093}
2094
2095fn transfer_to_h273(transfer: codec::frame::TransferFn) -> u8 {
2101 use codec::frame::TransferFn;
2102 match transfer {
2103 TransferFn::Bt709 => 1,
2104 TransferFn::Bt470Bg => 4,
2105 TransferFn::Linear => 8,
2106 TransferFn::St2084 => 16,
2107 TransferFn::AribStdB67 => 18,
2108 TransferFn::Unspecified => 2,
2114 }
2115}
2116
2117fn build_colr_nclx(color_metadata: &ColorMetadata) -> Vec<u8> {
2128 let mut b = BoxBuilder::new(b"colr");
2129 b.extend(b"nclx");
2130 b.u16(color_metadata.colour_primaries as u16);
2131 b.u16(transfer_to_h273(color_metadata.transfer) as u16);
2132 b.u16(color_metadata.matrix_coefficients as u16);
2133 let full_range_byte: u8 = if color_metadata.full_range {
2136 0x80
2137 } else {
2138 0x00
2139 };
2140 b.u8(full_range_byte);
2141 b.finish()
2142}
2143
2144fn build_mdcv(md: &codec::frame::MasteringDisplay) -> Vec<u8> {
2171 let mut b = BoxBuilder::new(b"mdcv");
2172 b.u16(md.primaries_r_x);
2173 b.u16(md.primaries_r_y);
2174 b.u16(md.primaries_g_x);
2175 b.u16(md.primaries_g_y);
2176 b.u16(md.primaries_b_x);
2177 b.u16(md.primaries_b_y);
2178 b.u16(md.white_point_x);
2179 b.u16(md.white_point_y);
2180 b.u32(md.max_luminance);
2181 b.u32(md.min_luminance);
2182 b.finish()
2183}
2184
2185fn build_clli(cll: &codec::frame::ContentLightLevel) -> Vec<u8> {
2200 let mut b = BoxBuilder::new(b"clli");
2201 b.u16(cll.max_cll);
2202 b.u16(cll.max_fall);
2203 b.finish()
2204}
2205
2206fn build_av1c(config_obus: &[u8]) -> Vec<u8> {
2207 let mut b = BoxBuilder::new(b"av1C");
2208 b.u8(0x81);
2210 let (
2212 seq_profile,
2213 seq_level_idx_0,
2214 seq_tier_0,
2215 high_bitdepth,
2216 twelve_bit,
2217 monochrome,
2218 chroma_sub_x,
2219 chroma_sub_y,
2220 chroma_sample_position,
2221 ) = parse_seq_header_params(config_obus);
2222 b.u8(((seq_profile & 0x7) << 5) | (seq_level_idx_0 & 0x1F));
2223 let byte3 = ((seq_tier_0 & 0x1) << 7)
2224 | ((high_bitdepth as u8 & 0x1) << 6)
2225 | ((twelve_bit as u8 & 0x1) << 5)
2226 | ((monochrome as u8 & 0x1) << 4)
2227 | ((chroma_sub_x & 0x1) << 3)
2228 | ((chroma_sub_y & 0x1) << 2)
2229 | (chroma_sample_position & 0x3);
2230 b.u8(byte3);
2231 b.u8(0);
2233 b.extend(config_obus);
2235 b.finish()
2236}
2237
2238fn build_stts(sample_count: u32, frame_duration: u32) -> Vec<u8> {
2239 let mut b = BoxBuilder::new(b"stts");
2240 b.u8(0);
2241 b.extend(&[0, 0, 0]);
2242 b.u32(1); b.u32(sample_count);
2244 b.u32(frame_duration);
2245 b.finish()
2246}
2247
2248fn build_stss(keyframes: &[u32]) -> Vec<u8> {
2249 let mut b = BoxBuilder::new(b"stss");
2250 b.u8(0);
2251 b.extend(&[0, 0, 0]);
2252 b.u32(keyframes.len() as u32);
2253 for &k in keyframes {
2254 b.u32(k);
2255 }
2256 b.finish()
2257}
2258
2259fn build_stsc(sample_count: u32, samples_per_chunk: u32) -> Vec<u8> {
2265 let mut b = BoxBuilder::new(b"stsc");
2266 b.u8(0);
2267 b.extend(&[0, 0, 0]);
2268
2269 let spc = samples_per_chunk.max(1);
2270 if sample_count == 0 {
2273 b.u32(0);
2274 return b.finish();
2275 }
2276
2277 let full_chunks = sample_count / spc;
2278 let remainder = sample_count % spc;
2279
2280 if remainder == 0 {
2281 b.u32(1);
2283 b.u32(1); b.u32(spc); b.u32(1); } else if full_chunks == 0 {
2287 b.u32(1);
2289 b.u32(1);
2290 b.u32(remainder);
2291 b.u32(1);
2292 } else {
2293 b.u32(2);
2296 b.u32(1);
2297 b.u32(spc);
2298 b.u32(1);
2299 b.u32(full_chunks + 1); b.u32(remainder);
2301 b.u32(1);
2302 }
2303 b.finish()
2304}
2305
2306fn build_stsz(sample_sizes: &[u32]) -> Vec<u8> {
2307 let mut b = BoxBuilder::new(b"stsz");
2308 b.u8(0);
2309 b.extend(&[0, 0, 0]);
2310 b.u32(0); b.u32(sample_sizes.len() as u32); for &s in sample_sizes {
2313 b.u32(s);
2314 }
2315 b.finish()
2316}
2317
2318fn build_stco(chunk_offsets: &[u64]) -> Vec<u8> {
2323 let mut b = BoxBuilder::new(b"stco");
2324 b.u8(0);
2325 b.extend(&[0, 0, 0]);
2326 b.u32(chunk_offsets.len() as u32);
2327 for &off in chunk_offsets {
2328 debug_assert!(
2329 off <= u32::MAX as u64,
2330 "stco offset exceeds u32; should be co64"
2331 );
2332 b.u32(off as u32);
2333 }
2334 b.finish()
2335}
2336
2337fn build_co64(chunk_offsets: &[u64]) -> Vec<u8> {
2341 let mut b = BoxBuilder::new(b"co64");
2342 b.u8(0);
2343 b.extend(&[0, 0, 0]);
2344 b.u32(chunk_offsets.len() as u32);
2345 for &off in chunk_offsets {
2346 b.u64(off);
2347 }
2348 b.finish()
2349}
2350
2351#[cfg(test)]
2359fn compute_chunk_offsets(
2360 first_sample_file_offset: u64,
2361 sample_sizes: &[u32],
2362 samples_per_chunk: u32,
2363) -> Vec<u64> {
2364 let spc = samples_per_chunk.max(1) as usize;
2365 let total = sample_sizes.len();
2366 if total == 0 {
2367 return Vec::new();
2368 }
2369 let chunk_count = (total + spc - 1) / spc;
2370 let mut offsets = Vec::with_capacity(chunk_count);
2371 let mut cursor = first_sample_file_offset;
2372 let mut sample_idx = 0usize;
2373 for _ in 0..chunk_count {
2374 offsets.push(cursor);
2375 let end = (sample_idx + spc).min(total);
2376 for &size in &sample_sizes[sample_idx..end] {
2377 cursor = cursor.saturating_add(size as u64);
2378 }
2379 sample_idx = end;
2380 }
2381 offsets
2382}
2383
2384pub(crate) fn write_unity_matrix(b: &mut BoxBuilder) {
2385 b.u32(0x00010000);
2386 b.u32(0);
2387 b.u32(0);
2388 b.u32(0);
2389 b.u32(0x00010000);
2390 b.u32(0);
2391 b.u32(0);
2392 b.u32(0);
2393 b.u32(0x40000000);
2394}
2395
2396pub(crate) struct BoxBuilder {
2397 buf: Vec<u8>,
2398}
2399
2400impl BoxBuilder {
2401 pub(crate) fn new(box_type: &[u8; 4]) -> Self {
2402 let mut buf = Vec::with_capacity(64);
2403 buf.extend_from_slice(&[0, 0, 0, 0]); buf.extend_from_slice(box_type);
2405 Self { buf }
2406 }
2407
2408 pub(crate) fn u8(&mut self, v: u8) {
2409 self.buf.push(v);
2410 }
2411 pub(crate) fn u16(&mut self, v: u16) {
2412 self.buf.extend_from_slice(&v.to_be_bytes());
2413 }
2414 pub(crate) fn u32(&mut self, v: u32) {
2415 self.buf.extend_from_slice(&v.to_be_bytes());
2416 }
2417 pub(crate) fn u64(&mut self, v: u64) {
2418 self.buf.extend_from_slice(&v.to_be_bytes());
2419 }
2420 pub(crate) fn extend(&mut self, v: &[u8]) {
2421 self.buf.extend_from_slice(v);
2422 }
2423
2424 pub(crate) fn current_len(&self) -> usize {
2428 self.buf.len()
2429 }
2430
2431 pub(crate) fn finish(mut self) -> Vec<u8> {
2432 let size = self.buf.len() as u32;
2433 self.buf[0..4].copy_from_slice(&size.to_be_bytes());
2434 self.buf
2435 }
2436}
2437
2438pub(crate) fn extract_sequence_header(data: &[u8]) -> Result<Vec<u8>> {
2446 let mut pos = 0;
2447 while pos < data.len() {
2448 let header_byte = data[pos];
2449 pos += 1;
2450 let obu_type = (header_byte >> 3) & 0x0F;
2451 let extension_flag = (header_byte >> 2) & 0x1;
2452 let has_size = (header_byte >> 1) & 0x1;
2453 if has_size == 0 {
2454 anyhow::bail!(
2455 "AV1 packet uses Annex-B style OBUs (obu_has_size_field=0); \
2456 expected LOB format from the encoder"
2457 );
2458 }
2459 if extension_flag != 0 {
2460 if pos >= data.len() {
2461 anyhow::bail!("truncated OBU extension header");
2462 }
2463 pos += 1;
2464 }
2465 let (size64, size_len) = read_leb128(&data[pos..])?;
2466 let size = size64 as usize;
2467 pos += size_len;
2468 if pos + size > data.len() {
2469 anyhow::bail!("OBU payload extends past packet");
2470 }
2471 if obu_type == 1 {
2472 let header: u8 = (1 << 3) | (1 << 1);
2474 let mut out = Vec::with_capacity(1 + 8 + size);
2475 out.push(header);
2476 write_leb128(&mut out, size as u64);
2477 out.extend_from_slice(&data[pos..pos + size]);
2478 return Ok(out);
2479 }
2480 pos += size;
2481 }
2482 anyhow::bail!("no OBU_SEQUENCE_HEADER found in first packet")
2483}
2484
2485fn read_leb128(data: &[u8]) -> Result<(u64, usize)> {
2486 let mut value: u64 = 0;
2487 let mut len = 0usize;
2488 for i in 0..8 {
2489 if i >= data.len() {
2490 anyhow::bail!("truncated leb128");
2491 }
2492 let byte = data[i];
2493 value |= ((byte & 0x7F) as u64) << (i * 7);
2494 len += 1;
2495 if (byte & 0x80) == 0 {
2496 return Ok((value, len));
2497 }
2498 }
2499 anyhow::bail!("leb128 too long")
2500}
2501
2502fn write_leb128(out: &mut Vec<u8>, mut value: u64) {
2503 loop {
2504 let mut byte = (value & 0x7F) as u8;
2505 value >>= 7;
2506 if value != 0 {
2507 byte |= 0x80;
2508 out.push(byte);
2509 } else {
2510 out.push(byte);
2511 return;
2512 }
2513 }
2514}
2515
2516fn parse_seq_header_params(obu: &[u8]) -> (u8, u8, u8, bool, bool, bool, u8, u8, u8) {
2525 if obu.len() < 2 {
2526 return (0, 0, 0, false, false, false, 1, 1, 0);
2527 }
2528 let mut pos = 1;
2530 if obu[0] & 0x02 != 0 {
2531 match read_leb128(&obu[pos..]) {
2533 Ok((_, len)) => pos += len,
2534 Err(_) => return (0, 0, 0, false, false, false, 1, 1, 0),
2535 }
2536 }
2537 if pos >= obu.len() {
2538 return (0, 0, 0, false, false, false, 1, 1, 0);
2539 }
2540
2541 let mut br = BitReader::new(&obu[pos..]);
2542 let seq_profile = br.bits(3).unwrap_or(0) as u8;
2543 let _still_picture = br.bits(1).unwrap_or(0);
2544 let reduced_still_picture_header = br.bits(1).unwrap_or(0);
2545
2546 let (seq_level_idx_0, seq_tier_0) = if reduced_still_picture_header != 0 {
2547 (br.bits(5).unwrap_or(0) as u8, 0)
2548 } else {
2549 let timing_info_present = br.bits(1).unwrap_or(0);
2550 if timing_info_present != 0 {
2551 let _num_units = br.bits(32);
2552 let _time_scale = br.bits(32);
2553 let equal_pts = br.bits(1).unwrap_or(0);
2554 if equal_pts != 0 {
2555 let _nticks = read_uvlc(&mut br);
2556 }
2557 let decoder_model_info_present = br.bits(1).unwrap_or(0);
2558 if decoder_model_info_present != 0 {
2559 let _bdlm1 = br.bits(5);
2560 let _nts = br.bits(32);
2561 let _brslm1 = br.bits(5);
2562 let _frpdlm1 = br.bits(5);
2563 }
2564 }
2565 let initial_display_delay_present = br.bits(1).unwrap_or(0);
2566 let operating_points_cnt_minus_1 = br.bits(5).unwrap_or(0);
2567 let mut level0 = 0u8;
2568 let mut tier0 = 0u8;
2569 for i in 0..=operating_points_cnt_minus_1 {
2570 let _operating_point_idc = br.bits(12).unwrap_or(0);
2571 let seq_level_idx_i = br.bits(5).unwrap_or(0) as u8;
2572 let seq_tier_i = if seq_level_idx_i > 7 {
2573 br.bits(1).unwrap_or(0) as u8
2574 } else {
2575 0
2576 };
2577 if i == 0 {
2578 level0 = seq_level_idx_i;
2579 tier0 = seq_tier_i;
2580 }
2581 if initial_display_delay_present != 0 {
2584 let present = br.bits(1).unwrap_or(0);
2585 if present != 0 {
2586 let _iddm1 = br.bits(4);
2587 }
2588 }
2589 }
2590 (level0, tier0)
2591 };
2592
2593 let frame_width_bits_minus_1 = br.bits(4).unwrap_or(0);
2594 let frame_height_bits_minus_1 = br.bits(4).unwrap_or(0);
2595 let _max_frame_width_minus_1 = br.bits(frame_width_bits_minus_1 + 1);
2596 let _max_frame_height_minus_1 = br.bits(frame_height_bits_minus_1 + 1);
2597
2598 if reduced_still_picture_header == 0 {
2599 let frame_id_numbers_present = br.bits(1).unwrap_or(0);
2600 if frame_id_numbers_present != 0 {
2601 let _delta_fid_len = br.bits(4);
2602 let _add_fid_len = br.bits(3);
2603 }
2604 }
2605 let _use_128x128 = br.bits(1);
2606 let _enable_filter_intra = br.bits(1);
2607 let _enable_intra_edge_filter = br.bits(1);
2608 if reduced_still_picture_header == 0 {
2609 let _enable_interintra = br.bits(1);
2610 let _enable_masked = br.bits(1);
2611 let _enable_warped = br.bits(1);
2612 let _enable_dual_filter = br.bits(1);
2613 let _enable_order_hint = br.bits(1);
2614 let enable_order_hint = _enable_order_hint.unwrap_or(0);
2615 if enable_order_hint != 0 {
2616 let _enable_jnt_comp = br.bits(1);
2617 let _enable_ref_frame_mvs = br.bits(1);
2618 }
2619 let seq_choose_screen_detection_tools = br.bits(1).unwrap_or(0);
2620 let seq_force_screen_content_tools = if seq_choose_screen_detection_tools != 0 {
2621 2
2622 } else {
2623 br.bits(1).unwrap_or(0)
2624 };
2625 if seq_force_screen_content_tools > 0 {
2626 let seq_choose_integer_mv = br.bits(1).unwrap_or(0);
2627 if seq_choose_integer_mv == 0 {
2628 let _seq_force_integer_mv = br.bits(1);
2629 }
2630 }
2631 if enable_order_hint != 0 {
2632 let _order_hint_bits_minus_1 = br.bits(3);
2633 }
2634 }
2635 let _enable_superres = br.bits(1);
2636 let _enable_cdef = br.bits(1);
2637 let _enable_restoration = br.bits(1);
2638
2639 let high_bitdepth = br.bits(1).unwrap_or(0) != 0;
2641 let twelve_bit = if seq_profile == 2 && high_bitdepth {
2642 br.bits(1).unwrap_or(0) != 0
2643 } else {
2644 false
2645 };
2646 let monochrome = if seq_profile == 1 {
2647 false
2648 } else {
2649 br.bits(1).unwrap_or(0) != 0
2650 };
2651 let color_description_present = br.bits(1).unwrap_or(0) != 0;
2652 let (color_primaries, transfer_characteristics, matrix_coefficients) =
2653 if color_description_present {
2654 let cp = br.bits(8).unwrap_or(2) as u8;
2655 let tc = br.bits(8).unwrap_or(2) as u8;
2656 let mc = br.bits(8).unwrap_or(2) as u8;
2657 (cp, tc, mc)
2658 } else {
2659 (2u8, 2u8, 2u8) };
2661 let (subsampling_x, subsampling_y, chroma_sample_position) = if monochrome {
2662 let _color_range = br.bits(1);
2664 (1u8, 1u8, 0u8)
2665 } else if color_primaries == 1 && transfer_characteristics == 13 && matrix_coefficients == 0
2668 {
2670 (0u8, 0u8, 0u8)
2672 } else {
2673 let _color_range = br.bits(1);
2674 let (sx, sy) = if seq_profile == 0 {
2675 (1u8, 1u8)
2676 } else if seq_profile == 1 {
2677 (0u8, 0u8)
2678 } else {
2679 let bit_depth = if high_bitdepth {
2680 if twelve_bit { 12 } else { 10 }
2681 } else {
2682 8
2683 };
2684 if bit_depth == 12 {
2685 let sxb = br.bits(1).unwrap_or(1) as u8;
2686 let syb = if sxb != 0 {
2687 br.bits(1).unwrap_or(1) as u8
2688 } else {
2689 0
2690 };
2691 (sxb, syb)
2692 } else {
2693 (1u8, 0u8)
2694 }
2695 };
2696 let csp = if sx != 0 && sy != 0 {
2697 br.bits(2).unwrap_or(0) as u8
2698 } else {
2699 0u8
2700 };
2701 (sx, sy, csp)
2702 };
2703 (
2706 seq_profile,
2707 seq_level_idx_0,
2708 seq_tier_0,
2709 high_bitdepth,
2710 twelve_bit,
2711 monochrome,
2712 subsampling_x,
2713 subsampling_y,
2714 chroma_sample_position,
2715 )
2716}
2717
2718struct BitReader<'a> {
2719 data: &'a [u8],
2720 pos: usize,
2721}
2722
2723impl<'a> BitReader<'a> {
2724 fn new(data: &'a [u8]) -> Self {
2725 Self { data, pos: 0 }
2726 }
2727
2728 fn bits(&mut self, n: u32) -> Option<u32> {
2729 let mut v: u32 = 0;
2730 for _ in 0..n {
2731 if self.pos / 8 >= self.data.len() {
2732 return None;
2733 }
2734 let byte = self.data[self.pos / 8];
2735 let bit = (byte >> (7 - (self.pos % 8))) & 1;
2736 v = (v << 1) | bit as u32;
2737 self.pos += 1;
2738 }
2739 Some(v)
2740 }
2741}
2742
2743fn read_uvlc(br: &mut BitReader) -> u32 {
2744 let mut leading_zeros = 0u32;
2745 while leading_zeros < 32 {
2746 match br.bits(1) {
2747 Some(0) => leading_zeros += 1,
2748 Some(_) => break,
2749 None => return 0,
2750 }
2751 }
2752 if leading_zeros >= 32 {
2753 return u32::MAX;
2754 }
2755 let value = br.bits(leading_zeros).unwrap_or(0);
2756 value + ((1u32 << leading_zeros) - 1)
2757}
2758
2759#[cfg(test)]
2760mod tests {
2761 use super::*;
2762
2763 #[test]
2764 fn ftyp_starts_with_size_and_type() {
2765 let ftyp = build_ftyp();
2766 let size = u32::from_be_bytes([ftyp[0], ftyp[1], ftyp[2], ftyp[3]]);
2767 assert_eq!(size as usize, ftyp.len());
2768 assert_eq!(&ftyp[4..8], b"ftyp");
2769 }
2770
2771 #[test]
2772 fn leb128_roundtrip() {
2773 let mut buf = Vec::new();
2774 write_leb128(&mut buf, 300);
2775 let (v, n) = read_leb128(&buf).unwrap();
2776 assert_eq!(v, 300);
2777 assert_eq!(n, buf.len());
2778 }
2779
2780 #[test]
2781 fn box_builder_sizes_correctly() {
2782 let mut b = BoxBuilder::new(b"test");
2783 b.u32(0xDEADBEEF);
2784 let out = b.finish();
2785 assert_eq!(out.len(), 12);
2786 assert_eq!(&out[4..8], b"test");
2787 assert_eq!(u32::from_be_bytes([out[0], out[1], out[2], out[3]]), 12);
2788 }
2789
2790 fn parse_stsc_entries(stsc: &[u8]) -> Vec<(u32, u32, u32)> {
2794 assert_eq!(&stsc[4..8], b"stsc");
2795 let count = u32::from_be_bytes([stsc[12], stsc[13], stsc[14], stsc[15]]) as usize;
2797 let mut out = Vec::with_capacity(count);
2798 let mut p = 16usize;
2799 for _ in 0..count {
2800 let fc = u32::from_be_bytes([stsc[p], stsc[p + 1], stsc[p + 2], stsc[p + 3]]);
2801 let spc = u32::from_be_bytes([stsc[p + 4], stsc[p + 5], stsc[p + 6], stsc[p + 7]]);
2802 let sdi = u32::from_be_bytes([stsc[p + 8], stsc[p + 9], stsc[p + 10], stsc[p + 11]]);
2803 out.push((fc, spc, sdi));
2804 p += 12;
2805 }
2806 out
2807 }
2808
2809 #[test]
2810 fn mux_stsc_emits_multiple_chunk_runs() {
2811 let stsc = build_stsc(120, 24);
2813 let entries = parse_stsc_entries(&stsc);
2814 assert_eq!(entries, vec![(1, 24, 1)]);
2815 }
2816
2817 #[test]
2818 fn mux_stsc_last_chunk_under_spc_emits_tail_entry() {
2819 let stsc = build_stsc(121, 24);
2821 let entries = parse_stsc_entries(&stsc);
2822 assert_eq!(entries, vec![(1, 24, 1), (6, 1, 1)]);
2823 }
2824
2825 #[test]
2826 fn mux_stsc_all_under_spc_single_entry() {
2827 let stsc = build_stsc(10, 24);
2829 let entries = parse_stsc_entries(&stsc);
2830 assert_eq!(entries, vec![(1, 10, 1)]);
2831 }
2832
2833 #[test]
2836 fn compute_chunk_offsets_walks_sample_sizes() {
2837 let sizes = vec![100u32, 200, 300, 400, 500, 600, 700];
2838 let offs = compute_chunk_offsets(1000, &sizes, 3);
2839 assert_eq!(offs, vec![1000, 1600, 3100]);
2841 }
2842
2843 #[test]
2844 fn compute_chunk_offsets_single_chunk() {
2845 let sizes = vec![10u32; 5];
2846 let offs = compute_chunk_offsets(42, &sizes, 120);
2847 assert_eq!(offs, vec![42]);
2848 }
2849
2850 #[test]
2853 fn build_stco_emits_32bit_offsets() {
2854 let offs = vec![8u64, 1_000_000, u32::MAX as u64];
2855 let box_bytes = build_stco(&offs);
2856 assert_eq!(&box_bytes[4..8], b"stco");
2857 let count =
2858 u32::from_be_bytes([box_bytes[12], box_bytes[13], box_bytes[14], box_bytes[15]]);
2859 assert_eq!(count, 3);
2860 assert_eq!(box_bytes.len(), 16 + 12);
2862 let last = u32::from_be_bytes([box_bytes[24], box_bytes[25], box_bytes[26], box_bytes[27]]);
2863 assert_eq!(last, u32::MAX);
2864 }
2865
2866 #[test]
2867 fn build_co64_emits_64bit_offsets() {
2868 let big = (u32::MAX as u64) + 100;
2869 let offs = vec![8u64, big, big + 1_000_000];
2870 let box_bytes = build_co64(&offs);
2871 assert_eq!(&box_bytes[4..8], b"co64");
2872 let count =
2873 u32::from_be_bytes([box_bytes[12], box_bytes[13], box_bytes[14], box_bytes[15]]);
2874 assert_eq!(count, 3);
2875 assert_eq!(box_bytes.len(), 16 + 24);
2877 let got = u64::from_be_bytes([
2879 box_bytes[24],
2880 box_bytes[25],
2881 box_bytes[26],
2882 box_bytes[27],
2883 box_bytes[28],
2884 box_bytes[29],
2885 box_bytes[30],
2886 box_bytes[31],
2887 ]);
2888 assert_eq!(got, big);
2889 }
2890
2891 #[test]
2892 fn build_co64_offsets_are_monotonic_and_be() {
2893 let offs: Vec<u64> = (0..5)
2896 .map(|i| 10_000_000_000u64 + i as u64 * 4096)
2897 .collect();
2898 let box_bytes = build_co64(&offs);
2899 let mut prev = 0u64;
2900 for i in 0..5 {
2901 let p = 16 + i * 8;
2902 let v = u64::from_be_bytes([
2903 box_bytes[p],
2904 box_bytes[p + 1],
2905 box_bytes[p + 2],
2906 box_bytes[p + 3],
2907 box_bytes[p + 4],
2908 box_bytes[p + 5],
2909 box_bytes[p + 6],
2910 box_bytes[p + 7],
2911 ]);
2912 assert!(v > prev, "offsets not monotonic: {v} after {prev}");
2913 prev = v;
2914 }
2915 }
2916
2917 fn find_fourcc(data: &[u8], tag: &[u8; 4]) -> Option<usize> {
2922 data.windows(4).position(|w| w == tag)
2923 }
2924
2925 #[test]
2926 fn moov_with_use_co64_true_emits_co64_not_stco() {
2927 let sample_sizes = vec![1000u32; 120];
2928 let chunk_offsets: Vec<u64> = (0..5)
2930 .map(|i| (u32::MAX as u64) + i * 1_000_000_000)
2931 .collect();
2932 let config_obus = vec![0x0Au8, 0x03, 0x00, 0x00, 0x00];
2934 let moov = build_moov(
2935 1920,
2936 1080,
2937 90_000,
2938 120 * 3750,
2939 3750,
2940 &sample_sizes,
2941 &[],
2942 &config_obus,
2943 &chunk_offsets,
2944 24,
2945 true,
2946 );
2947 assert!(find_fourcc(&moov, b"co64").is_some(), "co64 box missing");
2948 assert!(
2951 find_fourcc(&moov, b"stco").is_none(),
2952 "stco present when co64 chosen"
2953 );
2954 }
2955
2956 #[test]
2957 fn moov_with_use_co64_false_emits_stco_not_co64() {
2958 let sample_sizes = vec![1000u32; 120];
2959 let chunk_offsets: Vec<u64> = (0..5).map(|i| 1000 + i * 24_000).collect();
2960 let config_obus = vec![0x0Au8, 0x03, 0x00, 0x00, 0x00];
2961 let moov = build_moov(
2962 1920,
2963 1080,
2964 90_000,
2965 120 * 3750,
2966 3750,
2967 &sample_sizes,
2968 &[],
2969 &config_obus,
2970 &chunk_offsets,
2971 24,
2972 false,
2973 );
2974 assert!(find_fourcc(&moov, b"stco").is_some(), "stco box missing");
2975 assert!(
2976 find_fourcc(&moov, b"co64").is_none(),
2977 "co64 present when stco chosen"
2978 );
2979 }
2980
2981 #[test]
2988 fn ftyp_lists_av01_and_iso6_and_mp42_brands() {
2989 let ftyp = build_ftyp();
2990 assert_eq!(&ftyp[8..12], b"iso6", "major_brand should be iso6");
2992 let compat = &ftyp[16..];
2994 let brands: Vec<&[u8]> = compat.chunks_exact(4).collect();
2995 assert!(
2996 brands.contains(&b"av01".as_ref()),
2997 "compatible_brands must list av01 per AV1-ISOBMFF §2.1; got {:?}",
2998 brands
2999 );
3000 assert!(
3001 brands.contains(&b"iso6".as_ref()),
3002 "compatible_brands must list iso6 (14496-12 v6 — covers co64/largesize)"
3003 );
3004 assert!(
3005 brands.contains(&b"mp42".as_ref()),
3006 "compatible_brands should list mp42 for AAC parsing rules"
3007 );
3008 }
3009
3010 fn count_fourcc_occurrences(data: &[u8], tag: &[u8; 4]) -> usize {
3015 data.windows(4).filter(|w| *w == tag).count()
3016 }
3017
3018 #[test]
3019 fn av01_sample_entry_includes_colr_nclx_box() {
3020 let cm = ColorMetadata::default();
3021 let sample_sizes = vec![100u32; 30];
3022 let chunk_offsets: Vec<u64> = vec![1000];
3023 let config_obus = vec![0x0Au8, 0x03, 0x00, 0x00, 0x00];
3024 let moov = build_moov_any(
3025 1920,
3026 1080,
3027 90_000,
3028 90_000,
3029 30 * 3000,
3030 30 * 3000,
3031 3000,
3032 &sample_sizes,
3033 &[],
3034 &config_obus,
3035 &chunk_offsets,
3036 30,
3037 None,
3038 &[],
3039 false,
3040 &cm,
3041 );
3042 let colr_pos = find_fourcc(&moov, b"colr").expect("colr atom missing");
3043 assert_eq!(
3046 &moov[colr_pos + 4..colr_pos + 8],
3047 b"nclx",
3048 "colour_type must be 'nclx' per ISO/IEC 23001-8"
3049 );
3050 let cp = u16::from_be_bytes([moov[colr_pos + 8], moov[colr_pos + 9]]);
3052 assert_eq!(cp, 1, "default BT.709 colour_primaries=1");
3053 let tc = u16::from_be_bytes([moov[colr_pos + 10], moov[colr_pos + 11]]);
3055 assert_eq!(tc, 1, "default BT.709 transfer_characteristics=1");
3056 let mc = u16::from_be_bytes([moov[colr_pos + 12], moov[colr_pos + 13]]);
3058 assert_eq!(mc, 1, "default BT.709 matrix_coefficients=1");
3059 let fr = moov[colr_pos + 14];
3061 assert_eq!(fr & 0x80, 0x00, "default limited-range full_range_flag=0");
3062 }
3063
3064 #[test]
3065 fn colr_nclx_carries_hdr10_metadata() {
3066 let cm = ColorMetadata {
3071 transfer: codec::frame::TransferFn::St2084,
3072 matrix_coefficients: 9,
3073 colour_primaries: 9,
3074 full_range: false,
3075 ..ColorMetadata::default()
3076 };
3077 let colr = build_colr_nclx(&cm);
3078 assert_eq!(&colr[4..8], b"colr");
3079 assert_eq!(&colr[8..12], b"nclx");
3080 let cp = u16::from_be_bytes([colr[12], colr[13]]);
3081 let tc = u16::from_be_bytes([colr[14], colr[15]]);
3082 let mc = u16::from_be_bytes([colr[16], colr[17]]);
3083 let fr = colr[18];
3084 assert_eq!(cp, 9, "BT.2020 NCL primaries");
3085 assert_eq!(tc, 16, "ST 2084 PQ transfer");
3086 assert_eq!(mc, 9, "BT.2020 NCL matrix");
3087 assert_eq!(fr & 0x80, 0x00, "HDR10 typically signals limited range");
3088 }
3089
3090 #[test]
3091 fn colr_nclx_full_range_sets_high_bit() {
3092 let cm = ColorMetadata {
3093 transfer: codec::frame::TransferFn::Bt709,
3094 matrix_coefficients: 1,
3095 colour_primaries: 1,
3096 full_range: true,
3097 ..ColorMetadata::default()
3098 };
3099 let colr = build_colr_nclx(&cm);
3100 assert_eq!(colr[18] & 0x80, 0x80, "full_range high bit must be set");
3101 assert_eq!(colr[18] & 0x7F, 0x00, "reserved bits must be zero");
3103 }
3104
3105 #[test]
3106 fn colr_nclx_box_size_matches_layout() {
3107 let colr = build_colr_nclx(&ColorMetadata::default());
3109 let size = u32::from_be_bytes([colr[0], colr[1], colr[2], colr[3]]) as usize;
3110 assert_eq!(
3111 size,
3112 colr.len(),
3113 "colr box size field must equal box length"
3114 );
3115 assert_eq!(size, 19, "colr nclx must be exactly 19 bytes");
3116 }
3117
3118 #[test]
3122 fn colr_lives_inside_av01_sample_entry() {
3123 let cm = ColorMetadata::default();
3124 let sample_sizes = vec![100u32; 30];
3125 let chunk_offsets: Vec<u64> = vec![1000];
3126 let config_obus = vec![0x0Au8, 0x03, 0x00, 0x00, 0x00];
3127 let moov = build_moov_any(
3128 1920,
3129 1080,
3130 90_000,
3131 90_000,
3132 30 * 3000,
3133 30 * 3000,
3134 3000,
3135 &sample_sizes,
3136 &[],
3137 &config_obus,
3138 &chunk_offsets,
3139 30,
3140 None,
3141 &[],
3142 false,
3143 &cm,
3144 );
3145 let av01_pos = find_fourcc(&moov, b"av01").expect("av01 sample entry missing");
3146 let av01_size = u32::from_be_bytes([
3147 moov[av01_pos - 4],
3148 moov[av01_pos - 3],
3149 moov[av01_pos - 2],
3150 moov[av01_pos - 1],
3151 ]) as usize;
3152 let av01_end = av01_pos - 4 + av01_size;
3153 let colr_pos = find_fourcc(&moov, b"colr").expect("colr missing");
3154 assert!(
3155 colr_pos > av01_pos && colr_pos < av01_end,
3156 "colr must be nested inside av01 sample entry: av01@{}..{} colr@{}",
3157 av01_pos,
3158 av01_end,
3159 colr_pos
3160 );
3161 assert_eq!(
3162 count_fourcc_occurrences(&moov, b"colr"),
3163 1,
3164 "exactly one colr atom expected"
3165 );
3166 }
3167
3168 #[test]
3174 fn transfer_to_h273_emits_canonical_codes() {
3175 use codec::frame::TransferFn;
3176 assert_eq!(transfer_to_h273(TransferFn::Bt709), 1);
3177 assert_eq!(transfer_to_h273(TransferFn::Bt470Bg), 4);
3178 assert_eq!(transfer_to_h273(TransferFn::Linear), 8);
3179 assert_eq!(transfer_to_h273(TransferFn::St2084), 16);
3180 assert_eq!(transfer_to_h273(TransferFn::AribStdB67), 18);
3181 assert_eq!(transfer_to_h273(TransferFn::Unspecified), 2);
3182 }
3183
3184 fn hdr10_mastering_display() -> codec::frame::MasteringDisplay {
3199 codec::frame::MasteringDisplay {
3200 primaries_r_x: 35400,
3201 primaries_r_y: 14600,
3202 primaries_g_x: 8500,
3203 primaries_g_y: 39850,
3204 primaries_b_x: 6550,
3205 primaries_b_y: 2300,
3206 white_point_x: 15635,
3207 white_point_y: 16450,
3208 max_luminance: 10_000_000,
3209 min_luminance: 1,
3210 }
3211 }
3212
3213 #[test]
3216 fn mdcv_box_24_byte_payload_layout() {
3217 let md = hdr10_mastering_display();
3218 let mdcv = build_mdcv(&md);
3219 assert_eq!(
3220 mdcv.len(),
3221 32,
3222 "mdcv box must be exactly 32 bytes (8 header + 24 payload)"
3223 );
3224 let size = u32::from_be_bytes([mdcv[0], mdcv[1], mdcv[2], mdcv[3]]) as usize;
3225 assert_eq!(size, mdcv.len(), "size field must equal box length");
3226 assert_eq!(&mdcv[4..8], b"mdcv", "box type must be 'mdcv' (not 'SmDm')");
3227 let u16_at = |off: usize| u16::from_be_bytes([mdcv[off], mdcv[off + 1]]);
3229 let u32_at = |off: usize| {
3230 u32::from_be_bytes([mdcv[off], mdcv[off + 1], mdcv[off + 2], mdcv[off + 3]])
3231 };
3232 assert_eq!(u16_at(8), 35400, "primaries_r_x");
3233 assert_eq!(u16_at(10), 14600, "primaries_r_y");
3234 assert_eq!(u16_at(12), 8500, "primaries_g_x");
3235 assert_eq!(u16_at(14), 39850, "primaries_g_y");
3236 assert_eq!(u16_at(16), 6550, "primaries_b_x");
3237 assert_eq!(u16_at(18), 2300, "primaries_b_y");
3238 assert_eq!(u16_at(20), 15635, "white_point_x");
3239 assert_eq!(u16_at(22), 16450, "white_point_y");
3240 assert_eq!(u32_at(24), 10_000_000, "max_luminance (0.0001 cd/m² steps)");
3241 assert_eq!(u32_at(28), 1, "min_luminance");
3242 }
3243
3244 #[test]
3247 fn clli_box_4_byte_payload_layout() {
3248 let cll = codec::frame::ContentLightLevel {
3249 max_cll: 1000,
3250 max_fall: 400,
3251 };
3252 let clli = build_clli(&cll);
3253 assert_eq!(
3254 clli.len(),
3255 12,
3256 "clli box must be exactly 12 bytes (8 header + 4 payload)"
3257 );
3258 let size = u32::from_be_bytes([clli[0], clli[1], clli[2], clli[3]]) as usize;
3259 assert_eq!(size, clli.len(), "size field must equal box length");
3260 assert_eq!(&clli[4..8], b"clli", "box type must be 'clli' (not 'CoLL')");
3261 let max_cll = u16::from_be_bytes([clli[8], clli[9]]);
3262 let max_fall = u16::from_be_bytes([clli[10], clli[11]]);
3263 assert_eq!(max_cll, 1000, "max_cll");
3264 assert_eq!(max_fall, 400, "max_fall");
3265 }
3266
3267 #[test]
3271 fn mdcv_omitted_when_none() {
3272 let cm = ColorMetadata::default(); let sample_sizes = vec![100u32; 30];
3274 let chunk_offsets: Vec<u64> = vec![1000];
3275 let config_obus = vec![0x0Au8, 0x03, 0x00, 0x00, 0x00];
3276 let moov = build_moov_any(
3277 1920,
3278 1080,
3279 90_000,
3280 90_000,
3281 30 * 3000,
3282 30 * 3000,
3283 3000,
3284 &sample_sizes,
3285 &[],
3286 &config_obus,
3287 &chunk_offsets,
3288 30,
3289 None,
3290 &[],
3291 false,
3292 &cm,
3293 );
3294 assert!(
3295 find_fourcc(&moov, b"mdcv").is_none(),
3296 "SDR (mastering_display=None) moov must NOT contain mdcv box"
3297 );
3298 }
3299
3300 #[test]
3303 fn clli_omitted_when_none() {
3304 let cm = ColorMetadata::default();
3305 let sample_sizes = vec![100u32; 30];
3306 let chunk_offsets: Vec<u64> = vec![1000];
3307 let config_obus = vec![0x0Au8, 0x03, 0x00, 0x00, 0x00];
3308 let moov = build_moov_any(
3309 1920,
3310 1080,
3311 90_000,
3312 90_000,
3313 30 * 3000,
3314 30 * 3000,
3315 3000,
3316 &sample_sizes,
3317 &[],
3318 &config_obus,
3319 &chunk_offsets,
3320 30,
3321 None,
3322 &[],
3323 false,
3324 &cm,
3325 );
3326 assert!(
3327 find_fourcc(&moov, b"clli").is_none(),
3328 "SDR (content_light_level=None) moov must NOT contain clli box"
3329 );
3330 }
3331
3332 #[test]
3338 fn av01_sample_entry_emits_mdcv_and_clli_in_order() {
3339 let cm = ColorMetadata {
3340 transfer: codec::frame::TransferFn::St2084,
3341 matrix_coefficients: 9,
3342 colour_primaries: 9,
3343 full_range: false,
3344 mastering_display: Some(hdr10_mastering_display()),
3345 content_light_level: Some(codec::frame::ContentLightLevel {
3346 max_cll: 1000,
3347 max_fall: 400,
3348 }),
3349 };
3350 let sample_sizes = vec![100u32; 30];
3351 let chunk_offsets: Vec<u64> = vec![1000];
3352 let config_obus = vec![0x0Au8, 0x03, 0x00, 0x00, 0x00];
3353 let moov = build_moov_any(
3354 1920,
3355 1080,
3356 90_000,
3357 90_000,
3358 30 * 3000,
3359 30 * 3000,
3360 3000,
3361 &sample_sizes,
3362 &[],
3363 &config_obus,
3364 &chunk_offsets,
3365 30,
3366 None,
3367 &[],
3368 false,
3369 &cm,
3370 );
3371 let av01_pos = find_fourcc(&moov, b"av01").expect("av01 sample entry missing");
3372 let av01_size = u32::from_be_bytes([
3373 moov[av01_pos - 4],
3374 moov[av01_pos - 3],
3375 moov[av01_pos - 2],
3376 moov[av01_pos - 1],
3377 ]) as usize;
3378 let av01_end = av01_pos - 4 + av01_size;
3379 let av01_body = &moov[av01_pos..av01_end];
3380 let colr_rel = av01_body
3381 .windows(4)
3382 .position(|w| w == b"colr")
3383 .expect("colr nested in av01");
3384 let mdcv_rel = av01_body
3385 .windows(4)
3386 .position(|w| w == b"mdcv")
3387 .expect("mdcv nested in av01");
3388 let clli_rel = av01_body
3389 .windows(4)
3390 .position(|w| w == b"clli")
3391 .expect("clli nested in av01");
3392 assert!(
3393 colr_rel < mdcv_rel,
3394 "colr ({}) must precede mdcv ({})",
3395 colr_rel,
3396 mdcv_rel
3397 );
3398 assert!(
3399 mdcv_rel < clli_rel,
3400 "mdcv ({}) must precede clli ({})",
3401 mdcv_rel,
3402 clli_rel
3403 );
3404 assert_eq!(
3406 count_fourcc_occurrences(&moov, b"mdcv"),
3407 1,
3408 "exactly one mdcv expected"
3409 );
3410 assert_eq!(
3411 count_fourcc_occurrences(&moov, b"clli"),
3412 1,
3413 "exactly one clli expected"
3414 );
3415 }
3416
3417 #[test]
3423 fn colr_handles_pq_transfer_code_16() {
3424 let cm = ColorMetadata {
3425 transfer: codec::frame::TransferFn::St2084,
3426 matrix_coefficients: 9,
3427 colour_primaries: 9,
3428 full_range: false,
3429 ..ColorMetadata::default()
3430 };
3431 let colr = build_colr_nclx(&cm);
3432 let tc = u16::from_be_bytes([colr[14], colr[15]]);
3433 assert_eq!(tc, 16, "PQ transfer must encode as H.273 code 16");
3434 }
3435
3436 #[test]
3440 fn colr_handles_hlg_transfer_code_18() {
3441 let cm = ColorMetadata {
3442 transfer: codec::frame::TransferFn::AribStdB67,
3443 matrix_coefficients: 9,
3444 colour_primaries: 9,
3445 full_range: false,
3446 ..ColorMetadata::default()
3447 };
3448 let colr = build_colr_nclx(&cm);
3449 let tc = u16::from_be_bytes([colr[14], colr[15]]);
3450 assert_eq!(tc, 18, "HLG transfer must encode as H.273 code 18");
3451 }
3452
3453 #[test]
3458 fn colr_bt2020_primaries_matrix() {
3459 let cm_ncl = ColorMetadata {
3461 transfer: codec::frame::TransferFn::St2084,
3462 matrix_coefficients: 9,
3463 colour_primaries: 9,
3464 full_range: false,
3465 ..ColorMetadata::default()
3466 };
3467 let colr_ncl = build_colr_nclx(&cm_ncl);
3468 let cp_ncl = u16::from_be_bytes([colr_ncl[12], colr_ncl[13]]);
3469 let mc_ncl = u16::from_be_bytes([colr_ncl[16], colr_ncl[17]]);
3470 assert_eq!(cp_ncl, 9, "BT.2020 colour_primaries must be 9");
3471 assert_eq!(mc_ncl, 9, "BT.2020 NCL matrix must be 9");
3472
3473 let cm_cl = ColorMetadata {
3475 matrix_coefficients: 10,
3476 ..cm_ncl
3477 };
3478 let colr_cl = build_colr_nclx(&cm_cl);
3479 let mc_cl = u16::from_be_bytes([colr_cl[16], colr_cl[17]]);
3480 assert_eq!(
3481 mc_cl, 10,
3482 "BT.2020 CL matrix must be 10 (preserved verbatim)"
3483 );
3484 }
3485
3486 fn opus_head_stereo_48k_preskip_312() -> Vec<u8> {
3501 let mut head = Vec::with_capacity(11);
3502 head.push(1u8); head.push(2u8); head.extend_from_slice(&312u16.to_le_bytes()); head.extend_from_slice(&48_000u32.to_le_bytes()); head.extend_from_slice(&0i16.to_le_bytes()); head.push(0u8); head
3509 }
3510
3511 fn opus_info_stereo_48k() -> AudioInfo {
3512 AudioInfo {
3513 codec: "opus".into(),
3514 sample_rate: 48_000,
3515 channels: 2,
3516 timescale: 48_000,
3517 asc_bytes: Vec::new(),
3518 codec_private: opus_head_stereo_48k_preskip_312(),
3519 }
3520 }
3521
3522 #[test]
3527 fn dops_box_11_byte_payload_layout() {
3528 let info = opus_info_stereo_48k();
3529 let dops = build_dops(&info);
3530 assert_eq!(
3531 dops.len(),
3532 19,
3533 "dOps must be exactly 19 bytes (8 header + 11 payload)"
3534 );
3535 let size = u32::from_be_bytes([dops[0], dops[1], dops[2], dops[3]]) as usize;
3536 assert_eq!(size, dops.len(), "size field must equal box length");
3537 assert_eq!(
3538 &dops[4..8],
3539 b"dOps",
3540 "box type must be 'dOps' (capital O lowercase ps)"
3541 );
3542 assert_eq!(dops[8], 0, "Version (RFC 7845 §4.5: MUST be 0)");
3544 assert_eq!(dops[9], 2, "OutputChannelCount = stereo");
3545 let pre_skip = u16::from_be_bytes([dops[10], dops[11]]);
3546 assert_eq!(pre_skip, 312, "PreSkip = 312 (BE)");
3547 let input_sample_rate = u32::from_be_bytes([dops[12], dops[13], dops[14], dops[15]]);
3548 assert_eq!(input_sample_rate, 48_000, "InputSampleRate = 48000 (BE)");
3549 let output_gain = i16::from_be_bytes([dops[16], dops[17]]);
3550 assert_eq!(output_gain, 0, "OutputGain = 0 (Q8 dB, BE)");
3551 assert_eq!(dops[18], 0, "ChannelMappingFamily = 0 (mono/stereo)");
3552 }
3553
3554 #[test]
3558 fn dops_byte_order_flipped_from_opushead() {
3559 let info = opus_info_stereo_48k();
3560 assert_eq!(
3562 info.codec_private[2..4],
3563 [0x38, 0x01],
3564 "OpusHead PreSkip must be LE"
3565 );
3566 let dops = build_dops(&info);
3567 assert_eq!(
3569 dops[10..12],
3570 [0x01, 0x38],
3571 "dOps PreSkip must be BE — got {:02X?}",
3572 &dops[10..12]
3573 );
3574 }
3575
3576 #[test]
3581 fn opus_sample_entry_size_and_fourcc() {
3582 let info = opus_info_stereo_48k();
3583 let entry = build_opus_sample_entry(&info);
3584 let size = u32::from_be_bytes([entry[0], entry[1], entry[2], entry[3]]) as usize;
3585 assert_eq!(size, entry.len(), "size field must equal box length");
3586 assert_eq!(&entry[4..8], b"Opus", "4-cc MUST be 'Opus' (capital O)");
3587 assert_ne!(&entry[4..8], b"opus", "lowercase 'opus' is non-conformant");
3588 assert_eq!(
3590 entry.len(),
3591 55,
3592 "Opus sample entry should be 55 bytes for stereo + dOps minimum"
3593 );
3594 }
3595
3596 #[test]
3601 fn opus_sample_entry_samplerate_is_48000_q16() {
3602 let info = AudioInfo {
3603 sample_rate: 44_100,
3606 ..opus_info_stereo_48k()
3607 };
3608 let entry = build_opus_sample_entry(&info);
3609 let sr_q16 = u32::from_be_bytes([entry[32], entry[33], entry[34], entry[35]]);
3615 assert_eq!(
3616 sr_q16,
3617 48_000u32 << 16,
3618 "samplerate field MUST be 48000<<16 (Q16); got 0x{:08X}",
3619 sr_q16
3620 );
3621 }
3622
3623 #[test]
3626 fn dops_nests_inside_opus_sample_entry() {
3627 let info = opus_info_stereo_48k();
3628 let entry = build_opus_sample_entry(&info);
3629 let dops_pos = entry
3630 .windows(4)
3631 .position(|w| w == b"dOps")
3632 .expect("dOps child missing inside Opus sample entry");
3633 assert!(
3635 dops_pos > 28,
3636 "dOps must come after the AudioSampleEntry preamble; got pos={}",
3637 dops_pos
3638 );
3639 }
3640
3641 #[test]
3644 fn stsd_dispatcher_routes_codec_to_correct_sample_entry() {
3645 let aac = AudioInfo {
3646 codec: "aac".into(),
3647 sample_rate: 44_100,
3648 channels: 2,
3649 timescale: 44_100,
3650 asc_bytes: vec![0x12, 0x10],
3651 codec_private: Vec::new(),
3652 };
3653 let stsd_aac = build_audio_stsd(&aac);
3654 assert!(
3655 stsd_aac.windows(4).any(|w| w == b"mp4a"),
3656 "AAC stsd must contain mp4a"
3657 );
3658 assert!(
3659 !stsd_aac.windows(4).any(|w| w == b"Opus"),
3660 "AAC stsd must NOT contain Opus"
3661 );
3662 assert!(
3663 stsd_aac.windows(4).any(|w| w == b"esds"),
3664 "AAC stsd must contain esds"
3665 );
3666
3667 let opus = opus_info_stereo_48k();
3668 let stsd_opus = build_audio_stsd(&opus);
3669 assert!(
3670 stsd_opus.windows(4).any(|w| w == b"Opus"),
3671 "Opus stsd must contain Opus"
3672 );
3673 assert!(
3674 !stsd_opus.windows(4).any(|w| w == b"mp4a"),
3675 "Opus stsd must NOT contain mp4a"
3676 );
3677 assert!(
3678 stsd_opus.windows(4).any(|w| w == b"dOps"),
3679 "Opus stsd must contain dOps"
3680 );
3681 assert!(
3682 !stsd_opus.windows(4).any(|w| w == b"esds"),
3683 "Opus stsd must NOT contain esds"
3684 );
3685 }
3686
3687 #[test]
3690 fn dops_handles_negative_output_gain() {
3691 let mut head = opus_head_stereo_48k_preskip_312();
3692 let gain: i16 = -768;
3694 head[8..10].copy_from_slice(&gain.to_le_bytes());
3695 let info = AudioInfo {
3696 codec_private: head,
3697 ..opus_info_stereo_48k()
3698 };
3699 let dops = build_dops(&info);
3700 let recovered = i16::from_be_bytes([dops[16], dops[17]]);
3701 assert_eq!(
3702 recovered, -768,
3703 "negative OutputGain must survive LE→BE roundtrip"
3704 );
3705 }
3706
3707 #[test]
3711 fn dops_preserves_arbitrary_preskip() {
3712 for &expected in &[0u16, 156, 312, 480, 1024, 65535] {
3713 let mut head = opus_head_stereo_48k_preskip_312();
3714 head[2..4].copy_from_slice(&expected.to_le_bytes());
3715 let info = AudioInfo {
3716 codec_private: head,
3717 ..opus_info_stereo_48k()
3718 };
3719 let dops = build_dops(&info);
3720 let got = u16::from_be_bytes([dops[10], dops[11]]);
3721 assert_eq!(got, expected, "PreSkip {} must survive LE→BE", expected);
3722 }
3723 }
3724
3725 fn opus_head_surround(
3732 channels: u8,
3733 pre_skip: u16,
3734 input_sample_rate: u32,
3735 streams: u8,
3736 coupled: u8,
3737 mapping: &[u8],
3738 ) -> Vec<u8> {
3739 assert_eq!(mapping.len(), channels as usize);
3740 let mut h = Vec::with_capacity(11 + 2 + channels as usize);
3741 h.push(1u8); h.push(channels);
3743 h.extend_from_slice(&pre_skip.to_le_bytes());
3744 h.extend_from_slice(&input_sample_rate.to_le_bytes());
3745 h.extend_from_slice(&0i16.to_le_bytes()); h.push(1u8); h.push(streams);
3748 h.push(coupled);
3749 h.extend_from_slice(mapping);
3750 h
3751 }
3752
3753 fn opus_info_5_1() -> AudioInfo {
3754 let cp = opus_head_surround(6, 312, 48_000, 4, 2, &[0, 4, 1, 2, 3, 5]);
3758 AudioInfo {
3759 codec: "opus".into(),
3760 sample_rate: 48_000,
3761 channels: 6,
3762 timescale: 48_000,
3763 asc_bytes: Vec::new(),
3764 codec_private: cp,
3765 }
3766 }
3767
3768 #[test]
3773 fn dops_box_5_1_payload_is_19_bytes_total_27() {
3774 let info = opus_info_5_1();
3775 let dops = build_dops(&info);
3776 assert_eq!(
3777 dops.len(),
3778 27,
3779 "5.1 dOps box = 8 header + 19 payload = 27 bytes; got {}",
3780 dops.len()
3781 );
3782 let size = u32::from_be_bytes([dops[0], dops[1], dops[2], dops[3]]) as usize;
3783 assert_eq!(size, dops.len());
3784 assert_eq!(&dops[4..8], b"dOps");
3785 assert_eq!(dops[8], 0, "Version");
3787 assert_eq!(dops[9], 6, "OutputChannelCount = 6 for 5.1");
3788 let pre_skip = u16::from_be_bytes([dops[10], dops[11]]);
3789 assert_eq!(pre_skip, 312);
3790 let isr = u32::from_be_bytes([dops[12], dops[13], dops[14], dops[15]]);
3791 assert_eq!(isr, 48_000);
3792 assert_eq!(i16::from_be_bytes([dops[16], dops[17]]), 0);
3793 assert_eq!(dops[18], 1, "ChannelMappingFamily = 1 for surround");
3794 assert_eq!(dops[19], 4, "StreamCount = 4 for 5.1");
3795 assert_eq!(dops[20], 2, "CoupledCount = 2 for 5.1");
3796 assert_eq!(
3797 &dops[21..27],
3798 &[0u8, 4, 1, 2, 3, 5][..],
3799 "ChannelMapping for 5.1"
3800 );
3801 }
3802
3803 #[test]
3806 fn dops_box_7_1_payload_is_21_bytes_total_29() {
3807 let cp = opus_head_surround(8, 312, 48_000, 5, 3, &[0, 6, 1, 2, 3, 4, 5, 7]);
3808 let info = AudioInfo {
3809 codec: "opus".into(),
3810 sample_rate: 48_000,
3811 channels: 8,
3812 timescale: 48_000,
3813 asc_bytes: Vec::new(),
3814 codec_private: cp,
3815 };
3816 let dops = build_dops(&info);
3817 assert_eq!(dops.len(), 29);
3818 assert_eq!(dops[18], 1, "Family = 1");
3819 assert_eq!(dops[19], 5, "StreamCount = 5 for 7.1");
3820 assert_eq!(dops[20], 3, "CoupledCount = 3 for 7.1");
3821 assert_eq!(&dops[21..29], &[0u8, 6, 1, 2, 3, 4, 5, 7][..]);
3822 }
3823
3824 #[test]
3826 fn dops_box_5_1_hex_dump() {
3827 let info = opus_info_5_1();
3828 let dops = build_dops(&info);
3829 let hex: String = dops.iter().map(|b| format!("{b:02x} ")).collect();
3830 println!("5.1 dOps box hex (27 bytes total): {}", hex.trim_end());
3831 }
3832
3833 #[test]
3836 fn opus_sample_entry_5_1_size_and_dops_nesting() {
3837 let info = opus_info_5_1();
3838 let entry = build_opus_sample_entry(&info);
3839 assert_eq!(
3840 entry.len(),
3841 36 + 27,
3842 "Opus sample entry for 5.1 = 36 + 27 = 63 bytes; got {}",
3843 entry.len()
3844 );
3845 let entry_channels = u16::from_be_bytes([entry[24], entry[25]]);
3849 assert_eq!(
3850 entry_channels, 6,
3851 "channel_count in AudioSampleEntry must reflect 5.1"
3852 );
3853 assert!(entry[36..].windows(4).any(|w| w == b"dOps"));
3855 assert_eq!(
3858 entry[36 + 8 + 10],
3859 1,
3860 "dOps inside Opus sample entry must carry family=1 for 5.1"
3861 );
3862 }
3863
3864 #[test]
3868 fn with_audio_rejects_family_1_with_truncated_codec_private() {
3869 let mut muxer = Av1Mp4Muxer::new(640, 480, 30.0).unwrap();
3870 let mut info = opus_info_5_1();
3871 info.codec_private.truncate(13); let err = match muxer.with_audio(info) {
3874 Ok(_) => panic!("truncated family=1 codec_private must reject"),
3875 Err(e) => e,
3876 };
3877 let msg = format!("{}", err);
3878 assert!(
3879 msg.contains("≥") && msg.contains("preamble"),
3880 "error message must explain the size requirement; got: {msg}"
3881 );
3882 }
3883
3884 #[test]
3885 fn with_audio_rejects_family_1_with_zero_streams() {
3886 let mut muxer = Av1Mp4Muxer::new(640, 480, 30.0).unwrap();
3887 let mut info = opus_info_5_1();
3888 info.codec_private[11] = 0;
3890 let r = muxer.with_audio(info);
3891 assert!(r.is_err(), "StreamCount = 0 must reject");
3892 }
3893
3894 #[test]
3895 fn with_audio_rejects_family_1_with_coupled_exceeding_streams() {
3896 let mut muxer = Av1Mp4Muxer::new(640, 480, 30.0).unwrap();
3897 let mut info = opus_info_5_1();
3898 info.codec_private[11] = 2;
3900 info.codec_private[12] = 5;
3901 let r = muxer.with_audio(info);
3902 assert!(r.is_err(), "CoupledCount > StreamCount must reject");
3903 }
3904
3905 #[test]
3906 fn with_audio_rejects_family_1_with_mapping_index_out_of_range() {
3907 let mut muxer = Av1Mp4Muxer::new(640, 480, 30.0).unwrap();
3908 let mut info = opus_info_5_1();
3909 info.codec_private[13] = 99;
3912 let r = muxer.with_audio(info);
3913 assert!(r.is_err(), "ChannelMapping out-of-range must reject");
3914 }
3915
3916 #[test]
3917 fn with_audio_rejects_family_0_with_5_1_channels() {
3918 let mut muxer = Av1Mp4Muxer::new(640, 480, 30.0).unwrap();
3919 let mut head = Vec::with_capacity(11);
3922 head.push(1u8);
3923 head.push(6u8);
3924 head.extend_from_slice(&312u16.to_le_bytes());
3925 head.extend_from_slice(&48_000u32.to_le_bytes());
3926 head.extend_from_slice(&0i16.to_le_bytes());
3927 head.push(0u8); let info = AudioInfo {
3929 codec: "opus".into(),
3930 sample_rate: 48_000,
3931 channels: 6,
3932 timescale: 48_000,
3933 asc_bytes: Vec::new(),
3934 codec_private: head,
3935 };
3936 let r = muxer.with_audio(info);
3937 assert!(r.is_err(), "family=0 + 6 channels must reject");
3938 }
3939
3940 #[test]
3941 fn with_audio_accepts_5_1_opus() {
3942 let mut muxer = Av1Mp4Muxer::new(640, 480, 30.0).unwrap();
3943 let info = opus_info_5_1();
3944 muxer
3945 .with_audio(info)
3946 .expect("5.1 Opus with valid family=1 trailer must accept");
3947 }
3948
3949 #[test]
3950 fn with_audio_rejects_9_channel_opus() {
3951 let mut muxer = Av1Mp4Muxer::new(640, 480, 30.0).unwrap();
3952 let mut head = Vec::with_capacity(11 + 2 + 9);
3954 head.push(1u8);
3955 head.push(9u8);
3956 head.extend_from_slice(&312u16.to_le_bytes());
3957 head.extend_from_slice(&48_000u32.to_le_bytes());
3958 head.extend_from_slice(&0i16.to_le_bytes());
3959 head.push(1u8); head.push(5);
3961 head.push(3);
3962 head.extend_from_slice(&[0u8, 1, 2, 3, 4, 5, 6, 7, 0]);
3963 let info = AudioInfo {
3964 codec: "opus".into(),
3965 sample_rate: 48_000,
3966 channels: 9,
3967 timescale: 48_000,
3968 asc_bytes: Vec::new(),
3969 codec_private: head,
3970 };
3971 let r = muxer.with_audio(info);
3972 assert!(
3973 r.is_err(),
3974 "9-channel Opus must reject (no family-1 layout above 8)"
3975 );
3976 }
3977
3978 #[test]
3982 fn chan_box_omitted_for_mono_and_stereo() {
3983 assert!(build_chan_box(1).is_none(), "mono should not emit chan");
3984 assert!(build_chan_box(2).is_none(), "stereo should not emit chan");
3985 }
3986
3987 #[test]
3991 fn chan_box_omitted_for_unsupported_counts() {
3992 for &c in &[0u16, 3, 4, 5, 8, 9, 16] {
3993 assert!(
3994 build_chan_box(c).is_none(),
3995 "channels={c} must not emit chan"
3996 );
3997 }
3998 }
3999
4000 #[test]
4004 fn chan_box_5_1_layout_and_size() {
4005 let chan = build_chan_box(6).expect("5.1 must emit chan");
4006 assert_eq!(
4007 chan.len(),
4008 20,
4009 "5.1 chan box must be 20 bytes (8 header + 12 body)"
4010 );
4011 let size = u32::from_be_bytes([chan[0], chan[1], chan[2], chan[3]]);
4012 assert_eq!(
4013 size as usize,
4014 chan.len(),
4015 "size field must equal box length"
4016 );
4017 assert_eq!(&chan[4..8], b"chan", "fourcc must be 'chan'");
4018 let tag = u32::from_be_bytes([chan[8], chan[9], chan[10], chan[11]]);
4019 assert_eq!(
4020 tag, 0x00720006u32,
4021 "5.1 tag must be kAudioChannelLayoutTag_MPEG_5_1_C = 0x00720006; got 0x{tag:08X}"
4022 );
4023 let bitmap = u32::from_be_bytes([chan[12], chan[13], chan[14], chan[15]]);
4024 assert_eq!(bitmap, 0, "mChannelBitmap must be 0 for tag form");
4025 let ndescs = u32::from_be_bytes([chan[16], chan[17], chan[18], chan[19]]);
4026 assert_eq!(
4027 ndescs, 0,
4028 "mNumberChannelDescriptions must be 0 for tag form"
4029 );
4030 }
4031
4032 #[test]
4034 fn chan_box_7_1_layout_and_size() {
4035 let chan = build_chan_box(7).expect("7.1 must emit chan");
4036 assert_eq!(chan.len(), 20);
4037 let tag = u32::from_be_bytes([chan[8], chan[9], chan[10], chan[11]]);
4038 assert_eq!(
4039 tag, 0x007F0008u32,
4040 "7.1 tag must be kAudioChannelLayoutTag_MPEG_7_1_C = 0x007F0008; got 0x{tag:08X}"
4041 );
4042 }
4043
4044 #[test]
4048 fn chan_nests_inside_mp4a_for_5_1() {
4049 let info = AudioInfo {
4051 codec: "aac".into(),
4052 sample_rate: 48_000,
4053 channels: 6,
4054 timescale: 48_000,
4055 asc_bytes: vec![0x11, 0xB0],
4056 codec_private: Vec::new(),
4057 };
4058 let mp4a = build_mp4a(&info);
4059 assert_eq!(&mp4a[4..8], b"mp4a", "outer box must be mp4a");
4060 let chan_pos = mp4a
4061 .windows(4)
4062 .position(|w| w == b"chan")
4063 .expect("multichannel mp4a must contain chan child");
4064 let esds_pos = mp4a
4065 .windows(4)
4066 .position(|w| w == b"esds")
4067 .expect("mp4a must always contain esds child");
4068 assert!(
4070 chan_pos > esds_pos,
4071 "chan should come after esds in mp4a (esds @ {}, chan @ {})",
4072 esds_pos,
4073 chan_pos
4074 );
4075 }
4076
4077 #[test]
4081 fn chan_absent_from_stereo_mp4a() {
4082 let info = AudioInfo {
4083 codec: "aac".into(),
4084 sample_rate: 48_000,
4085 channels: 2,
4086 timescale: 48_000,
4087 asc_bytes: vec![0x11, 0x90],
4088 codec_private: Vec::new(),
4089 };
4090 let mp4a = build_mp4a(&info);
4091 assert!(
4092 mp4a.windows(4).all(|w| w != b"chan"),
4093 "stereo mp4a must not contain a chan box"
4094 );
4095 }
4096
4097 use crate::ac3_sync::{Ac3SyncInfo, Eac3SyncInfo};
4100
4101 fn ac3_sync_5_1_384k_48k() -> Ac3SyncInfo {
4104 Ac3SyncInfo {
4105 fscod: 0,
4106 bit_rate_code: 14,
4107 bsid: 8,
4108 bsmod: 0,
4109 acmod: 7,
4110 lfeon: true,
4111 }
4112 }
4113
4114 fn ac3_info_5_1_384k() -> AudioInfo {
4115 let body = dac3_body_from_sync(&ac3_sync_5_1_384k_48k());
4116 AudioInfo::ac3(48_000, 6, body.to_vec())
4117 }
4118
4119 fn eac3_sync_5_1_48k() -> Eac3SyncInfo {
4121 Eac3SyncInfo {
4122 strmtyp: 0,
4123 substreamid: 0,
4124 frmsiz: 191,
4128 fscod: 0,
4129 fscod2: 0,
4130 numblkscod: 3,
4131 acmod: 7,
4132 lfeon: true,
4133 bsid: 16,
4134 dialnorm: 0,
4135 bsmod: 0,
4136 }
4137 }
4138
4139 fn eac3_info_5_1_384k() -> AudioInfo {
4140 let body = dec3_body_from_sync(&eac3_sync_5_1_48k(), 192);
4142 AudioInfo::eac3(48_000, 6, body.to_vec())
4143 }
4144
4145 #[test]
4149 fn dac3_box_3_byte_payload_layout() {
4150 let info = ac3_info_5_1_384k();
4151 let dac3 = build_dac3(&info);
4152 assert_eq!(dac3.len(), 11, "dac3 = 8-byte header + 3-byte body");
4153 let size = u32::from_be_bytes([dac3[0], dac3[1], dac3[2], dac3[3]]) as usize;
4154 assert_eq!(size, dac3.len(), "size field equals box length");
4155 assert_eq!(&dac3[4..8], b"dac3", "box type 'dac3'");
4156 let raw = ((dac3[8] as u32) << 16) | ((dac3[9] as u32) << 8) | dac3[10] as u32;
4158 assert_eq!((raw >> 22) & 0x03, 0, "fscod = 0 (48 kHz)");
4159 assert_eq!((raw >> 17) & 0x1F, 8, "bsid = 8 (AC-3)");
4160 assert_eq!((raw >> 14) & 0x07, 0, "bsmod = 0");
4161 assert_eq!((raw >> 11) & 0x07, 7, "acmod = 7 (3/2 = 5.1 with LFE)");
4162 assert_eq!((raw >> 10) & 0x01, 1, "lfeon = 1");
4163 assert_eq!((raw >> 5) & 0x1F, 14, "bit_rate_code = 14 (= 384 kbps)");
4164 assert_eq!(raw & 0x1F, 0, "reserved 5 bits = 0");
4165 }
4166
4167 #[test]
4171 fn ac3_sample_entry_size_and_fourcc() {
4172 let info = ac3_info_5_1_384k();
4173 let entry = build_ac3_sample_entry(&info);
4174 let size = u32::from_be_bytes([entry[0], entry[1], entry[2], entry[3]]) as usize;
4175 assert_eq!(size, entry.len(), "size field equals box length");
4176 assert_eq!(&entry[4..8], b"ac-3", "4cc MUST be 'ac-3' (with hyphen)");
4177 assert_ne!(
4179 &entry[4..8],
4180 b"ac3\0",
4181 "4cc 'ac3' (3-char) is non-conformant"
4182 );
4183 assert_eq!(
4184 entry.len(),
4185 47,
4186 "ac-3 sample entry = 36 (preamble) + 11 (dac3)"
4187 );
4188 let dac3_pos = entry
4190 .windows(4)
4191 .position(|w| w == b"dac3")
4192 .expect("dac3 child missing");
4193 assert!(
4194 dac3_pos > 28,
4195 "dac3 must come after AudioSampleEntry preamble"
4196 );
4197 let sr_q16 = u32::from_be_bytes([entry[32], entry[33], entry[34], entry[35]]);
4199 assert_eq!(sr_q16, 48_000u32 << 16, "samplerate = 48000 << 16 (Q16)");
4200 }
4201
4202 #[test]
4207 fn dec3_box_5_byte_payload_layout() {
4208 let info = eac3_info_5_1_384k();
4209 let dec3 = build_dec3(&info);
4210 assert_eq!(dec3.len(), 13, "dec3 = 8-byte header + 5-byte body");
4211 let size = u32::from_be_bytes([dec3[0], dec3[1], dec3[2], dec3[3]]) as usize;
4212 assert_eq!(size, dec3.len(), "size field equals box length");
4213 assert_eq!(&dec3[4..8], b"dec3", "box type 'dec3'");
4214 let header = ((dec3[8] as u16) << 8) | dec3[9] as u16;
4216 let data_rate = (header >> 3) & 0x1FFF;
4217 assert_eq!(data_rate, 192, "data_rate = 192 (= 384 kbps / 2)");
4218 let num_ind_sub_minus_1 = header & 0x07;
4219 assert_eq!(num_ind_sub_minus_1, 0, "single substream → field = 0");
4220 let sub = ((dec3[10] as u32) << 16) | ((dec3[11] as u32) << 8) | dec3[12] as u32;
4233 assert_eq!((sub >> 22) & 0x03, 0, "fscod = 0 (48 kHz)");
4234 assert_eq!((sub >> 17) & 0x1F, 16, "bsid = 16 (E-AC-3 marker)");
4235 assert_eq!((sub >> 12) & 0x07, 0, "bsmod = 0");
4236 assert_eq!((sub >> 9) & 0x07, 7, "acmod = 7 (3/2 = 5.1 with LFE)");
4237 assert_eq!((sub >> 8) & 0x01, 1, "lfeon = 1");
4238 assert_eq!((sub >> 1) & 0x0F, 0, "num_dep_sub = 0 (single substream)");
4239 }
4240
4241 #[test]
4244 fn ec3_sample_entry_size_and_fourcc() {
4245 let info = eac3_info_5_1_384k();
4246 let entry = build_ec3_sample_entry(&info);
4247 let size = u32::from_be_bytes([entry[0], entry[1], entry[2], entry[3]]) as usize;
4248 assert_eq!(size, entry.len(), "size field equals box length");
4249 assert_eq!(&entry[4..8], b"ec-3", "4cc MUST be 'ec-3' (with hyphen)");
4250 assert_eq!(
4251 entry.len(),
4252 49,
4253 "ec-3 sample entry = 36 (preamble) + 13 (dec3)"
4254 );
4255 let dec3_pos = entry
4256 .windows(4)
4257 .position(|w| w == b"dec3")
4258 .expect("dec3 child missing");
4259 assert!(
4260 dec3_pos > 28,
4261 "dec3 must come after AudioSampleEntry preamble"
4262 );
4263 }
4264
4265 #[test]
4268 fn stsd_dispatcher_routes_ac3_eac3() {
4269 let stsd_ac3 = build_audio_stsd(&ac3_info_5_1_384k());
4270 assert!(
4271 stsd_ac3.windows(4).any(|w| w == b"ac-3"),
4272 "AC-3 stsd has 'ac-3'"
4273 );
4274 assert!(
4275 stsd_ac3.windows(4).any(|w| w == b"dac3"),
4276 "AC-3 stsd has 'dac3'"
4277 );
4278 assert!(
4279 !stsd_ac3.windows(4).any(|w| w == b"mp4a"),
4280 "AC-3 stsd MUST NOT have mp4a"
4281 );
4282 assert!(
4283 !stsd_ac3.windows(4).any(|w| w == b"Opus"),
4284 "AC-3 stsd MUST NOT have Opus"
4285 );
4286 assert!(
4287 !stsd_ac3.windows(4).any(|w| w == b"esds"),
4288 "AC-3 stsd MUST NOT have esds"
4289 );
4290
4291 let stsd_eac3 = build_audio_stsd(&eac3_info_5_1_384k());
4292 assert!(
4293 stsd_eac3.windows(4).any(|w| w == b"ec-3"),
4294 "E-AC-3 stsd has 'ec-3'"
4295 );
4296 assert!(
4297 stsd_eac3.windows(4).any(|w| w == b"dec3"),
4298 "E-AC-3 stsd has 'dec3'"
4299 );
4300 assert!(
4301 !stsd_eac3.windows(4).any(|w| w == b"mp4a"),
4302 "E-AC-3 stsd MUST NOT have mp4a"
4303 );
4304 assert!(
4305 !stsd_eac3.windows(4).any(|w| w == b"esds"),
4306 "E-AC-3 stsd MUST NOT have esds"
4307 );
4308 assert!(
4309 !stsd_eac3.windows(4).any(|w| w == b"dac3"),
4310 "E-AC-3 stsd MUST NOT have dac3"
4311 );
4312 }
4313
4314 #[test]
4317 fn with_audio_accepts_ac3_5_1_and_rejects_bad_shape() {
4318 let mut muxer = Av1Mp4Muxer::new(320, 240, 30.0).unwrap();
4319 muxer
4320 .with_audio(ac3_info_5_1_384k())
4321 .expect("5.1 AC-3 must be accepted");
4322
4323 let mut muxer2 = Av1Mp4Muxer::new(320, 240, 30.0).unwrap();
4325 let mut bad = ac3_info_5_1_384k();
4326 bad.codec_private = vec![0u8; 2];
4327 let err = muxer2
4328 .with_audio(bad)
4329 .err()
4330 .expect("must reject 2-byte dac3");
4331 assert!(format!("{err:#}").contains("3 bytes"));
4332
4333 let mut muxer3 = Av1Mp4Muxer::new(320, 240, 30.0).unwrap();
4335 let bad_sr = AudioInfo {
4336 sample_rate: 22_050,
4337 timescale: 22_050,
4338 ..ac3_info_5_1_384k()
4339 };
4340 let err = muxer3
4341 .with_audio(bad_sr)
4342 .err()
4343 .expect("must reject 22050 for AC-3");
4344 assert!(format!("{err:#}").contains("32000"));
4345 }
4346
4347 #[test]
4350 fn with_audio_accepts_eac3_5_1_and_rejects_short_dec3() {
4351 let mut muxer = Av1Mp4Muxer::new(320, 240, 30.0).unwrap();
4352 muxer
4353 .with_audio(eac3_info_5_1_384k())
4354 .expect("5.1 E-AC-3 must be accepted");
4355
4356 let mut muxer2 = Av1Mp4Muxer::new(320, 240, 30.0).unwrap();
4357 let mut bad = eac3_info_5_1_384k();
4358 bad.codec_private = vec![0u8; 4];
4359 let err = muxer2
4360 .with_audio(bad)
4361 .err()
4362 .expect("must reject short dec3");
4363 assert!(format!("{err:#}").contains("≥5"));
4364 }
4365
4366 #[test]
4368 fn with_audio_rejects_ac3_more_than_6_channels() {
4369 let mut muxer = Av1Mp4Muxer::new(320, 240, 30.0).unwrap();
4370 let bad = AudioInfo {
4371 channels: 8,
4372 ..ac3_info_5_1_384k()
4373 };
4374 let err = muxer.with_audio(bad).err().expect("must reject 8 channels");
4375 assert!(format!("{err:#}").contains("1..=6"));
4376 }
4377
4378 #[test]
4382 fn ac3_sync_to_dac3_to_sample_entry_roundtrip() {
4383 let sync = ac3_sync_5_1_384k_48k();
4384 let body = dac3_body_from_sync(&sync);
4385 let info = AudioInfo::ac3(48_000, 6, body.to_vec());
4386 let entry = build_ac3_sample_entry(&info);
4387 let dac3_pos = entry.windows(4).position(|w| w == b"dac3").unwrap();
4390 let dac3_body_start = dac3_pos + 4;
4391 let raw = ((entry[dac3_body_start] as u32) << 16)
4392 | ((entry[dac3_body_start + 1] as u32) << 8)
4393 | entry[dac3_body_start + 2] as u32;
4394 assert_eq!((raw >> 22) & 0x03, sync.fscod as u32);
4395 assert_eq!((raw >> 17) & 0x1F, sync.bsid as u32);
4396 assert_eq!((raw >> 11) & 0x07, sync.acmod as u32);
4397 assert_eq!((raw >> 10) & 0x01, sync.lfeon as u32);
4398 assert_eq!((raw >> 5) & 0x1F, sync.bit_rate_code as u32);
4399 }
4400}