1mod export;
10mod import;
11
12pub use export::*;
13pub use import::*;
14
15#[cfg(test)]
16mod export_test;
17#[cfg(test)]
18mod import_test;
19
20use std::task::Poll;
21
22use bytes::Bytes;
23use hang::catalog::{AudioCodec, AudioConfig, VideoCodec, VideoConfig};
24use mp4_atom::Atom;
25
26use crate::container::{Container, Frame, Timestamp};
27
28#[derive(Debug, Clone, thiserror::Error)]
29#[non_exhaustive]
30pub enum Error {
31 #[error("mp4: {0}")]
32 Mp4(std::sync::Arc<mp4_atom::Error>),
33
34 #[error("moq: {0}")]
35 Moq(#[from] moq_net::Error),
36
37 #[error("missing keyframe: a group must open on a keyframe")]
38 MissingKeyframe(#[from] crate::container::MissingKeyframe),
39
40 #[error("timestamp overflow")]
41 TimestampOverflow(#[from] moq_net::TimeOverflow),
42
43 #[error("no traf in moof")]
44 NoTraf,
45
46 #[error("no tfdt in traf")]
47 NoTfdt,
48
49 #[error("PTS overflow")]
50 PtsOverflow,
51
52 #[error("missing moof")]
53 NoMoof,
54
55 #[error("missing mdat")]
56 NoMdat,
57
58 #[error("missing moov")]
59 NoMoov,
60
61 #[error("no tracks in moov")]
62 NoTracks,
63
64 #[error("multiple tracks in moov, use Trak instead")]
65 MultipleTracks,
66
67 #[error("can't synthesize CMAF init for {0}")]
68 UnsupportedSynthesis(String),
69
70 #[error("subtitle tracks are not supported")]
71 UnsupportedSubtitle,
72
73 #[error("unknown track handler: {0:?}")]
74 UnknownTrackHandler([u8; 4]),
75
76 #[error("missing codec")]
77 MissingCodec,
78
79 #[error("multiple codecs")]
80 MultipleCodecs,
81
82 #[error("unknown codec: {0:?}")]
83 UnknownCodec(mp4_atom::FourCC),
84
85 #[error("unsupported codec: {0:?}")]
86 UnsupportedCodec(Box<mp4_atom::Codec>),
87
88 #[error("unsupported codec: MPEG2")]
89 UnsupportedMpeg2,
90
91 #[error("duplicate moof")]
92 DuplicateMoof,
93
94 #[error("missing trun")]
95 MissingTrun,
96
97 #[error("missing tfdt")]
98 MissingTfdt,
99
100 #[error("video codec {0} needs a description (codec config record) to synthesize a CMAF init")]
101 MissingVideoDescription(String),
102
103 #[error("video track {0} missing in catalog")]
104 MissingVideoTrack(String),
105
106 #[error("audio track {0} missing in catalog")]
107 MissingAudioTrack(String),
108
109 #[error("invalid data offset")]
110 InvalidDataOffset,
111
112 #[error("unknown track {0}")]
113 UnknownTrack(u32),
114
115 #[error("no keyframe at start of group")]
116 NoKeyframe,
117
118 #[error("track sample range {start}..{end} is out of bounds of mdat (len {len})")]
119 SampleRangeOutOfBounds { start: usize, end: usize, len: usize },
120
121 #[error("no catalog snapshot")]
122 NoCatalogSnapshot,
123
124 #[error("encode_fragment called with no frames")]
125 NoFrames,
126
127 #[error("audio codec {0} needs a description (AudioSpecificConfig) to synthesize a CMAF init")]
128 MissingAudioDescription(String),
129
130 #[error("multi-sample fragment has a non-final sample with no duration; DTS is unrecoverable")]
131 MissingSampleDuration,
132}
133
134impl From<mp4_atom::Error> for Error {
135 fn from(err: mp4_atom::Error) -> Self {
136 Error::Mp4(std::sync::Arc::new(err))
137 }
138}
139
140pub type Result<T> = std::result::Result<T, Error>;
141
142pub struct Wire {
151 trak: Box<mp4_atom::Trak>,
152}
153
154impl Wire {
155 pub fn new(trak: mp4_atom::Trak) -> Self {
157 Self { trak: Box::new(trak) }
158 }
159
160 pub fn from_init(init_data: &[u8]) -> Result<Self> {
162 use mp4_atom::DecodeMaybe;
163
164 let mut cursor = std::io::Cursor::new(init_data);
165 while let Some(atom) = mp4_atom::Any::decode_maybe(&mut cursor)? {
166 if let mp4_atom::Any::Moov(mut moov) = atom {
167 return match moov.trak.len() {
168 1 => Ok(Self::new(moov.trak.remove(0))),
169 0 => Err(Error::NoTracks),
170 _ => Err(Error::MultipleTracks),
171 };
172 }
173 }
174 Err(Error::NoMoov)
175 }
176
177 pub fn trak(&self) -> &mp4_atom::Trak {
178 &self.trak
179 }
180}
181
182impl Container for Wire {
183 type Error = Error;
184
185 fn write(&self, group: &mut moq_net::GroupProducer, frames: &[Frame]) -> std::result::Result<(), Self::Error> {
186 let timescale = self.trak.mdia.mdhd.timescale as u64;
187 let track_id = self.trak.tkhd.track_id;
188 encode(group, frames, timescale, track_id)
189 }
190
191 fn poll_read(
192 &self,
193 group: &mut moq_net::GroupConsumer,
194 waiter: &kio::Waiter,
195 ) -> Poll<std::result::Result<crate::container::Read, Self::Error>> {
196 use std::task::ready;
197
198 let Some(data) = ready!(group.poll_read_frame(waiter)?) else {
199 return Poll::Ready(Ok(crate::container::Read::Done));
200 };
201
202 let timescale = self.trak.mdia.mdhd.timescale as u64;
203 Poll::Ready(Ok(crate::container::Read::Fragment(decode(data, timescale)?)))
205 }
206}
207
208pub(crate) fn decode(data: Bytes, timescale: u64) -> Result<Vec<Frame>> {
209 use mp4_atom::DecodeMaybe;
210
211 let mut cursor = std::io::Cursor::new(&data);
212 let mut moof = None;
213 let mut mdat_data = None;
214
215 while let Some(atom) = mp4_atom::Any::decode_maybe(&mut cursor)? {
216 match atom {
217 mp4_atom::Any::Moof(m) => moof = Some(m),
218 mp4_atom::Any::Mdat(m) => mdat_data = Some(m.data),
219 _ => {}
220 }
221 }
222
223 let moof = moof.ok_or(Error::NoMoof)?;
224 let mdat_data = mdat_data.ok_or(Error::NoMdat)?;
225 let traf = moof.traf.first().ok_or(Error::NoTraf)?;
226 let tfdt = traf.tfdt.as_ref().ok_or(Error::NoTfdt)?;
227 let base_dts = tfdt.base_media_decode_time;
228
229 let default_size = traf.tfhd.default_sample_size;
230 let default_duration = traf.tfhd.default_sample_duration;
231
232 let total_samples: usize = traf.trun.iter().map(|t| t.entries.len()).sum();
236
237 let mut frames = Vec::new();
238 let mut offset = 0usize;
239 let mut dts = base_dts;
240 let mut sample_index = 0usize;
241
242 for trun in &traf.trun {
243 for entry in &trun.entries {
244 let size = entry.size.or(default_size).unwrap_or(0) as usize;
245 let end = offset + size;
246
247 if end > mdat_data.len() {
248 return Err(Error::SampleRangeOutOfBounds {
249 start: offset,
250 end,
251 len: mdat_data.len(),
252 });
253 }
254
255 let cts = entry.cts.unwrap_or_default() as i64;
256 let pts = dts.checked_add_signed(cts).ok_or(Error::PtsOverflow)?;
257 let timestamp = Timestamp::from_scale(pts, timescale)?;
259 let payload = Bytes::copy_from_slice(&mdat_data[offset..end]);
260 let flags = entry.flags.unwrap_or(0);
261 let keyframe = (flags >> 24) & 0x3 == 0x2;
263
264 let sample_duration = entry.duration.or(default_duration).filter(|d| *d != 0);
267
268 let is_last = sample_index + 1 == total_samples;
271 if sample_duration.is_none() && !is_last {
272 return Err(Error::MissingSampleDuration);
273 }
274
275 let duration = sample_duration
276 .map(|d| Timestamp::from_scale(d as u64, timescale))
277 .transpose()?;
278
279 frames.push(Frame {
280 timestamp,
281 payload,
282 keyframe,
283 duration,
284 });
285
286 offset = end;
287 dts += sample_duration.unwrap_or(0) as u64;
288 sample_index += 1;
289 }
290 }
291
292 Ok(frames)
293}
294
295pub(crate) fn encode(
296 group: &mut moq_net::GroupProducer,
297 frames: &[Frame],
298 timescale: u64,
299 track_id: u32,
300) -> Result<()> {
301 if frames.is_empty() {
302 return Ok(());
303 }
304
305 let sequence_number = group.frame_count() as u32;
306 let bytes = encode_fragment(track_id, timescale, sequence_number, frames)?;
307 let mut writer = group.create_frame(moq_net::Frame {
310 size: bytes.len() as u64,
311 })?;
312 writer.write(bytes)?;
313 writer.finish()?;
314
315 Ok(())
316}
317
318pub(crate) fn encode_fragment(track_id: u32, timescale: u64, sequence_number: u32, frames: &[Frame]) -> Result<Bytes> {
327 use mp4_atom::Encode;
328
329 if frames.is_empty() {
330 return Ok(Bytes::new());
331 }
332
333 let dts = frames[0].timestamp.as_scale(timescale) as u64;
338
339 let entries: Vec<_> = frames
340 .iter()
341 .map(|f| {
342 let flags = if f.keyframe { 0x0200_0000 } else { 0x0001_0000 };
343 let duration = f.duration.map(|d| d.as_scale(timescale) as u32);
346 mp4_atom::TrunEntry {
347 size: Some(f.payload.len() as u32),
348 flags: Some(flags),
349 duration,
350 ..Default::default()
351 }
352 })
353 .collect();
354
355 let mdat_data: Vec<u8> = frames.iter().flat_map(|f| f.payload.iter().copied()).collect();
356
357 let build_moof = |data_offset| mp4_atom::Moof {
358 mfhd: mp4_atom::Mfhd { sequence_number },
359 traf: vec![mp4_atom::Traf {
360 tfhd: mp4_atom::Tfhd {
361 track_id,
362 ..Default::default()
363 },
364 tfdt: Some(mp4_atom::Tfdt {
365 base_media_decode_time: dts,
366 }),
367 trun: vec![mp4_atom::Trun {
368 data_offset: Some(data_offset),
369 entries: entries.clone(),
370 }],
371 ..Default::default()
372 }],
373 };
374
375 let mut buf = Vec::new();
377 build_moof(0).encode(&mut buf)?;
378 let moof_size = buf.len();
379
380 buf.clear();
382 build_moof((moof_size + 8) as i32).encode(&mut buf)?;
383
384 let mdat = mp4_atom::Mdat { data: mdat_data };
385 mdat.encode(&mut buf)?;
386
387 Ok(Bytes::from(buf))
388}
389
390pub(crate) fn synthesize_video_trak(
398 track_id: u32,
399 timescale: u64,
400 config: &VideoConfig,
401 description: Option<&[u8]>,
402) -> Result<mp4_atom::Trak> {
403 let width = config.coded_width.unwrap_or(0) as u16;
404 let height = config.coded_height.unwrap_or(0) as u16;
405 let visual = mp4_atom::Visual {
406 data_reference_index: 1,
407 width,
408 height,
409 ..Default::default()
410 };
411
412 let require_description = || description.ok_or_else(|| Error::MissingVideoDescription(config.codec.to_string()));
414
415 let sample_entry = match &config.codec {
416 VideoCodec::H264(_) => {
417 let mut cursor = std::io::Cursor::new(require_description()?);
418 let avcc = mp4_atom::Avcc::decode_body(&mut cursor).map_err(Error::from)?;
419 mp4_atom::Codec::from(mp4_atom::Avc1 {
420 visual,
421 avcc,
422 ..Default::default()
423 })
424 }
425 VideoCodec::H265(h265) => {
426 let mut cursor = std::io::Cursor::new(require_description()?);
427 let hvcc = mp4_atom::Hvcc::decode_body(&mut cursor).map_err(Error::from)?;
428 if h265.in_band {
430 mp4_atom::Codec::from(mp4_atom::Hev1 {
431 visual,
432 hvcc,
433 ..Default::default()
434 })
435 } else {
436 mp4_atom::Codec::from(mp4_atom::Hvc1 {
437 visual,
438 hvcc,
439 ..Default::default()
440 })
441 }
442 }
443 VideoCodec::AV1(av1) => mp4_atom::Codec::from(mp4_atom::Av01 {
444 visual,
445 av1c: crate::codec::av1::av1c_from_av1(av1),
446 ..Default::default()
447 }),
448 VideoCodec::VP8 => mp4_atom::Codec::from(mp4_atom::Vp08 {
449 visual,
450 vpcc: crate::codec::vp8::vpcc(),
451 ..Default::default()
452 }),
453 VideoCodec::VP9(vp9) => mp4_atom::Codec::from(mp4_atom::Vp09 {
454 visual,
455 vpcc: crate::codec::vp9::vpcc(vp9),
456 ..Default::default()
457 }),
458 other => return Err(Error::UnsupportedSynthesis(format!("video codec {:?}", other))),
459 };
460
461 Ok(build_video_trak(track_id, timescale, sample_entry, width, height))
462}
463
464pub(crate) fn synthesize_audio_trak(track_id: u32, timescale: u64, config: &AudioConfig) -> Result<mp4_atom::Trak> {
466 use mp4_atom::Decode;
467
468 let audio = mp4_atom::Audio {
469 data_reference_index: 1,
470 channel_count: config.channel_count as u16,
471 sample_size: 16,
472 sample_rate: mp4_atom::FixedPoint::from(config.sample_rate as u16),
473 };
474
475 let sample_entry = match &config.codec {
476 AudioCodec::Opus => mp4_atom::Codec::from(mp4_atom::Opus {
477 audio,
478 dops: mp4_atom::Dops {
479 output_channel_count: config.channel_count as u8,
480 pre_skip: 0,
481 input_sample_rate: config.sample_rate,
482 output_gain: 0,
483 },
484 btrt: None,
485 }),
486 AudioCodec::AAC(_) => {
487 let description = config
492 .description
493 .as_ref()
494 .ok_or_else(|| Error::MissingAudioDescription(config.codec.to_string()))?;
495 let mut cursor = std::io::Cursor::new(description.as_ref());
496 let dec_specific = mp4_atom::esds::DecoderSpecific::decode(&mut cursor)?;
497
498 let bitrate = config.bitrate.unwrap_or(0) as u32;
499 mp4_atom::Codec::from(mp4_atom::Mp4a {
500 audio,
501 esds: mp4_atom::Esds {
502 es_desc: mp4_atom::esds::EsDescriptor {
503 es_id: 0,
505 dec_config: mp4_atom::esds::DecoderConfig {
506 object_type_indication: 0x40, stream_type: 0x05, up_stream: 0,
509 buffer_size_db: Default::default(),
510 max_bitrate: bitrate,
511 avg_bitrate: bitrate,
512 dec_specific,
513 },
514 sl_config: Default::default(),
515 },
516 },
517 btrt: None,
518 taic: None,
519 })
520 }
521 other => return Err(Error::UnsupportedSynthesis(format!("audio codec {:?}", other))),
522 };
523
524 Ok(build_audio_trak(track_id, timescale, sample_entry))
525}
526
527fn build_video_trak(
528 track_id: u32,
529 timescale: u64,
530 sample_entry: mp4_atom::Codec,
531 width: u16,
532 height: u16,
533) -> mp4_atom::Trak {
534 mp4_atom::Trak {
535 tkhd: mp4_atom::Tkhd {
536 track_id,
537 enabled: true,
538 width: mp4_atom::FixedPoint::from(width),
539 height: mp4_atom::FixedPoint::from(height),
540 ..Default::default()
541 },
542 mdia: build_mdia(timescale, b"vide", true, sample_entry),
543 ..Default::default()
544 }
545}
546
547fn build_audio_trak(track_id: u32, timescale: u64, sample_entry: mp4_atom::Codec) -> mp4_atom::Trak {
548 mp4_atom::Trak {
549 tkhd: mp4_atom::Tkhd {
550 track_id,
551 enabled: true,
552 ..Default::default()
553 },
554 mdia: build_mdia(timescale, b"soun", false, sample_entry),
555 ..Default::default()
556 }
557}
558
559fn build_mdia(timescale: u64, handler: &[u8; 4], is_video: bool, sample_entry: mp4_atom::Codec) -> mp4_atom::Mdia {
560 mp4_atom::Mdia {
561 mdhd: mp4_atom::Mdhd {
562 timescale: timescale as u32,
563 ..Default::default()
564 },
565 hdlr: mp4_atom::Hdlr {
566 handler: mp4_atom::FourCC::new(handler),
567 name: String::new(),
568 },
569 minf: mp4_atom::Minf {
570 vmhd: is_video.then(mp4_atom::Vmhd::default),
571 smhd: (!is_video).then(mp4_atom::Smhd::default),
572 dinf: mp4_atom::Dinf {
573 dref: mp4_atom::Dref {
574 urls: vec![mp4_atom::Url::default()],
575 },
576 },
577 stbl: mp4_atom::Stbl {
578 stsd: mp4_atom::Stsd {
579 codecs: vec![sample_entry],
580 },
581 ..Default::default()
582 },
583 ..Default::default()
584 },
585 }
586}
587
588pub(crate) fn default_video_timescale(config: &VideoConfig) -> u64 {
594 if let Some(fps) = config.framerate {
595 (fps * 1000.0) as u64
596 } else {
597 90000
598 }
599}
600
601#[cfg(test)]
602mod tests {
603 use super::*;
604
605 fn ts(micros: u64) -> Timestamp {
606 Timestamp::from_micros(micros).unwrap()
607 }
608
609 #[test]
610 fn decode_reads_trun_sample_duration() {
611 use mp4_atom::Encode;
612
613 let timescale = 1_000_000;
617 let moof = mp4_atom::Moof {
618 mfhd: mp4_atom::Mfhd { sequence_number: 0 },
619 traf: vec![mp4_atom::Traf {
620 tfhd: mp4_atom::Tfhd {
621 track_id: 1,
622 ..Default::default()
623 },
624 tfdt: Some(mp4_atom::Tfdt {
625 base_media_decode_time: 0,
626 }),
627 trun: vec![mp4_atom::Trun {
628 data_offset: Some(0),
629 entries: vec![
630 mp4_atom::TrunEntry {
631 size: Some(2),
632 duration: Some(33_333),
633 ..Default::default()
634 },
635 mp4_atom::TrunEntry {
636 size: Some(2),
637 duration: Some(33_333),
638 ..Default::default()
639 },
640 ],
641 }],
642 ..Default::default()
643 }],
644 };
645
646 let mut buf = Vec::new();
647 moof.encode(&mut buf).unwrap();
648 mp4_atom::Mdat {
649 data: vec![0xDE, 0xAD, 0xBE, 0xEF],
650 }
651 .encode(&mut buf)
652 .unwrap();
653
654 let frames = decode(Bytes::from(buf), timescale).unwrap();
655 assert_eq!(frames.len(), 2);
656 assert_eq!(frames[0].timestamp, ts(0));
657 assert_eq!(frames[0].duration, Some(ts(33_333)));
658 assert_eq!(frames[1].timestamp, ts(33_333));
659 assert_eq!(frames[1].duration, Some(ts(33_333)));
660 }
661
662 #[test]
663 fn duration_round_trips_through_encode() {
664 let timescale = 1_000_000;
666 let input = vec![Frame {
667 timestamp: ts(0),
668 payload: Bytes::from_static(&[0xDE, 0xAD]),
669 keyframe: true,
670 duration: Some(ts(33_333)),
671 }];
672
673 let fragment = encode_fragment(1, timescale, 0, &input).unwrap();
674 let frames = decode(fragment, timescale).unwrap();
675
676 assert_eq!(frames.len(), 1);
677 assert_eq!(frames[0].duration, Some(ts(33_333)));
678 }
679
680 #[test]
681 fn decode_without_duration_reports_none() {
682 let timescale = 90_000;
685 let frames = vec![Frame {
686 timestamp: ts(0),
687 payload: Bytes::from_static(&[0xDE, 0xAD]),
688 keyframe: true,
689 duration: None,
690 }];
691
692 let fragment = encode_fragment(1, timescale, 0, &frames).unwrap();
693 let frames = decode(fragment, timescale).unwrap();
694
695 assert_eq!(frames.len(), 1);
696 assert_eq!(frames[0].duration, None);
697 }
698
699 #[test]
700 fn decode_zero_duration_reports_none() {
701 use mp4_atom::Encode;
702
703 let timescale = 24_000;
704 let moof = mp4_atom::Moof {
705 mfhd: mp4_atom::Mfhd { sequence_number: 0 },
706 traf: vec![mp4_atom::Traf {
707 tfhd: mp4_atom::Tfhd {
708 track_id: 1,
709 default_sample_duration: Some(0),
710 default_sample_size: Some(2),
711 ..Default::default()
712 },
713 tfdt: Some(mp4_atom::Tfdt {
714 base_media_decode_time: 2_000,
715 }),
716 trun: vec![mp4_atom::Trun {
717 data_offset: Some(0),
718 entries: vec![mp4_atom::TrunEntry {
719 size: None,
720 duration: None,
721 ..Default::default()
722 }],
723 }],
724 ..Default::default()
725 }],
726 };
727
728 let mut buf = Vec::new();
729 moof.encode(&mut buf).unwrap();
730 mp4_atom::Mdat { data: vec![0xDE, 0xAD] }.encode(&mut buf).unwrap();
731
732 let frames = decode(Bytes::from(buf), timescale).unwrap();
733 assert_eq!(frames.len(), 1);
734 assert_eq!(frames[0].timestamp.as_micros(), 83_333);
735 assert_eq!(frames[0].duration, None);
736 }
737}