mod export;
mod import;
pub use export::*;
pub use import::*;
#[cfg(test)]
mod export_test;
#[cfg(test)]
mod import_test;
use std::task::Poll;
use bytes::Bytes;
use hang::catalog::{AudioCodec, AudioConfig, VideoCodec, VideoConfig};
use mp4_atom::Atom;
use crate::container::{Container, Frame, Timestamp};
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("mp4: {0}")]
Mp4(#[from] mp4_atom::Error),
#[error("moq: {0}")]
Moq(#[from] moq_net::Error),
#[error("timestamp overflow")]
TimestampOverflow(#[from] moq_net::TimeOverflow),
#[error("no traf in moof")]
NoTraf,
#[error("no tfdt in traf")]
NoTfdt,
#[error("PTS overflow")]
PtsOverflow,
#[error("no moof found in CMAF frame data")]
NoMoof,
#[error("no mdat found in CMAF frame data")]
NoMdat,
#[error("no moov found in init data")]
NoMoov,
#[error("no tracks in moov")]
NoTracks,
#[error("multiple tracks in moov, use Trak instead")]
MultipleTracks,
#[error("can't synthesize CMAF init for {0}")]
UnsupportedSynthesis(String),
#[error("video codec {0} needs a description (codec config record) to synthesize a CMAF init")]
MissingVideoDescription(String),
#[error("audio codec {0} needs a description (AudioSpecificConfig) to synthesize a CMAF init")]
MissingAudioDescription(String),
}
pub struct Wire {
trak: Box<mp4_atom::Trak>,
}
impl Wire {
pub fn new(trak: mp4_atom::Trak) -> Self {
Self { trak: Box::new(trak) }
}
pub fn from_init(init_data: &[u8]) -> Result<Self, Error> {
use mp4_atom::DecodeMaybe;
let mut cursor = std::io::Cursor::new(init_data);
while let Some(atom) = mp4_atom::Any::decode_maybe(&mut cursor)? {
if let mp4_atom::Any::Moov(mut moov) = atom {
return match moov.trak.len() {
1 => Ok(Self::new(moov.trak.remove(0))),
0 => Err(Error::NoTracks),
_ => Err(Error::MultipleTracks),
};
}
}
Err(Error::NoMoov)
}
pub fn trak(&self) -> &mp4_atom::Trak {
&self.trak
}
}
impl Container for Wire {
type Error = Error;
fn write(&self, group: &mut moq_net::GroupProducer, frames: &[Frame]) -> Result<(), Self::Error> {
let timescale = self.trak.mdia.mdhd.timescale as u64;
let track_id = self.trak.tkhd.track_id;
encode(group, frames, timescale, track_id)
}
fn poll_read(
&self,
group: &mut moq_net::GroupConsumer,
waiter: &kio::Waiter,
) -> Poll<Result<Option<Vec<Frame>>, Self::Error>> {
use std::task::ready;
let Some(data) = ready!(group.poll_read_frame(waiter)?) else {
return Poll::Ready(Ok(None));
};
let timescale = self.trak.mdia.mdhd.timescale as u64;
Poll::Ready(Ok(Some(decode(data, timescale)?)))
}
}
pub(crate) fn decode(data: Bytes, timescale: u64) -> Result<Vec<Frame>, Error> {
use mp4_atom::DecodeMaybe;
let mut cursor = std::io::Cursor::new(&data);
let mut moof = None;
let mut mdat_data = None;
while let Some(atom) = mp4_atom::Any::decode_maybe(&mut cursor)? {
match atom {
mp4_atom::Any::Moof(m) => moof = Some(m),
mp4_atom::Any::Mdat(m) => mdat_data = Some(m.data),
_ => {}
}
}
let moof = moof.ok_or(Error::NoMoof)?;
let mdat_data = mdat_data.ok_or(Error::NoMdat)?;
let traf = moof.traf.first().ok_or(Error::NoTraf)?;
let tfdt = traf.tfdt.as_ref().ok_or(Error::NoTfdt)?;
let base_dts = tfdt.base_media_decode_time;
let default_size = traf.tfhd.default_sample_size;
let default_duration = traf.tfhd.default_sample_duration;
let mut frames = Vec::new();
let mut offset = 0usize;
let mut dts = base_dts;
for trun in &traf.trun {
for entry in &trun.entries {
let size = entry.size.or(default_size).unwrap_or(0) as usize;
let end = offset + size;
if end > mdat_data.len() {
return Ok(frames);
}
let cts = entry.cts.unwrap_or_default() as i64;
let pts = dts.checked_add_signed(cts).ok_or(Error::PtsOverflow)?;
let timestamp = Timestamp::from_scale(pts, timescale)?;
let payload = Bytes::copy_from_slice(&mdat_data[offset..end]);
let flags = entry.flags.unwrap_or(0);
let keyframe = (flags >> 24) & 0x3 == 0x2;
frames.push(Frame {
timestamp,
payload,
keyframe,
});
offset = end;
dts += entry.duration.or(default_duration).unwrap_or(0) as u64;
}
}
Ok(frames)
}
pub(crate) fn encode(
group: &mut moq_net::GroupProducer,
frames: &[Frame],
timescale: u64,
track_id: u32,
) -> Result<(), Error> {
if frames.is_empty() {
return Ok(());
}
let sequence_number = group.frame_count() as u32;
let bytes = encode_fragment(track_id, timescale, sequence_number, frames)?;
let mut writer = group.create_frame(bytes.len().into())?;
writer.write(bytes)?;
writer.finish()?;
Ok(())
}
pub(crate) fn encode_fragment(
track_id: u32,
timescale: u64,
sequence_number: u32,
frames: &[Frame],
) -> Result<Bytes, Error> {
use mp4_atom::Encode;
if frames.is_empty() {
return Ok(Bytes::new());
}
let dts = (frames[0].timestamp.as_micros() * timescale as u128 / 1_000_000) as u64;
let entries: Vec<_> = frames
.iter()
.map(|f| {
let flags = if f.keyframe { 0x0200_0000 } else { 0x0001_0000 };
mp4_atom::TrunEntry {
size: Some(f.payload.len() as u32),
flags: Some(flags),
..Default::default()
}
})
.collect();
let mdat_data: Vec<u8> = frames.iter().flat_map(|f| f.payload.iter().copied()).collect();
let build_moof = |data_offset| mp4_atom::Moof {
mfhd: mp4_atom::Mfhd { sequence_number },
traf: vec![mp4_atom::Traf {
tfhd: mp4_atom::Tfhd {
track_id,
..Default::default()
},
tfdt: Some(mp4_atom::Tfdt {
base_media_decode_time: dts,
}),
trun: vec![mp4_atom::Trun {
data_offset: Some(data_offset),
entries: entries.clone(),
}],
..Default::default()
}],
};
let mut buf = Vec::new();
build_moof(0).encode(&mut buf)?;
let moof_size = buf.len();
buf.clear();
build_moof((moof_size + 8) as i32).encode(&mut buf)?;
let mdat = mp4_atom::Mdat { data: mdat_data };
mdat.encode(&mut buf)?;
Ok(Bytes::from(buf))
}
pub(crate) fn synthesize_video_trak(
track_id: u32,
timescale: u64,
config: &VideoConfig,
description: Option<&[u8]>,
) -> Result<mp4_atom::Trak, Error> {
let width = config.coded_width.unwrap_or(0) as u16;
let height = config.coded_height.unwrap_or(0) as u16;
let visual = mp4_atom::Visual {
data_reference_index: 1,
width,
height,
..Default::default()
};
let require_description = || description.ok_or_else(|| Error::MissingVideoDescription(config.codec.to_string()));
let sample_entry = match &config.codec {
VideoCodec::H264(_) => {
let mut cursor = std::io::Cursor::new(require_description()?);
let avcc = mp4_atom::Avcc::decode_body(&mut cursor).map_err(Error::Mp4)?;
mp4_atom::Codec::from(mp4_atom::Avc1 {
visual,
avcc,
..Default::default()
})
}
VideoCodec::H265(h265) => {
let mut cursor = std::io::Cursor::new(require_description()?);
let hvcc = mp4_atom::Hvcc::decode_body(&mut cursor).map_err(Error::Mp4)?;
if h265.in_band {
mp4_atom::Codec::from(mp4_atom::Hev1 {
visual,
hvcc,
..Default::default()
})
} else {
mp4_atom::Codec::from(mp4_atom::Hvc1 {
visual,
hvcc,
..Default::default()
})
}
}
VideoCodec::VP8 => mp4_atom::Codec::from(mp4_atom::Vp08 {
visual,
vpcc: crate::codec::vp8::vpcc(),
..Default::default()
}),
VideoCodec::VP9(vp9) => mp4_atom::Codec::from(mp4_atom::Vp09 {
visual,
vpcc: crate::codec::vp9::vpcc(vp9),
..Default::default()
}),
other => return Err(Error::UnsupportedSynthesis(format!("video codec {:?}", other))),
};
Ok(build_video_trak(track_id, timescale, sample_entry, width, height))
}
pub(crate) fn synthesize_audio_trak(
track_id: u32,
timescale: u64,
config: &AudioConfig,
) -> Result<mp4_atom::Trak, Error> {
use mp4_atom::Decode;
let audio = mp4_atom::Audio {
data_reference_index: 1,
channel_count: config.channel_count as u16,
sample_size: 16,
sample_rate: mp4_atom::FixedPoint::from(config.sample_rate as u16),
};
let sample_entry = match &config.codec {
AudioCodec::Opus => mp4_atom::Codec::from(mp4_atom::Opus {
audio,
dops: mp4_atom::Dops {
output_channel_count: config.channel_count as u8,
pre_skip: 0,
input_sample_rate: config.sample_rate,
output_gain: 0,
},
btrt: None,
}),
AudioCodec::AAC(_) => {
let description = config
.description
.as_ref()
.ok_or_else(|| Error::MissingAudioDescription(config.codec.to_string()))?;
let mut cursor = std::io::Cursor::new(description.as_ref());
let dec_specific = mp4_atom::esds::DecoderSpecific::decode(&mut cursor)?;
let bitrate = config.bitrate.unwrap_or(0) as u32;
mp4_atom::Codec::from(mp4_atom::Mp4a {
audio,
esds: mp4_atom::Esds {
es_desc: mp4_atom::esds::EsDescriptor {
es_id: 0,
dec_config: mp4_atom::esds::DecoderConfig {
object_type_indication: 0x40, stream_type: 0x05, up_stream: 0,
buffer_size_db: Default::default(),
max_bitrate: bitrate,
avg_bitrate: bitrate,
dec_specific,
},
sl_config: Default::default(),
},
},
btrt: None,
taic: None,
})
}
other => return Err(Error::UnsupportedSynthesis(format!("audio codec {:?}", other))),
};
Ok(build_audio_trak(track_id, timescale, sample_entry))
}
fn build_video_trak(
track_id: u32,
timescale: u64,
sample_entry: mp4_atom::Codec,
width: u16,
height: u16,
) -> mp4_atom::Trak {
mp4_atom::Trak {
tkhd: mp4_atom::Tkhd {
track_id,
enabled: true,
width: mp4_atom::FixedPoint::from(width),
height: mp4_atom::FixedPoint::from(height),
..Default::default()
},
mdia: build_mdia(timescale, b"vide", true, sample_entry),
..Default::default()
}
}
fn build_audio_trak(track_id: u32, timescale: u64, sample_entry: mp4_atom::Codec) -> mp4_atom::Trak {
mp4_atom::Trak {
tkhd: mp4_atom::Tkhd {
track_id,
enabled: true,
..Default::default()
},
mdia: build_mdia(timescale, b"soun", false, sample_entry),
..Default::default()
}
}
fn build_mdia(timescale: u64, handler: &[u8; 4], is_video: bool, sample_entry: mp4_atom::Codec) -> mp4_atom::Mdia {
mp4_atom::Mdia {
mdhd: mp4_atom::Mdhd {
timescale: timescale as u32,
..Default::default()
},
hdlr: mp4_atom::Hdlr {
handler: mp4_atom::FourCC::new(handler),
name: String::new(),
},
minf: mp4_atom::Minf {
vmhd: is_video.then(mp4_atom::Vmhd::default),
smhd: (!is_video).then(mp4_atom::Smhd::default),
dinf: mp4_atom::Dinf {
dref: mp4_atom::Dref {
urls: vec![mp4_atom::Url::default()],
},
},
stbl: mp4_atom::Stbl {
stsd: mp4_atom::Stsd {
codecs: vec![sample_entry],
},
..Default::default()
},
..Default::default()
},
}
}
pub(crate) fn default_video_timescale(config: &VideoConfig) -> u64 {
if let Some(fps) = config.framerate {
(fps * 1000.0) as u64
} else {
90000
}
}