use super::Muxer;
use crate::{CodecId, MediaFrame, Result, StreamError};
use bytes::Bytes;
const TIMESCALE: u32 = 1000; const TRACK_ID: u32 = 1;
const FALLBACK_DURATION: u64 = TIMESCALE as u64 / 30;
struct Sample {
data: Bytes,
dts: i64,
cts: i32, is_key: bool,
duration: Option<u64>,
}
pub struct Fmp4Muxer {
codec: Option<CodecId>,
width: u16,
height: u16,
codec_str: Option<String>,
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,
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-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 durations(&self) -> Vec<u64> {
let n = self.samples.len();
let mut out = Vec::with_capacity(n);
for i in 0..n {
let dur = if i + 1 < n {
(self.samples[i + 1].dts - self.samples[i].dts).max(0) as u64
} else {
self.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_video() {
return Ok(());
}
let is_nal = matches!(self.codec, Some(CodecId::H264) | 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> {
if self.samples.is_empty() {
return Ok(Bytes::new());
}
let durations = self.durations();
let total: u64 = durations.iter().sum();
let moof = build_moof(self.seq, self.base_decode_time, &self.samples, &durations);
let mut out = moof;
let mdat_len: usize = self.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 s in &self.samples {
out.extend_from_slice(&s.data);
}
self.seq += 1;
self.base_decode_time += total;
self.samples.clear();
Ok(Bytes::from(out))
}
fn init_segment(&mut self, codec: CodecId, config_record: &[u8]) -> Result<Option<Bytes>> {
let sample_entry = match codec {
CodecId::H264 => self.ingest_h264_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);
let mut seg = build_ftyp();
seg.extend_from_slice(&build_moov(self.width, self.height, &sample_entry));
Ok(Some(Bytes::from(seg)))
}
fn codec_string(&self) -> Option<String> {
self.codec_str.clone()
}
}
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, entry: &[u8]) -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&build_mvhd());
body.extend_from_slice(&build_trak(width, height, entry));
body.extend_from_slice(&build_mvex());
bx(b"moov", &body)
}
fn build_mvhd() -> 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(&2u32.to_be_bytes()); full(b"mvhd", 0, 0, &b)
}
fn build_trak(width: u16, height: u16, entry: &[u8]) -> Vec<u8> {
let mut b = Vec::new();
b.extend_from_slice(&build_tkhd(width, height));
b.extend_from_slice(&build_mdia(entry));
bx(b"trak", &b)
}
fn build_tkhd(width: u16, height: 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(&0u16.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(entry: &[u8]) -> Vec<u8> {
let mut b = Vec::new();
b.extend_from_slice(&build_mdhd());
b.extend_from_slice(&build_hdlr());
b.extend_from_slice(&build_minf(entry));
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() -> Vec<u8> {
let mut b = Vec::new();
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");
full(b"hdlr", 0, 0, &b)
}
fn build_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_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))
}
fn build_mvex() -> Vec<u8> {
let mut trex = Vec::new();
trex.extend_from_slice(&TRACK_ID.to_be_bytes());
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))
}
fn sample_flags(is_key: bool) -> u32 {
if is_key {
0x0200_0000 } else {
0x0101_0000 }
}
fn build_moof(seq: u32, base_decode_time: u64, samples: &[Sample], durations: &[u64]) -> Vec<u8> {
let mfhd = full(b"mfhd", 0, 0, &seq.to_be_bytes());
let tfhd = full(b"tfhd", 0, 0x0002_0000, &TRACK_ID.to_be_bytes());
let tfdt = full(b"tfdt", 1, 0, &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(&(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 samples.iter().zip(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 mut moof_body = Vec::new();
moof_body.extend_from_slice(&mfhd);
let traf_pos_in_moof_body = moof_body.len();
moof_body.extend_from_slice(&traf);
let mut moof = bx(b"moof", &moof_body);
let off = 8 + traf_pos_in_moof_body + 8 + trun_pos_in_traf_body + 12 + data_offset_pos_in_body;
let data_offset = (moof.len() + 8) as i32;
moof[off..off + 4].copy_from_slice(&data_offset.to_be_bytes());
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-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]));
}
#[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\""));
});
}
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)
}
}