use super::Muxer;
use crate::{CodecId, MediaFrame, Result, StreamError};
use bytes::Bytes;
const TIMESCALE: u32 = 1000; const VIDEO_TRACK_ID: u32 = 1;
const AUDIO_TRACK_ID: u32 = 2;
const FALLBACK_DURATION: u64 = TIMESCALE as u64 / 30;
struct Sample {
data: Bytes,
dts: i64,
cts: i32, is_key: bool,
duration: Option<u64>,
}
struct AudioTrack {
entry: Vec<u8>,
samples: Vec<Sample>,
base_decode_time: u64,
codec_str: String,
}
pub struct Fmp4Muxer {
codec: Option<CodecId>,
width: u16,
height: u16,
codec_str: Option<String>,
video_entry: Option<Vec<u8>>,
audio: Option<AudioTrack>,
seq: u32,
base_decode_time: u64,
samples: Vec<Sample>,
}
impl Default for Fmp4Muxer {
fn default() -> Self {
Self::new()
}
}
impl Fmp4Muxer {
pub fn new() -> Self {
Self {
codec: None,
width: 0,
height: 0,
codec_str: None,
video_entry: None,
audio: None,
seq: 1,
base_decode_time: 0,
samples: Vec::new(),
}
}
fn ingest_h264_config(&mut self, config_record: &[u8]) -> Result<Vec<u8>> {
let mut sps: Option<&[u8]> = None;
let mut pps: Option<&[u8]> = None;
for nal in crate::codec::h264::iter_nals_annexb(config_record) {
match nal.first().map(|b| b & 0x1f) {
Some(t) if t == crate::codec::h264::NAL_SPS => sps = Some(nal),
Some(t) if t == crate::codec::h264::NAL_PPS => pps = Some(nal),
_ => {}
}
}
let sps = sps.ok_or_else(|| StreamError::codec("fmp4: no SPS in H.264 config"))?;
let pps = pps.ok_or_else(|| StreamError::codec("fmp4: no PPS in H.264 config"))?;
if sps.len() < 4 {
return Err(StreamError::codec("fmp4: truncated SPS"));
}
let info = crate::codec::h264::parse_sps(sps)
.ok_or_else(|| StreamError::codec("fmp4: unparseable SPS"))?;
self.width = info.width as u16;
self.height = info.height as u16;
self.codec_str = Some(format!("avc1.{:02X}{:02X}{:02X}", sps[1], sps[2], sps[3]));
let mut c = Vec::with_capacity(16 + sps.len() + pps.len());
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());
c.extend_from_slice(sps);
c.push(1); c.extend_from_slice(&(pps.len() as u16).to_be_bytes());
c.extend_from_slice(pps);
Ok(build_avc1(self.width, self.height, &c))
}
#[cfg(feature = "codec-h265")]
fn ingest_h265_config(&mut self, config_record: &[u8]) -> Result<Vec<u8>> {
use crate::codec::{h265::H265, CodecParser};
let (params, hvcc) = crate::codec::h265::hvcc_config_record(config_record)
.ok_or_else(|| StreamError::codec("fmp4: no parsable SPS in H.265 config"))?;
self.width = params.width as u16;
self.height = params.height as u16;
self.codec_str = Some(H265::hls_codec_string(¶ms));
Ok(build_hvc1(self.width, self.height, &hvcc))
}
#[cfg(feature = "codec-av1")]
fn ingest_av1_config(&mut self, config_record: &[u8]) -> Result<Vec<u8>> {
use crate::codec::{av1::Av1, CodecParser};
let (params, av1c) = crate::codec::av1::av1c_config_record(config_record)
.ok_or_else(|| StreamError::codec("fmp4: no AV1 sequence header in config"))?;
self.width = params.width as u16;
self.height = params.height as u16;
self.codec_str = Some(Av1::hls_codec_string(¶ms));
Ok(build_av01(self.width, self.height, &av1c))
}
#[cfg(feature = "codec-vvc")]
fn ingest_vvc_config(&mut self, config_record: &[u8]) -> Result<Vec<u8>> {
use crate::codec::{vvc::Vvc, CodecParser};
let (params, vvcc) =
crate::codec::vvc::vvcc_config_record(config_record).ok_or_else(|| {
StreamError::codec(
"fmp4: VVC config needs an SPS without a PTL block (parser limit)",
)
})?;
self.width = params.width as u16;
self.height = params.height as u16;
self.codec_str = Some(Vvc::hls_codec_string(¶ms));
Ok(build_vvc1(self.width, self.height, &vvcc))
}
fn ingest_video_config(&mut self, codec: CodecId, config_record: &[u8]) -> Result<Vec<u8>> {
let entry = match codec {
CodecId::H264 => self.ingest_h264_config(config_record)?,
#[cfg(feature = "codec-h265")]
CodecId::H265 => self.ingest_h265_config(config_record)?,
#[cfg(feature = "codec-av1")]
CodecId::AV1 => self.ingest_av1_config(config_record)?,
#[cfg(feature = "codec-vvc")]
CodecId::VVC => self.ingest_vvc_config(config_record)?,
_ => {
return Err(StreamError::UnsupportedCodec(format!(
"fmp4 init segment: {codec:?} not supported in this build"
)))
}
};
self.codec = Some(codec);
self.video_entry = Some(entry.clone());
Ok(entry)
}
fn set_audio_config(&mut self, asc: &[u8]) -> Result<()> {
if asc.len() < 2 {
return Err(StreamError::codec(
"fmp4: truncated AAC AudioSpecificConfig",
));
}
let object_type = asc[0] >> 3;
let freq_index = (((asc[0] & 0x07) << 1) | (asc[1] >> 7)) as usize;
let channels = (asc[1] >> 3) & 0x0F;
let sample_rate = AAC_SAMPLE_RATES.get(freq_index).copied().unwrap_or(48_000);
self.audio = Some(AudioTrack {
entry: build_mp4a(channels.max(1) as u16, sample_rate, asc),
samples: Vec::new(),
base_decode_time: 0,
codec_str: format!("mp4a.40.{}", object_type.max(1)),
});
Ok(())
}
fn flush_fragment(&mut self) -> Option<Bytes> {
let have_audio = self.audio.as_ref().is_some_and(|a| !a.samples.is_empty());
if self.samples.is_empty() && !have_audio {
return None;
}
let mut tracks: Vec<TrackFrag> = Vec::with_capacity(2);
if !self.samples.is_empty() {
let durations = sample_durations(&self.samples);
tracks.push(TrackFrag {
track_id: VIDEO_TRACK_ID,
base_decode_time: self.base_decode_time,
samples: &self.samples,
durations,
});
}
if let Some(audio) = self.audio.as_ref() {
if !audio.samples.is_empty() {
let durations = sample_durations(&audio.samples);
tracks.push(TrackFrag {
track_id: AUDIO_TRACK_ID,
base_decode_time: audio.base_decode_time,
samples: &audio.samples,
durations,
});
}
}
let mut out = build_moof(self.seq, &tracks);
let mdat_len: usize = tracks
.iter()
.flat_map(|t| t.samples.iter())
.map(|s| s.data.len())
.sum();
out.extend_from_slice(&((mdat_len + 8) as u32).to_be_bytes());
out.extend_from_slice(b"mdat");
for t in &tracks {
for s in t.samples {
out.extend_from_slice(&s.data);
}
}
let video_total: u64 = tracks
.iter()
.find(|t| t.track_id == VIDEO_TRACK_ID)
.map(|t| t.durations.iter().sum())
.unwrap_or(0);
let audio_total: u64 = tracks
.iter()
.find(|t| t.track_id == AUDIO_TRACK_ID)
.map(|t| t.durations.iter().sum())
.unwrap_or(0);
drop(tracks);
self.base_decode_time += video_total;
self.seq += 1;
self.samples.clear();
if let Some(audio) = self.audio.as_mut() {
audio.base_decode_time += audio_total;
audio.samples.clear();
}
Some(Bytes::from(out))
}
}
fn sample_durations(samples: &[Sample]) -> Vec<u64> {
let n = samples.len();
let mut out = Vec::with_capacity(n);
for i in 0..n {
let dur = if i + 1 < n {
(samples[i + 1].dts - samples[i].dts).max(0) as u64
} else {
samples[i]
.duration
.or_else(|| out.last().copied())
.unwrap_or(FALLBACK_DURATION)
};
out.push(dur);
}
out
}
impl Muxer for Fmp4Muxer {
fn extension(&self) -> &'static str {
"m4s"
}
fn start_segment(&mut self) -> Result<()> {
self.samples.clear();
Ok(())
}
fn write(&mut self, frame: &MediaFrame) -> Result<()> {
if frame.is_audio() {
if frame.flags.contains(crate::FrameFlags::CONFIG) || frame.codec != CodecId::AAC {
return Ok(());
}
if let Some(audio) = self.audio.as_mut() {
let raw = strip_adts(&frame.data);
if !raw.is_empty() {
audio.samples.push(Sample {
data: raw,
dts: frame.pts, cts: 0,
is_key: true, duration: frame.duration,
});
}
}
return Ok(());
}
if !frame.is_video() {
return Ok(());
}
let is_nal = matches!(
self.codec,
Some(CodecId::H264) | Some(CodecId::H265) | Some(CodecId::VVC)
);
let data = if is_nal {
crate::codec::h264::annexb_to_avcc(&frame.data) } else {
frame.data.clone()
};
if data.is_empty() {
return Ok(());
}
self.samples.push(Sample {
data,
dts: frame.dts,
cts: (frame.pts - frame.dts) as i32,
is_key: frame.is_keyframe(),
duration: frame.duration,
});
Ok(())
}
fn finish_segment(&mut self) -> Result<Bytes> {
Ok(self.flush_fragment().unwrap_or_default())
}
fn take_partial(&mut self) -> Result<Option<Bytes>> {
Ok(self.flush_fragment())
}
fn init_segment(&mut self, codec: CodecId, config_record: &[u8]) -> Result<Option<Bytes>> {
let sample_entry = self.ingest_video_config(codec, config_record)?;
let mut seg = build_ftyp();
seg.extend_from_slice(&build_moov(self.width, self.height, &sample_entry, None));
Ok(Some(Bytes::from(seg)))
}
fn build_init_from(&mut self, configs: &[(CodecId, Bytes)]) -> Result<Option<Bytes>> {
let mut video_entry = None;
for (codec, data) in configs {
match codec {
CodecId::AAC => self.set_audio_config(data)?,
CodecId::Opus => {} _ => video_entry = Some(self.ingest_video_config(*codec, data)?),
}
}
let Some(entry) = video_entry else {
return Ok(None);
};
let mut seg = build_ftyp();
seg.extend_from_slice(&build_moov(
self.width,
self.height,
&entry,
self.audio.as_ref(),
));
Ok(Some(Bytes::from(seg)))
}
fn codec_string(&self) -> Option<String> {
match (&self.codec_str, self.audio.as_ref()) {
(Some(v), Some(a)) => Some(format!("{v},{}", a.codec_str)),
(Some(v), None) => Some(v.clone()),
(None, _) => None,
}
}
}
fn bx(typ: &[u8; 4], body: &[u8]) -> Vec<u8> {
let mut v = Vec::with_capacity(8 + body.len());
v.extend_from_slice(&((body.len() + 8) as u32).to_be_bytes());
v.extend_from_slice(typ);
v.extend_from_slice(body);
v
}
fn full(typ: &[u8; 4], version: u8, flags: u32, body: &[u8]) -> Vec<u8> {
let mut b = Vec::with_capacity(4 + body.len());
b.extend_from_slice(&(((version as u32) << 24) | (flags & 0x00FF_FFFF)).to_be_bytes());
b.extend_from_slice(body);
bx(typ, &b)
}
const MATRIX_IDENTITY: [u32; 9] = [0x0001_0000, 0, 0, 0, 0x0001_0000, 0, 0, 0, 0x4000_0000];
fn put_matrix(v: &mut Vec<u8>) {
for m in MATRIX_IDENTITY {
v.extend_from_slice(&m.to_be_bytes());
}
}
fn build_ftyp() -> Vec<u8> {
let mut b = Vec::new();
b.extend_from_slice(b"iso5"); b.extend_from_slice(&0u32.to_be_bytes()); for brand in [b"iso5", b"iso6", b"mp41"] {
b.extend_from_slice(brand);
}
bx(b"ftyp", &b)
}
fn build_moov(width: u16, height: u16, video_entry: &[u8], audio: Option<&AudioTrack>) -> Vec<u8> {
let next_track_id = if audio.is_some() {
AUDIO_TRACK_ID + 1
} else {
2
};
let mut body = Vec::new();
body.extend_from_slice(&build_mvhd(next_track_id));
body.extend_from_slice(&build_video_trak(width, height, video_entry));
if let Some(a) = audio {
body.extend_from_slice(&build_audio_trak(&a.entry));
}
body.extend_from_slice(&build_mvex(audio.is_some()));
bx(b"moov", &body)
}
fn build_mvhd(next_track_id: u32) -> Vec<u8> {
let mut b = Vec::new();
b.extend_from_slice(&0u32.to_be_bytes()); b.extend_from_slice(&0u32.to_be_bytes()); b.extend_from_slice(&TIMESCALE.to_be_bytes());
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);
b.extend_from_slice(&[0u8; 24]); b.extend_from_slice(&next_track_id.to_be_bytes());
full(b"mvhd", 0, 0, &b)
}
fn build_video_trak(width: u16, height: u16, entry: &[u8]) -> Vec<u8> {
let mut b = Vec::new();
b.extend_from_slice(&build_tkhd(VIDEO_TRACK_ID, width, height, 0));
b.extend_from_slice(&build_mdia(
b"vide",
b"VideoHandler\0",
&build_video_minf(entry),
));
bx(b"trak", &b)
}
fn build_audio_trak(entry: &[u8]) -> Vec<u8> {
let mut b = Vec::new();
b.extend_from_slice(&build_tkhd(AUDIO_TRACK_ID, 0, 0, 0x0100));
b.extend_from_slice(&build_mdia(
b"soun",
b"SoundHandler\0",
&build_audio_minf(entry),
));
bx(b"trak", &b)
}
fn build_tkhd(track_id: u32, width: u16, height: u16, volume: u16) -> Vec<u8> {
let mut b = Vec::new();
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());
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(&volume.to_be_bytes()); b.extend_from_slice(&0u16.to_be_bytes()); put_matrix(&mut b);
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) }
fn build_mdia(handler: &[u8; 4], name: &[u8], minf: &[u8]) -> Vec<u8> {
let mut b = Vec::new();
b.extend_from_slice(&build_mdhd());
b.extend_from_slice(&build_hdlr(handler, name));
b.extend_from_slice(minf);
bx(b"mdia", &b)
}
fn build_mdhd() -> Vec<u8> {
let mut b = Vec::new();
b.extend_from_slice(&0u32.to_be_bytes()); b.extend_from_slice(&0u32.to_be_bytes()); b.extend_from_slice(&TIMESCALE.to_be_bytes());
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)
}
fn build_hdlr(handler_type: &[u8; 4], name: &[u8]) -> Vec<u8> {
let mut b = Vec::new();
b.extend_from_slice(&0u32.to_be_bytes()); b.extend_from_slice(handler_type);
b.extend_from_slice(&[0u8; 12]); b.extend_from_slice(name);
full(b"hdlr", 0, 0, &b)
}
fn build_video_minf(entry: &[u8]) -> Vec<u8> {
let mut b = Vec::new();
b.extend_from_slice(&full(b"vmhd", 0, 1, &[0u8; 8])); b.extend_from_slice(&build_dinf());
b.extend_from_slice(&build_stbl(entry));
bx(b"minf", &b)
}
fn build_audio_minf(entry: &[u8]) -> Vec<u8> {
let mut b = Vec::new();
b.extend_from_slice(&full(b"smhd", 0, 0, &[0u8; 4]));
b.extend_from_slice(&build_dinf());
b.extend_from_slice(&build_stbl(entry));
bx(b"minf", &b)
}
fn build_dinf() -> Vec<u8> {
let url = full(b"url ", 0, 1, &[]);
let mut dref_body = Vec::new();
dref_body.extend_from_slice(&1u32.to_be_bytes()); dref_body.extend_from_slice(&url);
let dref = full(b"dref", 0, 0, &dref_body);
bx(b"dinf", &dref)
}
fn build_stbl(entry: &[u8]) -> Vec<u8> {
let mut b = Vec::new();
b.extend_from_slice(&build_stsd(entry));
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()));
let mut stsz = Vec::new();
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));
b.extend_from_slice(&full(b"stco", 0, 0, &0u32.to_be_bytes()));
bx(b"stbl", &b)
}
fn build_stsd(entry: &[u8]) -> Vec<u8> {
let mut b = Vec::new();
b.extend_from_slice(&1u32.to_be_bytes()); b.extend_from_slice(entry);
full(b"stsd", 0, 0, &b)
}
fn visual_sample_entry(typ: &[u8; 4], width: u16, height: u16, config_box: &[u8]) -> Vec<u8> {
let mut b = Vec::new();
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());
b.extend_from_slice(&height.to_be_bytes());
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);
bx(typ, &b)
}
fn build_avc1(width: u16, height: u16, avcc: &[u8]) -> Vec<u8> {
visual_sample_entry(b"avc1", width, height, &bx(b"avcC", avcc))
}
#[cfg(feature = "codec-av1")]
fn build_av01(width: u16, height: u16, av1c: &[u8]) -> Vec<u8> {
visual_sample_entry(b"av01", width, height, &bx(b"av1C", av1c))
}
#[cfg(feature = "codec-vvc")]
fn build_vvc1(width: u16, height: u16, vvcc: &[u8]) -> Vec<u8> {
visual_sample_entry(b"vvc1", width, height, &bx(b"vvcC", vvcc))
}
#[cfg(feature = "codec-h265")]
fn build_hvc1(width: u16, height: u16, hvcc: &[u8]) -> Vec<u8> {
visual_sample_entry(b"hvc1", width, height, &bx(b"hvcC", hvcc))
}
fn build_mvex(has_audio: bool) -> Vec<u8> {
let trex = |track_id: u32| {
let mut t = Vec::new();
t.extend_from_slice(&track_id.to_be_bytes());
t.extend_from_slice(&1u32.to_be_bytes()); t.extend_from_slice(&0u32.to_be_bytes()); t.extend_from_slice(&0u32.to_be_bytes()); t.extend_from_slice(&0u32.to_be_bytes()); full(b"trex", 0, 0, &t)
};
let mut body = trex(VIDEO_TRACK_ID);
if has_audio {
body.extend_from_slice(&trex(AUDIO_TRACK_ID));
}
bx(b"mvex", &body)
}
const AAC_SAMPLE_RATES: [u32; 13] = [
96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350,
];
fn strip_adts(data: &[u8]) -> Bytes {
if data.len() > 7 && data[0] == 0xFF && (data[1] & 0xF6) == 0xF0 {
let header = if data[1] & 0x01 == 0 { 9 } else { 7 };
if data.len() > header {
return Bytes::copy_from_slice(&data[header..]);
}
}
Bytes::copy_from_slice(data)
}
fn build_mp4a(channels: u16, sample_rate: u32, asc: &[u8]) -> Vec<u8> {
let mut b = Vec::new();
b.extend_from_slice(&[0u8; 6]); b.extend_from_slice(&1u16.to_be_bytes()); b.extend_from_slice(&[0u8; 8]); b.extend_from_slice(&channels.to_be_bytes()); b.extend_from_slice(&16u16.to_be_bytes()); b.extend_from_slice(&0u16.to_be_bytes()); b.extend_from_slice(&0u16.to_be_bytes()); b.extend_from_slice(&(sample_rate << 16).to_be_bytes()); b.extend_from_slice(&build_esds(asc));
bx(b"mp4a", &b)
}
fn descriptor(tag: u8, body: &[u8]) -> Vec<u8> {
let mut v = Vec::with_capacity(2 + body.len());
v.push(tag);
v.push(body.len() as u8);
v.extend_from_slice(body);
v
}
fn build_esds(asc: &[u8]) -> Vec<u8> {
let dsi = descriptor(0x05, asc); let mut dcd = Vec::new();
dcd.push(0x40); dcd.push(0x15); dcd.extend_from_slice(&[0, 0, 0]); dcd.extend_from_slice(&0u32.to_be_bytes()); dcd.extend_from_slice(&0u32.to_be_bytes()); dcd.extend_from_slice(&dsi);
let dcd = descriptor(0x04, &dcd);
let mut es = Vec::new();
es.extend_from_slice(&0u16.to_be_bytes()); es.push(0); es.extend_from_slice(&dcd);
es.extend_from_slice(&descriptor(0x06, &[0x02])); let es = descriptor(0x03, &es); full(b"esds", 0, 0, &es)
}
fn sample_flags(is_key: bool) -> u32 {
if is_key {
0x0200_0000 } else {
0x0101_0000 }
}
struct TrackFrag<'a> {
track_id: u32,
base_decode_time: u64,
samples: &'a [Sample],
durations: Vec<u64>,
}
fn build_moof(seq: u32, tracks: &[TrackFrag]) -> Vec<u8> {
let mfhd = full(b"mfhd", 0, 0, &seq.to_be_bytes());
let mut moof_body = Vec::new();
moof_body.extend_from_slice(&mfhd);
let mut data_offset_positions = Vec::with_capacity(tracks.len());
for t in tracks {
let tfhd = full(b"tfhd", 0, 0x0002_0000, &t.track_id.to_be_bytes());
let tfdt = full(b"tfdt", 1, 0, &t.base_decode_time.to_be_bytes());
let trun_flags: u32 = 0x0001 | 0x0100 | 0x0200 | 0x0400 | 0x0800;
let mut trun_body = Vec::new();
trun_body.extend_from_slice(&(t.samples.len() as u32).to_be_bytes());
let data_offset_pos_in_body = trun_body.len();
trun_body.extend_from_slice(&0i32.to_be_bytes()); for (s, &dur) in t.samples.iter().zip(&t.durations) {
trun_body.extend_from_slice(&(dur as u32).to_be_bytes());
trun_body.extend_from_slice(&(s.data.len() as u32).to_be_bytes());
trun_body.extend_from_slice(&sample_flags(s.is_key).to_be_bytes());
trun_body.extend_from_slice(&s.cts.to_be_bytes());
}
let trun = full(b"trun", 1, trun_flags, &trun_body);
let mut traf_body = Vec::new();
traf_body.extend_from_slice(&tfhd);
traf_body.extend_from_slice(&tfdt);
let trun_pos_in_traf_body = traf_body.len();
traf_body.extend_from_slice(&trun);
let traf = bx(b"traf", &traf_body);
let traf_pos_in_moof_body = moof_body.len();
moof_body.extend_from_slice(&traf);
data_offset_positions
.push(traf_pos_in_moof_body + 8 + trun_pos_in_traf_body + 12 + data_offset_pos_in_body);
}
let mut moof = bx(b"moof", &moof_body);
let mdat_data_start = moof.len() + 8;
let mut cumulative = 0usize;
for (i, t) in tracks.iter().enumerate() {
let abs = 8 + data_offset_positions[i]; let data_offset = (mdat_data_start + cumulative) as i32;
moof[abs..abs + 4].copy_from_slice(&data_offset.to_be_bytes());
cumulative += t.samples.iter().map(|s| s.data.len()).sum::<usize>();
}
moof
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{CodecId, FrameFlags, MediaFrame};
use bytes::Bytes;
fn h264_config() -> Vec<u8> {
let sps = [0x67u8, 0x42, 0x00, 0x1F, 0xF4, 0x02, 0x80, 0x2D, 0x80];
let pps = [0x68u8, 0xCE, 0x3C, 0x80];
let mut v = vec![0, 0, 0, 1];
v.extend_from_slice(&sps);
v.extend_from_slice(&[0, 0, 0, 1]);
v.extend_from_slice(&pps);
v
}
fn annexb_frame(pts: i64, is_key: bool) -> MediaFrame {
let nal_type = if is_key { 0x65 } else { 0x41 }; let data = Bytes::from(vec![0, 0, 0, 1, nal_type, 0xAA, 0xBB]);
MediaFrame::new_video(pts, pts, data, CodecId::H264, is_key)
}
fn box_types(buf: &[u8]) -> Vec<String> {
let mut types = Vec::new();
let mut i = 0;
while i + 8 <= buf.len() {
let size = u32::from_be_bytes(buf[i..i + 4].try_into().unwrap()) as usize;
let typ = String::from_utf8_lossy(&buf[i + 4..i + 8]).to_string();
types.push(typ);
assert!(size >= 8 && i + size <= buf.len(), "box size out of range");
i += size;
}
assert_eq!(i, buf.len(), "boxes must tile the buffer exactly");
types
}
#[test]
fn init_segment_has_ftyp_moov_and_codec_string() {
let mut m = Fmp4Muxer::new();
let init = m
.init_segment(CodecId::H264, &h264_config())
.unwrap()
.expect("init segment");
assert_eq!(box_types(&init), vec!["ftyp", "moov"]);
assert!(find(&init, b"avc1"));
assert!(find(&init, b"avcC"));
assert!(find(&init, b"mvex"));
assert_eq!(m.codec_string().as_deref(), Some("avc1.42001F"));
}
#[test]
fn unsupported_codec_init_is_rejected() {
let mut m = Fmp4Muxer::new();
assert!(m.init_segment(CodecId::VP9, &[0, 0]).is_err());
}
#[cfg(feature = "codec-h265")]
#[test]
fn h265_init_segment_has_hvc1_and_hvcc() {
use crate::codec::testutil::BitWriter;
let mut w = BitWriter::default();
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];
sps.extend_from_slice(&w.bytes());
let mut config = vec![0, 0, 0, 1, 0x40, 0x01, 0xAA]; config.extend_from_slice(&[0, 0, 0, 1]);
config.extend_from_slice(&sps);
config.extend_from_slice(&[0, 0, 0, 1, 0x44, 0x01, 0xBB]);
let mut m = Fmp4Muxer::new();
let init = m
.init_segment(CodecId::H265, &config)
.unwrap()
.expect("init segment");
assert_eq!(box_types(&init), vec!["ftyp", "moov"]);
assert!(find(&init, b"hvc1"), "hvc1 sample entry present");
assert!(find(&init, b"hvcC"), "hvcC decoder config present");
assert!(
m.codec_string()
.as_deref()
.is_some_and(|s| s.starts_with("hvc1.")),
"HEVC codec string"
);
}
#[cfg(feature = "codec-av1")]
#[test]
fn av1_init_segment_has_av01_and_av1c() {
use crate::codec::testutil::BitWriter;
let mut w = BitWriter::default();
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);
w.bits(1079, 12);
w.align();
let payload = w.bytes();
let mut config = vec![0x0A, payload.len() as u8];
config.extend_from_slice(&payload);
let mut m = Fmp4Muxer::new();
let init = m
.init_segment(CodecId::AV1, &config)
.unwrap()
.expect("av1 init segment");
assert_eq!(box_types(&init), vec!["ftyp", "moov"]);
assert!(find(&init, b"av01"), "av01 sample entry");
assert!(find(&init, b"av1C"), "av1C config box");
assert_eq!(m.codec_string().as_deref(), Some("av01.0.01M.08"));
m.start_segment().unwrap();
let mut frame = MediaFrame::new_video(
0,
0,
Bytes::from(vec![0x32, 0x02, 0xAA, 0xBB]), CodecId::AV1,
true,
);
frame.duration = Some(33);
m.write(&frame).unwrap();
let frag = m.finish_segment().unwrap();
assert_eq!(box_types(&frag), vec!["moof", "mdat"]);
assert!(find(&frag, &[0x32, 0x02, 0xAA, 0xBB]));
}
#[test]
fn fragment_has_moof_mdat_and_correct_sample_count() {
let mut m = Fmp4Muxer::new();
m.init_segment(CodecId::H264, &h264_config()).unwrap();
m.start_segment().unwrap();
m.write(&annexb_frame(0, true)).unwrap();
m.write(&annexb_frame(40, false)).unwrap();
m.write(&annexb_frame(80, false)).unwrap();
let audio = MediaFrame::new_audio(80, Bytes::from_static(b"aac"), CodecId::AAC);
m.write(&audio).unwrap();
let frag = m.finish_segment().unwrap();
assert_eq!(box_types(&frag), vec!["moof", "mdat"]);
assert!(find(&frag, b"tfdt"));
assert!(find(&frag, b"trun"));
let trun_at = position(&frag, b"trun").expect("trun present");
let count = u32::from_be_bytes(frag[trun_at + 8..trun_at + 12].try_into().unwrap());
assert_eq!(count, 3, "three video samples, audio skipped");
}
#[test]
fn base_decode_time_advances_across_fragments() {
let mut m = Fmp4Muxer::new();
m.init_segment(CodecId::H264, &h264_config()).unwrap();
m.start_segment().unwrap();
m.write(&annexb_frame(0, true)).unwrap();
m.write(&annexb_frame(40, false)).unwrap();
let _ = m.finish_segment().unwrap();
assert_eq!(m.base_decode_time, 80);
m.start_segment().unwrap();
m.write(&annexb_frame(80, true)).unwrap();
let frag2 = m.finish_segment().unwrap();
let tfdt_at = position(&frag2, b"tfdt").unwrap();
let base = u64::from_be_bytes(frag2[tfdt_at + 8..tfdt_at + 16].try_into().unwrap());
assert_eq!(base, 80);
}
#[cfg(feature = "codec-vvc")]
#[test]
fn vvc_init_segment_has_vvc1_and_vvcc() {
use crate::codec::testutil::BitWriter;
let mut w = BitWriter::default();
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);
w.ue(1080);
w.bit(0); let mut sps = vec![0x00u8, 0x79]; sps.extend_from_slice(&w.bytes());
let mut config = vec![0, 0, 0, 1];
config.extend_from_slice(&sps);
let mut m = Fmp4Muxer::new();
let init = m
.init_segment(CodecId::VVC, &config)
.unwrap()
.expect("vvc init segment");
assert_eq!(box_types(&init), vec!["ftyp", "moov"]);
assert!(find(&init, b"vvc1"), "vvc1 sample entry");
assert!(find(&init, b"vvcC"), "vvcC config box");
assert_eq!(m.codec_string().as_deref(), Some("vvc1.0.L0"));
m.start_segment().unwrap();
let mut frame = MediaFrame::new_video(
0,
0,
Bytes::from(vec![0, 0, 0, 1, 0x00, 0x39, 0xAA]), CodecId::VVC,
true,
);
frame.duration = Some(40);
m.write(&frame).unwrap();
let frag = m.finish_segment().unwrap();
assert_eq!(box_types(&frag), vec!["moof", "mdat"]);
assert!(find(&frag, &[0x00, 0x00, 0x00, 0x03, 0x00, 0x39, 0xAA]));
}
fn aac_asc() -> Vec<u8> {
vec![0x12, 0x10]
}
#[test]
fn init_with_audio_has_video_and_audio_traks() {
let mut m = Fmp4Muxer::new();
let configs = [
(CodecId::H264, Bytes::from(h264_config())),
(CodecId::AAC, Bytes::from(aac_asc())),
];
let init = m.build_init_from(&configs).unwrap().expect("init segment");
assert_eq!(box_types(&init), vec!["ftyp", "moov"]);
assert!(find(&init, b"avc1") && find(&init, b"avcC"), "video entry");
assert!(find(&init, b"mp4a") && find(&init, b"esds"), "audio entry");
assert!(find(&init, b"SoundHandler"), "audio handler");
assert!(find(&init, b"VideoHandler"), "video handler");
assert_eq!(
init.windows(4).filter(|w| *w == b"trak").count(),
2,
"two traks"
);
assert_eq!(init.windows(4).filter(|w| *w == b"trex").count(), 2);
let cs = m.codec_string().unwrap();
assert!(
cs.contains("avc1.") && cs.contains("mp4a.40.2"),
"codecs: {cs}"
);
}
#[test]
fn fragment_muxes_audio_alongside_video() {
let mut m = Fmp4Muxer::new();
m.build_init_from(&[
(CodecId::H264, Bytes::from(h264_config())),
(CodecId::AAC, Bytes::from(aac_asc())),
])
.unwrap();
m.start_segment().unwrap();
m.write(&annexb_frame(0, true)).unwrap();
m.write(&annexb_frame(40, false)).unwrap();
m.write(&MediaFrame::new_audio(
0,
Bytes::from_static(&[0x01, 0x02, 0x03]),
CodecId::AAC,
))
.unwrap();
m.write(&MediaFrame::new_audio(
21,
Bytes::from_static(&[0x04, 0x05]),
CodecId::AAC,
))
.unwrap();
let frag = m.finish_segment().unwrap();
assert_eq!(box_types(&frag), vec!["moof", "mdat"]);
assert_eq!(
frag.windows(4).filter(|w| *w == b"traf").count(),
2,
"two trafs"
);
assert!(find(&frag, &[0x01, 0x02, 0x03]), "audio sample 1 in mdat");
assert!(find(&frag, &[0x04, 0x05]), "audio sample 2 in mdat");
}
#[test]
fn strip_adts_removes_header() {
let adts = [0xFF, 0xF1, 0x50, 0x80, 0x00, 0x1F, 0xFC, 0xAA, 0xBB];
assert_eq!(&strip_adts(&adts)[..], &[0xAA, 0xBB]);
assert_eq!(&strip_adts(&[0x01, 0x02])[..], &[0x01, 0x02]);
}
#[test]
fn config_without_sps_errors() {
let mut m = Fmp4Muxer::new();
let cfg = vec![0, 0, 0, 1, 0x68, 0xCE, 0x3C, 0x80];
assert!(m.init_segment(CodecId::H264, &cfg).is_err());
}
#[test]
fn empty_fragment_yields_no_bytes() {
let mut m = Fmp4Muxer::new();
m.start_segment().unwrap();
assert!(m.finish_segment().unwrap().is_empty());
}
fn find(buf: &[u8], needle: &[u8]) -> bool {
position(buf, needle).is_some()
}
fn position(buf: &[u8], needle: &[u8]) -> Option<usize> {
buf.windows(needle.len()).position(|w| w == needle)
}
#[test]
fn integrates_with_segmenter_via_ext_x_map() {
use crate::packager::{HlsSegmenter, Packager};
use crate::testing::InMemoryStorage;
use crate::traits::StorageBackend;
tokio_test_block(async {
let store = InMemoryStorage::new();
let mut seg = HlsSegmenter::new(Fmp4Muxer::new(), store.clone(), "live/fmp4", 2, 5);
let mut cfg =
MediaFrame::new_video(0, 0, Bytes::from(h264_config()), CodecId::H264, true);
cfg.flags |= FrameFlags::CONFIG;
seg.push(&cfg).await.unwrap();
for i in 0..6 {
seg.push(&annexb_frame(i * 1000, true)).await.unwrap();
}
seg.finish().await.unwrap();
assert!(store.get("live/fmp4/init.m4s").await.is_ok());
let pl = String::from_utf8(store.get("live/fmp4/index.m3u8").await.unwrap().to_vec())
.unwrap();
assert!(pl.contains("#EXT-X-MAP:URI=\"init.m4s\""));
});
}
#[test]
fn segmenter_builds_init_with_audio_from_both_configs() {
use crate::packager::{HlsSegmenter, Packager};
use crate::testing::InMemoryStorage;
use crate::traits::StorageBackend;
tokio_test_block(async {
let store = InMemoryStorage::new();
let mut seg = HlsSegmenter::new(Fmp4Muxer::new(), store.clone(), "live/av", 2, 5);
let mut vcfg =
MediaFrame::new_video(0, 0, Bytes::from(h264_config()), CodecId::H264, true);
vcfg.flags |= FrameFlags::CONFIG;
seg.push(&vcfg).await.unwrap();
let mut acfg = MediaFrame::new_audio(0, Bytes::from(aac_asc()), CodecId::AAC);
acfg.flags |= FrameFlags::CONFIG;
seg.push(&acfg).await.unwrap();
for i in 0..6 {
seg.push(&annexb_frame(i * 1000, true)).await.unwrap();
seg.push(&MediaFrame::new_audio(
i * 1000,
Bytes::from_static(&[0xAA, 0xBB]),
CodecId::AAC,
))
.await
.unwrap();
}
seg.finish().await.unwrap();
let init = store.get("live/av/init.m4s").await.expect("init written");
assert!(find(&init, b"mp4a"), "init has the audio sample entry");
assert!(find(&init, b"avc1"), "init has the video sample entry");
});
}
fn tokio_test_block<F: std::future::Future>(fut: F) -> F::Output {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(fut)
}
}