1use super::Muxer;
40use crate::{CodecId, MediaFrame, Result, StreamError};
41use bytes::Bytes;
42
43const TIMESCALE: u32 = 1000; const TRACK_ID: u32 = 1;
45const FALLBACK_DURATION: u64 = TIMESCALE as u64 / 30; struct Sample {
49 data: Bytes,
50 dts: i64,
51 cts: i32, is_key: bool,
53 duration: Option<u64>,
54}
55
56pub struct Fmp4Muxer {
66 codec: Option<CodecId>,
67 width: u16,
68 height: u16,
69 codec_str: Option<String>,
70 seq: u32,
71 base_decode_time: u64,
72 samples: Vec<Sample>,
73}
74
75impl Default for Fmp4Muxer {
76 fn default() -> Self {
77 Self::new()
78 }
79}
80
81impl Fmp4Muxer {
82 pub fn new() -> Self {
85 Self {
86 codec: None,
87 width: 0,
88 height: 0,
89 codec_str: None,
90 seq: 1,
91 base_decode_time: 0,
92 samples: Vec::new(),
93 }
94 }
95
96 fn ingest_h264_config(&mut self, config_record: &[u8]) -> Result<Vec<u8>> {
99 let mut sps: Option<&[u8]> = None;
100 let mut pps: Option<&[u8]> = None;
101 for nal in crate::codec::h264::iter_nals_annexb(config_record) {
102 match nal.first().map(|b| b & 0x1f) {
103 Some(t) if t == crate::codec::h264::NAL_SPS => sps = Some(nal),
104 Some(t) if t == crate::codec::h264::NAL_PPS => pps = Some(nal),
105 _ => {}
106 }
107 }
108 let sps = sps.ok_or_else(|| StreamError::codec("fmp4: no SPS in H.264 config"))?;
109 let pps = pps.ok_or_else(|| StreamError::codec("fmp4: no PPS in H.264 config"))?;
110 if sps.len() < 4 {
111 return Err(StreamError::codec("fmp4: truncated SPS"));
112 }
113 let info = crate::codec::h264::parse_sps(sps)
114 .ok_or_else(|| StreamError::codec("fmp4: unparseable SPS"))?;
115 self.width = info.width as u16;
116 self.height = info.height as u16;
117 self.codec_str = Some(format!("avc1.{:02X}{:02X}{:02X}", sps[1], sps[2], sps[3]));
119
120 let mut c = Vec::with_capacity(16 + sps.len() + pps.len());
122 c.push(1); c.push(sps[1]); c.push(sps[2]); c.push(sps[3]); c.push(0xFF); c.push(0xE1); c.extend_from_slice(&(sps.len() as u16).to_be_bytes());
129 c.extend_from_slice(sps);
130 c.push(1); c.extend_from_slice(&(pps.len() as u16).to_be_bytes());
132 c.extend_from_slice(pps);
133 Ok(build_avc1(self.width, self.height, &c))
134 }
135
136 #[cfg(feature = "codec-h265")]
139 fn ingest_h265_config(&mut self, config_record: &[u8]) -> Result<Vec<u8>> {
140 use crate::codec::{h265::H265, CodecParser};
141 let (params, hvcc) = crate::codec::h265::hvcc_config_record(config_record)
142 .ok_or_else(|| StreamError::codec("fmp4: no parsable SPS in H.265 config"))?;
143 self.width = params.width as u16;
144 self.height = params.height as u16;
145 self.codec_str = Some(H265::hls_codec_string(¶ms));
146 Ok(build_hvc1(self.width, self.height, &hvcc))
147 }
148
149 #[cfg(feature = "codec-av1")]
152 fn ingest_av1_config(&mut self, config_record: &[u8]) -> Result<Vec<u8>> {
153 use crate::codec::{av1::Av1, CodecParser};
154 let (params, av1c) = crate::codec::av1::av1c_config_record(config_record)
155 .ok_or_else(|| StreamError::codec("fmp4: no AV1 sequence header in config"))?;
156 self.width = params.width as u16;
157 self.height = params.height as u16;
158 self.codec_str = Some(Av1::hls_codec_string(¶ms));
159 Ok(build_av01(self.width, self.height, &av1c))
160 }
161
162 #[cfg(feature = "codec-vvc")]
165 fn ingest_vvc_config(&mut self, config_record: &[u8]) -> Result<Vec<u8>> {
166 use crate::codec::{vvc::Vvc, CodecParser};
167 let (params, vvcc) =
168 crate::codec::vvc::vvcc_config_record(config_record).ok_or_else(|| {
169 StreamError::codec(
170 "fmp4: VVC config needs an SPS without a PTL block (parser limit)",
171 )
172 })?;
173 self.width = params.width as u16;
174 self.height = params.height as u16;
175 self.codec_str = Some(Vvc::hls_codec_string(¶ms));
176 Ok(build_vvc1(self.width, self.height, &vvcc))
177 }
178
179 fn durations(&self) -> Vec<u64> {
182 let n = self.samples.len();
183 let mut out = Vec::with_capacity(n);
184 for i in 0..n {
185 let dur = if i + 1 < n {
186 (self.samples[i + 1].dts - self.samples[i].dts).max(0) as u64
187 } else {
188 self.samples[i]
189 .duration
190 .or_else(|| out.last().copied())
191 .unwrap_or(FALLBACK_DURATION)
192 };
193 out.push(dur);
194 }
195 out
196 }
197}
198
199impl Muxer for Fmp4Muxer {
200 fn extension(&self) -> &'static str {
201 "m4s"
202 }
203
204 fn start_segment(&mut self) -> Result<()> {
205 self.samples.clear();
206 Ok(())
207 }
208
209 fn write(&mut self, frame: &MediaFrame) -> Result<()> {
210 if !frame.is_video() {
212 return Ok(());
213 }
214 let is_nal = matches!(
218 self.codec,
219 Some(CodecId::H264) | Some(CodecId::H265) | Some(CodecId::VVC)
220 );
221 let data = if is_nal {
222 crate::codec::h264::annexb_to_avcc(&frame.data) } else {
224 frame.data.clone()
225 };
226 if data.is_empty() {
227 return Ok(());
228 }
229 self.samples.push(Sample {
230 data,
231 dts: frame.dts,
232 cts: (frame.pts - frame.dts) as i32,
233 is_key: frame.is_keyframe(),
234 duration: frame.duration,
235 });
236 Ok(())
237 }
238
239 fn finish_segment(&mut self) -> Result<Bytes> {
240 if self.samples.is_empty() {
241 return Ok(Bytes::new());
242 }
243 let durations = self.durations();
244 let total: u64 = durations.iter().sum();
245
246 let moof = build_moof(self.seq, self.base_decode_time, &self.samples, &durations);
247 let mut out = moof;
248 let mdat_len: usize = self.samples.iter().map(|s| s.data.len()).sum();
250 out.extend_from_slice(&((mdat_len + 8) as u32).to_be_bytes());
251 out.extend_from_slice(b"mdat");
252 for s in &self.samples {
253 out.extend_from_slice(&s.data);
254 }
255
256 self.seq += 1;
257 self.base_decode_time += total;
258 self.samples.clear();
259 Ok(Bytes::from(out))
260 }
261
262 fn init_segment(&mut self, codec: CodecId, config_record: &[u8]) -> Result<Option<Bytes>> {
263 let sample_entry = match codec {
264 CodecId::H264 => self.ingest_h264_config(config_record)?,
265 #[cfg(feature = "codec-h265")]
266 CodecId::H265 => self.ingest_h265_config(config_record)?,
267 #[cfg(feature = "codec-av1")]
268 CodecId::AV1 => self.ingest_av1_config(config_record)?,
269 #[cfg(feature = "codec-vvc")]
270 CodecId::VVC => self.ingest_vvc_config(config_record)?,
271 _ => {
272 return Err(StreamError::UnsupportedCodec(format!(
273 "fmp4 init segment: {codec:?} not supported in this build"
274 )))
275 }
276 };
277 self.codec = Some(codec);
278 let mut seg = build_ftyp();
279 seg.extend_from_slice(&build_moov(self.width, self.height, &sample_entry));
280 Ok(Some(Bytes::from(seg)))
281 }
282
283 fn codec_string(&self) -> Option<String> {
284 self.codec_str.clone()
285 }
286}
287
288fn bx(typ: &[u8; 4], body: &[u8]) -> Vec<u8> {
292 let mut v = Vec::with_capacity(8 + body.len());
293 v.extend_from_slice(&((body.len() + 8) as u32).to_be_bytes());
294 v.extend_from_slice(typ);
295 v.extend_from_slice(body);
296 v
297}
298
299fn full(typ: &[u8; 4], version: u8, flags: u32, body: &[u8]) -> Vec<u8> {
301 let mut b = Vec::with_capacity(4 + body.len());
302 b.extend_from_slice(&(((version as u32) << 24) | (flags & 0x00FF_FFFF)).to_be_bytes());
303 b.extend_from_slice(body);
304 bx(typ, &b)
305}
306
307const MATRIX_IDENTITY: [u32; 9] = [0x0001_0000, 0, 0, 0, 0x0001_0000, 0, 0, 0, 0x4000_0000];
308
309fn put_matrix(v: &mut Vec<u8>) {
310 for m in MATRIX_IDENTITY {
311 v.extend_from_slice(&m.to_be_bytes());
312 }
313}
314
315fn build_ftyp() -> Vec<u8> {
316 let mut b = Vec::new();
317 b.extend_from_slice(b"iso5"); b.extend_from_slice(&0u32.to_be_bytes()); for brand in [b"iso5", b"iso6", b"mp41"] {
320 b.extend_from_slice(brand);
321 }
322 bx(b"ftyp", &b)
323}
324
325fn build_moov(width: u16, height: u16, entry: &[u8]) -> Vec<u8> {
327 let mut body = Vec::new();
328 body.extend_from_slice(&build_mvhd());
329 body.extend_from_slice(&build_trak(width, height, entry));
330 body.extend_from_slice(&build_mvex());
331 bx(b"moov", &body)
332}
333
334fn build_mvhd() -> Vec<u8> {
335 let mut b = Vec::new();
336 b.extend_from_slice(&0u32.to_be_bytes()); b.extend_from_slice(&0u32.to_be_bytes()); b.extend_from_slice(&TIMESCALE.to_be_bytes());
339 b.extend_from_slice(&0u32.to_be_bytes()); b.extend_from_slice(&0x0001_0000u32.to_be_bytes()); b.extend_from_slice(&0x0100u16.to_be_bytes()); b.extend_from_slice(&0u16.to_be_bytes()); b.extend_from_slice(&[0u8; 8]); put_matrix(&mut b);
345 b.extend_from_slice(&[0u8; 24]); b.extend_from_slice(&2u32.to_be_bytes()); full(b"mvhd", 0, 0, &b)
348}
349
350fn build_trak(width: u16, height: u16, entry: &[u8]) -> Vec<u8> {
351 let mut b = Vec::new();
352 b.extend_from_slice(&build_tkhd(width, height));
353 b.extend_from_slice(&build_mdia(entry));
354 bx(b"trak", &b)
355}
356
357fn build_tkhd(width: u16, height: u16) -> Vec<u8> {
358 let mut b = Vec::new();
359 b.extend_from_slice(&0u32.to_be_bytes()); b.extend_from_slice(&0u32.to_be_bytes()); b.extend_from_slice(&TRACK_ID.to_be_bytes());
362 b.extend_from_slice(&0u32.to_be_bytes()); b.extend_from_slice(&0u32.to_be_bytes()); b.extend_from_slice(&[0u8; 8]); b.extend_from_slice(&0u16.to_be_bytes()); b.extend_from_slice(&0u16.to_be_bytes()); b.extend_from_slice(&0u16.to_be_bytes()); b.extend_from_slice(&0u16.to_be_bytes()); put_matrix(&mut b);
370 b.extend_from_slice(&((width as u32) << 16).to_be_bytes()); b.extend_from_slice(&((height as u32) << 16).to_be_bytes()); full(b"tkhd", 0, 0x0000_0007, &b) }
374
375fn build_mdia(entry: &[u8]) -> Vec<u8> {
376 let mut b = Vec::new();
377 b.extend_from_slice(&build_mdhd());
378 b.extend_from_slice(&build_hdlr());
379 b.extend_from_slice(&build_minf(entry));
380 bx(b"mdia", &b)
381}
382
383fn build_mdhd() -> Vec<u8> {
384 let mut b = Vec::new();
385 b.extend_from_slice(&0u32.to_be_bytes()); b.extend_from_slice(&0u32.to_be_bytes()); b.extend_from_slice(&TIMESCALE.to_be_bytes());
388 b.extend_from_slice(&0u32.to_be_bytes()); b.extend_from_slice(&0x55C4u16.to_be_bytes()); b.extend_from_slice(&0u16.to_be_bytes()); full(b"mdhd", 0, 0, &b)
392}
393
394fn build_hdlr() -> Vec<u8> {
395 let mut b = Vec::new();
396 b.extend_from_slice(&0u32.to_be_bytes()); b.extend_from_slice(b"vide"); b.extend_from_slice(&[0u8; 12]); b.extend_from_slice(b"VideoHandler\0");
400 full(b"hdlr", 0, 0, &b)
401}
402
403fn build_minf(entry: &[u8]) -> Vec<u8> {
404 let mut b = Vec::new();
405 b.extend_from_slice(&full(b"vmhd", 0, 1, &[0u8; 8])); b.extend_from_slice(&build_dinf());
407 b.extend_from_slice(&build_stbl(entry));
408 bx(b"minf", &b)
409}
410
411fn build_dinf() -> Vec<u8> {
412 let url = full(b"url ", 0, 1, &[]);
414 let mut dref_body = Vec::new();
415 dref_body.extend_from_slice(&1u32.to_be_bytes()); dref_body.extend_from_slice(&url);
417 let dref = full(b"dref", 0, 0, &dref_body);
418 bx(b"dinf", &dref)
419}
420
421fn build_stbl(entry: &[u8]) -> Vec<u8> {
422 let mut b = Vec::new();
423 b.extend_from_slice(&build_stsd(entry));
424 b.extend_from_slice(&full(b"stts", 0, 0, &0u32.to_be_bytes())); b.extend_from_slice(&full(b"stsc", 0, 0, &0u32.to_be_bytes()));
426 let mut stsz = Vec::new();
427 stsz.extend_from_slice(&0u32.to_be_bytes()); stsz.extend_from_slice(&0u32.to_be_bytes()); b.extend_from_slice(&full(b"stsz", 0, 0, &stsz));
430 b.extend_from_slice(&full(b"stco", 0, 0, &0u32.to_be_bytes()));
431 bx(b"stbl", &b)
432}
433
434fn build_stsd(entry: &[u8]) -> Vec<u8> {
435 let mut b = Vec::new();
436 b.extend_from_slice(&1u32.to_be_bytes()); b.extend_from_slice(entry);
438 full(b"stsd", 0, 0, &b)
439}
440
441fn visual_sample_entry(typ: &[u8; 4], width: u16, height: u16, config_box: &[u8]) -> Vec<u8> {
444 let mut b = Vec::new();
445 b.extend_from_slice(&[0u8; 6]); b.extend_from_slice(&1u16.to_be_bytes()); b.extend_from_slice(&0u16.to_be_bytes()); b.extend_from_slice(&0u16.to_be_bytes()); b.extend_from_slice(&[0u8; 12]); b.extend_from_slice(&width.to_be_bytes());
451 b.extend_from_slice(&height.to_be_bytes());
452 b.extend_from_slice(&0x0048_0000u32.to_be_bytes()); b.extend_from_slice(&0x0048_0000u32.to_be_bytes()); b.extend_from_slice(&0u32.to_be_bytes()); b.extend_from_slice(&1u16.to_be_bytes()); b.extend_from_slice(&[0u8; 32]); b.extend_from_slice(&0x0018u16.to_be_bytes()); b.extend_from_slice(&0xFFFFu16.to_be_bytes()); b.extend_from_slice(config_box);
460 bx(typ, &b)
461}
462
463fn build_avc1(width: u16, height: u16, avcc: &[u8]) -> Vec<u8> {
465 visual_sample_entry(b"avc1", width, height, &bx(b"avcC", avcc))
466}
467
468#[cfg(feature = "codec-av1")]
470fn build_av01(width: u16, height: u16, av1c: &[u8]) -> Vec<u8> {
471 visual_sample_entry(b"av01", width, height, &bx(b"av1C", av1c))
472}
473
474#[cfg(feature = "codec-vvc")]
476fn build_vvc1(width: u16, height: u16, vvcc: &[u8]) -> Vec<u8> {
477 visual_sample_entry(b"vvc1", width, height, &bx(b"vvcC", vvcc))
478}
479
480#[cfg(feature = "codec-h265")]
482fn build_hvc1(width: u16, height: u16, hvcc: &[u8]) -> Vec<u8> {
483 visual_sample_entry(b"hvc1", width, height, &bx(b"hvcC", hvcc))
484}
485
486fn build_mvex() -> Vec<u8> {
487 let mut trex = Vec::new();
488 trex.extend_from_slice(&TRACK_ID.to_be_bytes());
489 trex.extend_from_slice(&1u32.to_be_bytes()); trex.extend_from_slice(&0u32.to_be_bytes()); trex.extend_from_slice(&0u32.to_be_bytes()); trex.extend_from_slice(&0u32.to_be_bytes()); bx(b"mvex", &full(b"trex", 0, 0, &trex))
494}
495
496fn sample_flags(is_key: bool) -> u32 {
498 if is_key {
499 0x0200_0000 } else {
501 0x0101_0000 }
503}
504
505fn build_moof(seq: u32, base_decode_time: u64, samples: &[Sample], durations: &[u64]) -> Vec<u8> {
506 let mfhd = full(b"mfhd", 0, 0, &seq.to_be_bytes());
508
509 let tfhd = full(b"tfhd", 0, 0x0002_0000, &TRACK_ID.to_be_bytes());
511
512 let tfdt = full(b"tfdt", 1, 0, &base_decode_time.to_be_bytes());
514
515 let trun_flags: u32 = 0x0001 | 0x0100 | 0x0200 | 0x0400 | 0x0800;
517 let mut trun_body = Vec::new();
518 trun_body.extend_from_slice(&(samples.len() as u32).to_be_bytes()); let data_offset_pos_in_body = trun_body.len();
520 trun_body.extend_from_slice(&0i32.to_be_bytes()); for (s, &dur) in samples.iter().zip(durations) {
522 trun_body.extend_from_slice(&(dur as u32).to_be_bytes());
523 trun_body.extend_from_slice(&(s.data.len() as u32).to_be_bytes());
524 trun_body.extend_from_slice(&sample_flags(s.is_key).to_be_bytes());
525 trun_body.extend_from_slice(&s.cts.to_be_bytes());
526 }
527 let trun = full(b"trun", 1, trun_flags, &trun_body);
528
529 let mut traf_body = Vec::new();
531 traf_body.extend_from_slice(&tfhd);
532 traf_body.extend_from_slice(&tfdt);
533 let trun_pos_in_traf_body = traf_body.len();
534 traf_body.extend_from_slice(&trun);
535 let traf = bx(b"traf", &traf_body);
536
537 let mut moof_body = Vec::new();
539 moof_body.extend_from_slice(&mfhd);
540 let traf_pos_in_moof_body = moof_body.len();
541 moof_body.extend_from_slice(&traf);
542 let mut moof = bx(b"moof", &moof_body);
543
544 let off = 8 + traf_pos_in_moof_body + 8 + trun_pos_in_traf_body + 12 + data_offset_pos_in_body;
550 let data_offset = (moof.len() + 8) as i32;
551 moof[off..off + 4].copy_from_slice(&data_offset.to_be_bytes());
552 moof
553}
554
555#[cfg(test)]
556mod tests {
557 use super::*;
558 use crate::{CodecId, FrameFlags, MediaFrame};
559 use bytes::Bytes;
560
561 fn h264_config() -> Vec<u8> {
563 let sps = [0x67u8, 0x42, 0x00, 0x1F, 0xF4, 0x02, 0x80, 0x2D, 0x80];
564 let pps = [0x68u8, 0xCE, 0x3C, 0x80];
565 let mut v = vec![0, 0, 0, 1];
566 v.extend_from_slice(&sps);
567 v.extend_from_slice(&[0, 0, 0, 1]);
568 v.extend_from_slice(&pps);
569 v
570 }
571
572 fn annexb_frame(pts: i64, is_key: bool) -> MediaFrame {
573 let nal_type = if is_key { 0x65 } else { 0x41 }; let data = Bytes::from(vec![0, 0, 0, 1, nal_type, 0xAA, 0xBB]);
575 MediaFrame::new_video(pts, pts, data, CodecId::H264, is_key)
576 }
577
578 fn box_types(buf: &[u8]) -> Vec<String> {
580 let mut types = Vec::new();
581 let mut i = 0;
582 while i + 8 <= buf.len() {
583 let size = u32::from_be_bytes(buf[i..i + 4].try_into().unwrap()) as usize;
584 let typ = String::from_utf8_lossy(&buf[i + 4..i + 8]).to_string();
585 types.push(typ);
586 assert!(size >= 8 && i + size <= buf.len(), "box size out of range");
587 i += size;
588 }
589 assert_eq!(i, buf.len(), "boxes must tile the buffer exactly");
590 types
591 }
592
593 #[test]
594 fn init_segment_has_ftyp_moov_and_codec_string() {
595 let mut m = Fmp4Muxer::new();
596 let init = m
597 .init_segment(CodecId::H264, &h264_config())
598 .unwrap()
599 .expect("init segment");
600 assert_eq!(box_types(&init), vec!["ftyp", "moov"]);
601 assert!(find(&init, b"avc1"));
603 assert!(find(&init, b"avcC"));
604 assert!(find(&init, b"mvex"));
605 assert_eq!(m.codec_string().as_deref(), Some("avc1.42001F"));
606 }
607
608 #[test]
609 fn unsupported_codec_init_is_rejected() {
610 let mut m = Fmp4Muxer::new();
611 assert!(m.init_segment(CodecId::VP9, &[0, 0]).is_err());
613 }
614
615 #[cfg(feature = "codec-h265")]
616 #[test]
617 fn h265_init_segment_has_hvc1_and_hvcc() {
618 use crate::codec::testutil::BitWriter;
619
620 let mut w = BitWriter::default();
623 w.bits(0, 4); w.bits(0, 3); w.bit(0); w.bits(0, 2); w.bit(0); w.bits(1, 5); w.bits(0, 32); w.bits(0, 32); w.bits(0, 16); w.bits(120, 8); w.ue(0); w.ue(1); w.ue(1920); w.ue(1080); w.bit(0); let mut sps = vec![0x42u8, 0x01];
639 sps.extend_from_slice(&w.bytes());
640
641 let mut config = vec![0, 0, 0, 1, 0x40, 0x01, 0xAA]; config.extend_from_slice(&[0, 0, 0, 1]);
643 config.extend_from_slice(&sps);
644 config.extend_from_slice(&[0, 0, 0, 1, 0x44, 0x01, 0xBB]); let mut m = Fmp4Muxer::new();
647 let init = m
648 .init_segment(CodecId::H265, &config)
649 .unwrap()
650 .expect("init segment");
651 assert_eq!(box_types(&init), vec!["ftyp", "moov"]);
652 assert!(find(&init, b"hvc1"), "hvc1 sample entry present");
653 assert!(find(&init, b"hvcC"), "hvcC decoder config present");
654 assert!(
655 m.codec_string()
656 .as_deref()
657 .is_some_and(|s| s.starts_with("hvc1.")),
658 "HEVC codec string"
659 );
660 }
661
662 #[cfg(feature = "codec-av1")]
663 #[test]
664 fn av1_init_segment_has_av01_and_av1c() {
665 use crate::codec::testutil::BitWriter;
666
667 let mut w = BitWriter::default();
670 w.bits(0, 3); w.bit(0); w.bit(0); w.bit(0); w.bit(0); w.bits(0, 5); w.bits(0, 12); w.bits(1, 5); w.bits(11, 4); w.bits(11, 4); w.bits(1919, 12);
681 w.bits(1079, 12);
682 w.align();
683 let payload = w.bytes();
684 let mut config = vec![0x0A, payload.len() as u8];
685 config.extend_from_slice(&payload);
686
687 let mut m = Fmp4Muxer::new();
688 let init = m
689 .init_segment(CodecId::AV1, &config)
690 .unwrap()
691 .expect("av1 init segment");
692 assert_eq!(box_types(&init), vec!["ftyp", "moov"]);
693 assert!(find(&init, b"av01"), "av01 sample entry");
694 assert!(find(&init, b"av1C"), "av1C config box");
695 assert_eq!(m.codec_string().as_deref(), Some("av01.0.01M.08"));
696
697 m.start_segment().unwrap();
699 let mut frame = MediaFrame::new_video(
700 0,
701 0,
702 Bytes::from(vec![0x32, 0x02, 0xAA, 0xBB]), CodecId::AV1,
704 true,
705 );
706 frame.duration = Some(33);
707 m.write(&frame).unwrap();
708 let frag = m.finish_segment().unwrap();
709 assert_eq!(box_types(&frag), vec!["moof", "mdat"]);
710 assert!(find(&frag, &[0x32, 0x02, 0xAA, 0xBB]));
712 }
713
714 #[test]
715 fn fragment_has_moof_mdat_and_correct_sample_count() {
716 let mut m = Fmp4Muxer::new();
717 m.init_segment(CodecId::H264, &h264_config()).unwrap();
718 m.start_segment().unwrap();
719 m.write(&annexb_frame(0, true)).unwrap();
720 m.write(&annexb_frame(40, false)).unwrap();
721 m.write(&annexb_frame(80, false)).unwrap();
722 let audio = MediaFrame::new_audio(80, Bytes::from_static(b"aac"), CodecId::AAC);
724 m.write(&audio).unwrap();
725
726 let frag = m.finish_segment().unwrap();
727 assert_eq!(box_types(&frag), vec!["moof", "mdat"]);
728 assert!(find(&frag, b"tfdt"));
729 assert!(find(&frag, b"trun"));
730
731 let trun_at = position(&frag, b"trun").expect("trun present");
733 let count = u32::from_be_bytes(frag[trun_at + 8..trun_at + 12].try_into().unwrap());
734 assert_eq!(count, 3, "three video samples, audio skipped");
735 }
736
737 #[test]
738 fn base_decode_time_advances_across_fragments() {
739 let mut m = Fmp4Muxer::new();
740 m.init_segment(CodecId::H264, &h264_config()).unwrap();
741
742 m.start_segment().unwrap();
743 m.write(&annexb_frame(0, true)).unwrap();
744 m.write(&annexb_frame(40, false)).unwrap();
745 let _ = m.finish_segment().unwrap();
746 assert_eq!(m.base_decode_time, 80);
749
750 m.start_segment().unwrap();
751 m.write(&annexb_frame(80, true)).unwrap();
752 let frag2 = m.finish_segment().unwrap();
753 let tfdt_at = position(&frag2, b"tfdt").unwrap();
755 let base = u64::from_be_bytes(frag2[tfdt_at + 8..tfdt_at + 16].try_into().unwrap());
756 assert_eq!(base, 80);
757 }
758
759 #[cfg(feature = "codec-vvc")]
760 #[test]
761 fn vvc_init_segment_has_vvc1_and_vvcc() {
762 use crate::codec::testutil::BitWriter;
763
764 let mut w = BitWriter::default();
766 w.bits(0, 4); w.bits(0, 4); w.bits(0, 3); w.bits(1, 2); w.bits(0, 2); w.bit(0); w.bit(0); w.bit(0); w.ue(1920);
775 w.ue(1080);
776 w.bit(0); let mut sps = vec![0x00u8, 0x79]; sps.extend_from_slice(&w.bytes());
779
780 let mut config = vec![0, 0, 0, 1];
781 config.extend_from_slice(&sps);
782
783 let mut m = Fmp4Muxer::new();
784 let init = m
785 .init_segment(CodecId::VVC, &config)
786 .unwrap()
787 .expect("vvc init segment");
788 assert_eq!(box_types(&init), vec!["ftyp", "moov"]);
789 assert!(find(&init, b"vvc1"), "vvc1 sample entry");
790 assert!(find(&init, b"vvcC"), "vvcC config box");
791 assert_eq!(m.codec_string().as_deref(), Some("vvc1.0.L0"));
792
793 m.start_segment().unwrap();
795 let mut frame = MediaFrame::new_video(
796 0,
797 0,
798 Bytes::from(vec![0, 0, 0, 1, 0x00, 0x39, 0xAA]), CodecId::VVC,
800 true,
801 );
802 frame.duration = Some(40);
803 m.write(&frame).unwrap();
804 let frag = m.finish_segment().unwrap();
805 assert_eq!(box_types(&frag), vec!["moof", "mdat"]);
806 assert!(find(&frag, &[0x00, 0x00, 0x00, 0x03, 0x00, 0x39, 0xAA]));
808 }
809
810 #[test]
811 fn config_without_sps_errors() {
812 let mut m = Fmp4Muxer::new();
813 let cfg = vec![0, 0, 0, 1, 0x68, 0xCE, 0x3C, 0x80];
815 assert!(m.init_segment(CodecId::H264, &cfg).is_err());
816 }
817
818 #[test]
819 fn empty_fragment_yields_no_bytes() {
820 let mut m = Fmp4Muxer::new();
821 m.start_segment().unwrap();
822 assert!(m.finish_segment().unwrap().is_empty());
823 }
824
825 fn find(buf: &[u8], needle: &[u8]) -> bool {
826 position(buf, needle).is_some()
827 }
828 fn position(buf: &[u8], needle: &[u8]) -> Option<usize> {
829 buf.windows(needle.len()).position(|w| w == needle)
830 }
831
832 #[test]
833 fn integrates_with_segmenter_via_ext_x_map() {
834 use crate::packager::{HlsSegmenter, Packager};
835 use crate::testing::InMemoryStorage;
836 use crate::traits::StorageBackend;
837
838 tokio_test_block(async {
839 let store = InMemoryStorage::new();
840 let mut seg = HlsSegmenter::new(Fmp4Muxer::new(), store.clone(), "live/fmp4", 2, 5);
841
842 let mut cfg =
843 MediaFrame::new_video(0, 0, Bytes::from(h264_config()), CodecId::H264, true);
844 cfg.flags |= FrameFlags::CONFIG;
845 seg.push(&cfg).await.unwrap();
846 for i in 0..6 {
847 seg.push(&annexb_frame(i * 1000, true)).await.unwrap();
848 }
849 seg.finish().await.unwrap();
850
851 assert!(store.get("live/fmp4/init.m4s").await.is_ok());
852 let pl = String::from_utf8(store.get("live/fmp4/index.m3u8").await.unwrap().to_vec())
853 .unwrap();
854 assert!(pl.contains("#EXT-X-MAP:URI=\"init.m4s\""));
855 });
856 }
857
858 fn tokio_test_block<F: std::future::Future>(fut: F) -> F::Output {
859 tokio::runtime::Builder::new_current_thread()
860 .enable_all()
861 .build()
862 .unwrap()
863 .block_on(fut)
864 }
865}