use crate::video_metadata::has_video_extension;
use crate::video_transcode::TranscodeError;
use ffmpeg_next as ffmpeg;
use ffmpeg_next::Rescale;
use percent_encoding::{AsciiSet, NON_ALPHANUMERIC, utf8_percent_encode};
use std::path::Path;
pub const REMUX_SEGMENT_TARGET_SECS: f64 = 4.0;
const FMP4_MOVFLAGS: &str = "frag_keyframe+empty_moov+default_base_moof";
const PLAYLIST_SUFFIX: &str = "-remux.m3u8";
const INIT_SUFFIX: &str = "-remux-init.mp4";
const SEGMENT_INFIX: &str = "-remux-";
const SEGMENT_SUFFIX: &str = ".m4s";
pub const PLAYLIST_CONTENT_TYPE: &str = "application/vnd.apple.mpegurl";
pub const INIT_CONTENT_TYPE: &str = "video/mp4";
pub const SEGMENT_CONTENT_TYPE: &str = "video/iso.segment";
const PLAYLIST_URI_ENCODE_SET: &AsciiSet = &NON_ALPHANUMERIC
.remove(b'-')
.remove(b'.')
.remove(b'_')
.remove(b'~');
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RemuxPart {
Playlist,
Init,
Segment(u32),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RemuxRequest<'a> {
pub video_path: &'a str,
pub part: RemuxPart,
}
#[must_use]
pub fn parse_remux_request(path: &str) -> Option<RemuxRequest<'_>> {
fn with_part(video_path: &str, part: RemuxPart) -> Option<RemuxRequest<'_>> {
has_video_extension(video_path).then_some(RemuxRequest { video_path, part })
}
if let Some(video_path) = path.strip_suffix(PLAYLIST_SUFFIX) {
return with_part(video_path, RemuxPart::Playlist);
}
if let Some(video_path) = path.strip_suffix(INIT_SUFFIX) {
return with_part(video_path, RemuxPart::Init);
}
if let Some(rest) = path.strip_suffix(SEGMENT_SUFFIX)
&& let Some((video_path, index)) = rest.rsplit_once(SEGMENT_INFIX)
&& let Ok(index) = index.parse::<u32>()
{
return with_part(video_path, RemuxPart::Segment(index));
}
None
}
#[must_use]
fn init_uri(base_name: &str) -> String {
encode_uri(&format!("{base_name}{INIT_SUFFIX}"))
}
#[must_use]
fn segment_uri(base_name: &str, index: u32) -> String {
encode_uri(&format!(
"{base_name}{SEGMENT_INFIX}{index:03}{SEGMENT_SUFFIX}"
))
}
fn encode_uri(raw: &str) -> String {
utf8_percent_encode(raw, PLAYLIST_URI_ENCODE_SET).to_string()
}
#[derive(Debug, Clone)]
struct RemuxSegmentation {
edges: Vec<i64>,
time_base: ffmpeg::Rational,
}
impl RemuxSegmentation {
#[must_use]
fn segment_count(&self) -> u32 {
u32::try_from(self.edges.len().saturating_sub(1)).unwrap_or(u32::MAX)
}
#[must_use]
fn origin(&self) -> i64 {
self.edges.first().copied().unwrap_or(0)
}
#[must_use]
fn window(&self, index: u32) -> Option<(i64, i64)> {
let i = usize::try_from(index).ok()?;
Some((*self.edges.get(i)?, *self.edges.get(i + 1)?))
}
#[must_use]
fn mux_window(&self, index: u32) -> Option<(i64, Option<i64>)> {
let (start, end) = self.window(index)?;
let is_last = index + 1 >= self.segment_count();
Some((start, (!is_last).then_some(end)))
}
#[must_use]
fn durations_secs(&self) -> Vec<f64> {
self.edges
.windows(2)
.map(|pair| self.to_secs(pair[1] - pair[0]))
.collect()
}
fn to_secs(&self, ts: i64) -> f64 {
ts as f64 * f64::from(self.time_base.numerator()) / f64::from(self.time_base.denominator())
}
}
#[must_use]
pub fn derive_segment_edges(keyframe_ts: &[i64], end_ts: i64, min_segment_ts: i64) -> Vec<i64> {
let Some(&origin) = keyframe_ts.first() else {
return Vec::new();
};
if end_ts <= origin {
return Vec::new();
}
let min_segment_ts = min_segment_ts.max(1);
let mut edges = vec![origin];
for &ts in keyframe_ts {
let last = edges.last().copied().unwrap_or(origin);
if ts > last && ts < end_ts && ts - last >= min_segment_ts {
edges.push(ts);
}
}
let last = edges.last().copied().unwrap_or(origin);
if end_ts <= last {
return Vec::new();
}
if edges.len() > 1 && end_ts - last < min_segment_ts / 2 {
edges.pop();
}
edges.push(end_ts);
edges
}
fn build_segmentation(
input: &mut ffmpeg::format::context::Input,
video_stream_index: usize,
) -> Result<RemuxSegmentation, TranscodeError> {
let (time_base, duration_ts) = {
let stream = input.stream(video_stream_index).ok_or_else(|| {
TranscodeError::RemuxFailed("video stream disappeared while segmenting".to_string())
})?;
let time_base = stream.time_base();
let duration_ts = if stream.duration() > 0 {
stream.duration()
} else {
let container = input.duration();
if container > 0 {
container.rescale(
ffmpeg::Rational::new(1, ffmpeg::ffi::AV_TIME_BASE),
time_base,
)
} else {
0
}
};
(time_base, duration_ts)
};
let keyframes = read_keyframe_timestamps(input, video_stream_index);
segmentation_from_keyframes(&keyframes, time_base, duration_ts)
}
fn segmentation_from_keyframes(
keyframes: &[i64],
time_base: ffmpeg::Rational,
duration_ts: i64,
) -> Result<RemuxSegmentation, TranscodeError> {
let Some(&first_keyframe) = keyframes.first() else {
return Err(TranscodeError::NoKeyframeIndex);
};
let end_ts = first_keyframe.saturating_add(duration_ts);
let min_segment_ts = (REMUX_SEGMENT_TARGET_SECS * f64::from(time_base.denominator())
/ f64::from(time_base.numerator()))
.round() as i64;
let edges = derive_segment_edges(keyframes, end_ts, min_segment_ts);
if edges.len() < 2 {
return Err(TranscodeError::RemuxFailed(format!(
"could not derive segment boundaries \
(first keyframe {first_keyframe}, end {end_ts}, time base {}/{})",
time_base.numerator(),
time_base.denominator()
)));
}
Ok(RemuxSegmentation { edges, time_base })
}
fn read_keyframe_timestamps(
input: &mut ffmpeg::format::context::Input,
stream_index: usize,
) -> Vec<i64> {
let Some(stream) = input.stream(stream_index) else {
return Vec::new();
};
unsafe {
let stream_ptr = stream.as_ptr().cast_mut();
let count = ffmpeg::ffi::avformat_index_get_entries_count(stream_ptr);
(0..count)
.filter_map(|i| {
let entry = ffmpeg::ffi::avformat_index_get_entry(stream_ptr, i).as_ref()?;
let is_keyframe = entry.flags() & ffmpeg::ffi::AVINDEX_KEYFRAME != 0;
let has_timestamp = entry.timestamp != ffmpeg::ffi::AV_NOPTS_VALUE;
(is_keyframe && has_timestamp).then_some(entry.timestamp)
})
.collect()
}
}
#[must_use]
pub fn build_remux_playlist(base_name: &str, durations_secs: &[f64]) -> String {
let target_duration = durations_secs
.iter()
.copied()
.fold(0.0f64, f64::max)
.ceil()
.max(1.0) as u32;
let mut playlist = String::with_capacity(128 + durations_secs.len() * 48);
playlist.push_str("#EXTM3U\n");
playlist.push_str("#EXT-X-VERSION:7\n");
playlist.push_str("#EXT-X-PLAYLIST-TYPE:VOD\n");
playlist.push_str("#EXT-X-INDEPENDENT-SEGMENTS\n");
playlist.push_str(&format!("#EXT-X-TARGETDURATION:{target_duration}\n"));
playlist.push_str("#EXT-X-MEDIA-SEQUENCE:0\n");
playlist.push_str(&format!(
"#EXT-X-MAP:URI=\"{}\"\n",
init_uri(base_name).replace('"', "%22")
));
for (index, duration) in durations_secs.iter().enumerate() {
let index = u32::try_from(index).unwrap_or(u32::MAX);
playlist.push_str(&format!("#EXTINF:{duration:.3},\n"));
playlist.push_str(&segment_uri(base_name, index));
playlist.push('\n');
}
playlist.push_str("#EXT-X-ENDLIST\n");
playlist
}
pub fn generate_remux_playlist(source: &Path, base_name: &str) -> Result<String, TranscodeError> {
let mut input = open_input(source)?;
let video_stream_index = best_video_stream(&input, source)?;
let segmentation = build_segmentation(&mut input, video_stream_index)?;
tracing::debug!(
"remux playlist for {}: {} keyframe-aligned segments",
source.display(),
segmentation.segment_count()
);
Ok(build_remux_playlist(
base_name,
&segmentation.durations_secs(),
))
}
struct MuxedOutput {
bytes: Vec<u8>,
first_dts: Vec<Option<i64>>,
}
pub fn generate_remux_init(source: &Path) -> Result<Vec<u8>, TranscodeError> {
let mut input = open_input(source)?;
let muxed = mux_fragmented(&mut input, source, None)?;
let (init, _media) = split_fragmented_output(&muxed.bytes)?;
Ok(init)
}
pub fn generate_remux_segment(source: &Path, index: u32) -> Result<Vec<u8>, TranscodeError> {
let mut input = open_input(source)?;
let video_stream_index = best_video_stream(&input, source)?;
let segmentation = build_segmentation(&mut input, video_stream_index)?;
let (start, end) = segmentation
.mux_window(index)
.ok_or(TranscodeError::SegmentOutOfRange {
segment_index: index,
video_duration: segmentation.durations_secs().iter().sum::<f64>(),
})?;
let window = MuxWindow {
origin: segmentation.origin(),
start,
end,
};
let muxed = mux_fragmented(&mut input, source, Some(window))?;
let (_init, mut media) = split_fragmented_output(&muxed.bytes)?;
patch_tfdt_bases(&mut media, &muxed.first_dts)?;
Ok(media)
}
fn open_input(source: &Path) -> Result<ffmpeg::format::context::Input, TranscodeError> {
ffmpeg::format::input(source).map_err(|e| TranscodeError::OpenFailed {
path: source.to_path_buf(),
source: e,
})
}
fn best_video_stream(
input: &ffmpeg::format::context::Input,
source: &Path,
) -> Result<usize, TranscodeError> {
input
.streams()
.best(ffmpeg::media::Type::Video)
.map(|stream| stream.index())
.ok_or_else(|| TranscodeError::NoVideoStream {
path: source.to_path_buf(),
})
}
#[derive(Debug, Clone, Copy)]
struct MuxWindow {
origin: i64,
start: i64,
end: Option<i64>,
}
struct MappedStream {
input_index: usize,
output_index: usize,
input_time_base: ffmpeg::Rational,
output_time_base: ffmpeg::Rational,
window: Option<MuxWindow>,
finished: bool,
}
fn mux_fragmented(
input: &mut ffmpeg::format::context::Input,
source: &Path,
window: Option<MuxWindow>,
) -> Result<MuxedOutput, TranscodeError> {
let video_stream_index = best_video_stream(input, source)?;
let audio_stream_index = input
.streams()
.best(ffmpeg::media::Type::Audio)
.map(|stream| stream.index());
let video_time_base = input
.stream(video_stream_index)
.ok_or_else(|| TranscodeError::NoVideoStream {
path: source.to_path_buf(),
})?
.time_base();
let temp_file = tempfile::Builder::new()
.prefix("mbr_remux_")
.suffix(".mp4")
.tempfile()
.map_err(TranscodeError::Io)?;
let mut mapped: Vec<MappedStream> = Vec::with_capacity(2);
let mut first_dts: Vec<Option<i64>> = Vec::with_capacity(2);
{
let mut output = ffmpeg::format::output_as(temp_file.path(), "mp4").map_err(|e| {
TranscodeError::RemuxFailed(format!("failed to create fMP4 output: {e}"))
})?;
for input_index in [Some(video_stream_index), audio_stream_index]
.into_iter()
.flatten()
{
let (parameters, input_time_base) = {
let stream = input.stream(input_index).ok_or_else(|| {
TranscodeError::RemuxFailed(format!("stream {input_index} disappeared"))
})?;
(stream.parameters(), stream.time_base())
};
let output_index = {
let mut out_stream = output
.add_stream(None::<ffmpeg::Codec>)
.map_err(|e| TranscodeError::RemuxFailed(format!("add_stream failed: {e}")))?;
out_stream.set_parameters(parameters);
out_stream.set_time_base(input_time_base);
unsafe {
(*out_stream.parameters().as_mut_ptr()).codec_tag = 0;
}
out_stream.index()
};
debug_assert_eq!(
output_index,
mapped.len(),
"output stream index must match its slot"
);
mapped.push(MappedStream {
input_index,
output_index,
input_time_base,
output_time_base: input_time_base,
window: None,
finished: false,
});
first_dts.push(None);
}
let mut options = ffmpeg::Dictionary::new();
options.set("movflags", FMP4_MOVFLAGS);
options.set("avoid_negative_ts", "disabled");
output.write_header_with(options).map_err(|e| {
TranscodeError::RemuxFailed(format!("failed to write fMP4 header: {e}"))
})?;
for entry in &mut mapped {
entry.output_time_base = output
.stream(entry.output_index)
.map_or(entry.input_time_base, |stream| stream.time_base());
entry.window = window.map(|w| MuxWindow {
origin: w.origin.rescale(video_time_base, entry.input_time_base),
start: w.start.rescale(video_time_base, entry.input_time_base),
end: w
.end
.map(|end| end.rescale(video_time_base, entry.input_time_base)),
});
}
if let Some(w) = window
&& w.start > w.origin
{
let start_us = w.start.rescale(
video_time_base,
ffmpeg::Rational::new(1, ffmpeg::ffi::AV_TIME_BASE),
);
input.seek(start_us, ..start_us).map_err(|e| {
TranscodeError::RemuxFailed(format!("seek to {start_us}us failed: {e}"))
})?;
}
copy_packets(input, &mut output, &mut mapped, &mut first_dts)?;
let trailer = unsafe { ffmpeg::ffi::av_write_trailer(output.as_mut_ptr()) };
if trailer < 0 {
return Err(TranscodeError::RemuxFailed(format!(
"failed to write fMP4 trailer: {:?}",
ffmpeg::Error::from(trailer)
)));
}
}
let bytes = std::fs::read(temp_file.path())?;
Ok(MuxedOutput { bytes, first_dts })
}
fn copy_packets(
input: &mut ffmpeg::format::context::Input,
output: &mut ffmpeg::format::context::Output,
mapped: &mut [MappedStream],
first_dts: &mut [Option<i64>],
) -> Result<(), TranscodeError> {
if mapped.iter().all(|entry| {
entry
.window
.is_none_or(|w| w.end.is_some_and(|end| w.start >= end))
}) {
return Ok(());
}
for (stream, mut packet) in input.packets() {
let stream_index = stream.index();
let Some(slot) = mapped
.iter()
.position(|entry| entry.input_index == stream_index)
else {
continue;
};
let Some(timestamp) = packet.dts().or_else(|| packet.pts()) else {
continue;
};
let Some(window) = mapped[slot].window else {
continue;
};
if window.end.is_some_and(|end| timestamp >= end) {
mapped[slot].finished = true;
if mapped.iter().all(|entry| entry.finished) {
break;
}
continue;
}
if timestamp < window.start {
continue;
}
packet.set_stream(mapped[slot].output_index);
packet.set_pts(packet.pts().map(|pts| pts - window.origin));
packet.set_dts(packet.dts().map(|dts| dts - window.origin));
packet.rescale_ts(mapped[slot].input_time_base, mapped[slot].output_time_base);
packet.set_position(-1);
if first_dts[slot].is_none() {
first_dts[slot] = packet.dts().or_else(|| packet.pts());
}
packet
.write_interleaved(output)
.map_err(|e| TranscodeError::RemuxFailed(format!("failed to write packet: {e}")))?;
}
Ok(())
}
const BOX_HEADER_LEN: usize = 8;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct BoxSpan {
kind: [u8; 4],
start: usize,
payload: usize,
end: usize,
}
impl BoxSpan {
fn is(&self, kind: &[u8; 4]) -> bool {
&self.kind == kind
}
}
fn scan_boxes(data: &[u8], range: std::ops::Range<usize>) -> Result<Vec<BoxSpan>, TranscodeError> {
if range.end > data.len() || range.start > range.end {
return Err(TranscodeError::RemuxFailed(format!(
"MP4 box range {range:?} is outside a {}-byte buffer",
data.len()
)));
}
let mut boxes = Vec::new();
let mut offset = range.start;
while offset + BOX_HEADER_LEN <= range.end {
let size_field = u32::from_be_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
]) as u64;
let kind = [
data[offset + 4],
data[offset + 5],
data[offset + 6],
data[offset + 7],
];
let (size, payload_offset) = match size_field {
1 => {
if offset + 16 > range.end {
return Err(TranscodeError::RemuxFailed(
"truncated 64-bit MP4 box header".to_string(),
));
}
let mut wide = [0u8; 8];
wide.copy_from_slice(&data[offset + 8..offset + 16]);
(u64::from_be_bytes(wide), offset + 16)
}
0 => ((range.end - offset) as u64, offset + BOX_HEADER_LEN),
size => (size, offset + BOX_HEADER_LEN),
};
let size = usize::try_from(size).map_err(|_| {
TranscodeError::RemuxFailed("MP4 box size exceeds addressable range".to_string())
})?;
let end = offset
.checked_add(size)
.ok_or_else(|| TranscodeError::RemuxFailed("MP4 box size overflows".to_string()))?;
if size < BOX_HEADER_LEN || end > range.end {
return Err(TranscodeError::RemuxFailed(format!(
"MP4 box '{}' has invalid size {size} at offset {offset}",
String::from_utf8_lossy(&kind)
)));
}
boxes.push(BoxSpan {
kind,
start: offset,
payload: payload_offset,
end,
});
offset = end;
}
Ok(boxes)
}
fn split_fragmented_output(data: &[u8]) -> Result<(Vec<u8>, Vec<u8>), TranscodeError> {
let boxes = scan_boxes(data, 0..data.len())?;
let mut init = Vec::new();
let mut media = Vec::new();
let mut saw_moov = false;
for span in &boxes {
let bytes = &data[span.start..span.end];
if span.is(b"ftyp") || span.is(b"moov") {
saw_moov |= span.is(b"moov");
init.extend_from_slice(bytes);
} else if span.is(b"styp") || span.is(b"sidx") || span.is(b"moof") || span.is(b"mdat") {
media.extend_from_slice(bytes);
}
}
if !saw_moov {
return Err(TranscodeError::RemuxFailed(
"fMP4 output has no 'moov' box".to_string(),
));
}
Ok((init, media))
}
fn patch_tfdt_bases(media: &mut [u8], first_dts: &[Option<i64>]) -> Result<usize, TranscodeError> {
let moofs: Vec<BoxSpan> = scan_boxes(media, 0..media.len())?
.into_iter()
.filter(|span| span.is(b"moof"))
.collect();
let mut patched = 0;
for moof in moofs {
for traf in scan_boxes(media, moof.payload..moof.end)?
.into_iter()
.filter(|span| span.is(b"traf"))
{
let children = scan_boxes(media, traf.payload..traf.end)?;
let track_id = children
.iter()
.find(|span| span.is(b"tfhd"))
.and_then(|span| read_u32(media, span.payload + 4))
.ok_or_else(|| {
TranscodeError::RemuxFailed("fragment has no usable 'tfhd'".to_string())
})?;
let offset = usize::try_from(track_id)
.ok()
.and_then(|id| id.checked_sub(1))
.and_then(|slot| first_dts.get(slot).copied())
.flatten()
.unwrap_or(0);
for tfdt in children.iter().filter(|span| span.is(b"tfdt")) {
patch_one_tfdt(media, *tfdt, offset)?;
patched += 1;
}
}
}
Ok(patched)
}
fn patch_one_tfdt(media: &mut [u8], tfdt: BoxSpan, offset: i64) -> Result<(), TranscodeError> {
let version = *media
.get(tfdt.payload)
.ok_or_else(|| TranscodeError::RemuxFailed("truncated 'tfdt' box".to_string()))?;
let value_offset = tfdt.payload + 4;
let malformed =
|| TranscodeError::RemuxFailed("truncated 'tfdt' baseMediaDecodeTime".to_string());
match version {
0 => {
let current = i64::from(read_u32(media, value_offset).ok_or_else(malformed)?);
let updated = u32::try_from(current.saturating_add(offset).max(0)).map_err(|_| {
TranscodeError::RemuxFailed(
"absolute decode time does not fit a version-0 'tfdt'".to_string(),
)
})?;
media
.get_mut(value_offset..value_offset + 4)
.ok_or_else(malformed)?
.copy_from_slice(&updated.to_be_bytes());
}
1 => {
let current = read_u64(media, value_offset).ok_or_else(malformed)?;
let updated = (current as i64).saturating_add(offset).max(0) as u64;
media
.get_mut(value_offset..value_offset + 8)
.ok_or_else(malformed)?
.copy_from_slice(&updated.to_be_bytes());
}
other => {
return Err(TranscodeError::RemuxFailed(format!(
"unsupported 'tfdt' version {other}"
)));
}
}
Ok(())
}
fn read_u32(data: &[u8], offset: usize) -> Option<u32> {
let bytes: [u8; 4] = data.get(offset..offset + 4)?.try_into().ok()?;
Some(u32::from_be_bytes(bytes))
}
fn read_u64(data: &[u8], offset: usize) -> Option<u64> {
let bytes: [u8; 8] = data.get(offset..offset + 8)?.try_into().ok()?;
Some(u64::from_be_bytes(bytes))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_remux_playlist() {
let request = parse_remux_request("videos/demo.mp4-remux.m3u8").expect("playlist");
assert_eq!(request.video_path, "videos/demo.mp4");
assert_eq!(request.part, RemuxPart::Playlist);
}
#[test]
fn test_parse_remux_init() {
let request = parse_remux_request("videos/demo.mp4-remux-init.mp4").expect("init");
assert_eq!(request.video_path, "videos/demo.mp4");
assert_eq!(request.part, RemuxPart::Init);
}
#[test]
fn test_parse_remux_segment() {
let request = parse_remux_request("videos/demo.mp4-remux-007.m4s").expect("segment");
assert_eq!(request.video_path, "videos/demo.mp4");
assert_eq!(request.part, RemuxPart::Segment(7));
}
#[test]
fn test_parse_remux_segment_large_index() {
let request = parse_remux_request("clip.mov-remux-1234.m4s").expect("segment");
assert_eq!(request.video_path, "clip.mov");
assert_eq!(request.part, RemuxPart::Segment(1234));
}
#[test]
fn test_parse_remux_preserves_spaces_in_path() {
let request =
parse_remux_request("videos/Eric Jones - Metal 3.mp4-remux.m3u8").expect("playlist");
assert_eq!(request.video_path, "videos/Eric Jones - Metal 3.mp4");
}
#[test]
fn test_parse_remux_rejects_neighbouring_url_shapes() {
for path in [
"videos/demo-720p.m3u8",
"videos/demo-480p.m3u8",
"videos/demo-720p-005.ts",
"videos/demo-480p-000.ts",
"videos/demo.mp4.cover.jpg",
"videos/demo.mp4.chapters.en.vtt",
"videos/demo.mp4.captions.en.vtt",
"docs/report.pdf.cover.jpg",
"videos/demo.mp4",
"videos/demo.m3u8",
"notes/agenda.md",
"docs/report.pdf-remux.m3u8",
"docs/notes.md-remux-000.m4s",
"docs/report.pdf-remux-init.mp4",
"videos/demo.mp4-remux-abc.m4s",
"videos/demo.mp4-remux-.m4s",
"videos/demo.mp4-remux-000.ts",
] {
assert!(
parse_remux_request(path).is_none(),
"{path} must not parse as a remux request"
);
}
}
#[test]
fn test_remux_urls_are_not_claimed_by_neighbouring_parsers() {
use crate::video_metadata::parse_metadata_request;
use crate::video_transcode::parse_hls_request;
for path in [
"videos/demo.mp4-remux.m3u8",
"videos/demo.mp4-remux-init.mp4",
"videos/demo.mp4-remux-000.m4s",
] {
assert!(
parse_hls_request(path).is_none(),
"{path} must not parse as a transcode request"
);
assert!(
parse_metadata_request(path).is_none(),
"{path} must not parse as a metadata request"
);
}
}
#[test]
fn test_segment_uri_percent_encodes() {
assert_eq!(
segment_uri("Eric Jones - Metal 3.mp4", 2),
"Eric%20Jones%20-%20Metal%203.mp4-remux-002.m4s"
);
assert_eq!(init_uri("demo.mp4"), "demo.mp4-remux-init.mp4");
}
#[test]
fn test_segment_uri_round_trips_through_parse() {
let name = "Eric Jones - Metal 3.mp4";
let uri = segment_uri(name, 5);
let decoded = percent_encoding::percent_decode_str(&uri).decode_utf8_lossy();
let request = parse_remux_request(&decoded).expect("round trip");
assert_eq!(request.video_path, name);
assert_eq!(request.part, RemuxPart::Segment(5));
}
#[test]
fn test_edges_are_keyframe_aligned() {
let keyframes: Vec<i64> = (0..=10).map(|i| i * 2).collect();
let edges = derive_segment_edges(&keyframes, 20, 4);
assert_eq!(edges, vec![0, 4, 8, 12, 16, 20]);
}
#[test]
fn test_edges_start_at_the_first_keyframe() {
let edges = derive_segment_edges(&[3, 9, 15], 20, 4);
assert_eq!(edges, vec![3, 9, 15, 20]);
let edges = derive_segment_edges(&[-4096, 16384, 36864, 57344], 241_664, 40_960);
assert_eq!(edges.first(), Some(&-4096));
assert!(edges.windows(2).all(|pair| pair[0] < pair[1]));
let total: i64 = edges.windows(2).map(|pair| pair[1] - pair[0]).sum();
assert_eq!(total, 241_664 - (-4096));
}
#[test]
fn test_edges_empty_without_keyframes() {
assert!(derive_segment_edges(&[], 1000, 10).is_empty());
}
#[test]
fn test_edges_respect_long_gops() {
let edges = derive_segment_edges(&[0, 30, 60, 90], 120, 4);
assert_eq!(edges, vec![0, 30, 60, 90, 120]);
}
#[test]
fn test_edges_skip_keyframes_closer_than_the_target() {
let keyframes: Vec<i64> = (0..40).collect();
let edges = derive_segment_edges(&keyframes, 40, 10);
assert_eq!(edges, vec![0, 10, 20, 30, 40]);
}
#[test]
fn test_edges_fold_trailing_sliver_into_last_segment() {
let edges = derive_segment_edges(&[0, 10, 20, 39], 40, 10);
assert_eq!(edges, vec![0, 10, 20, 40]);
let edges = derive_segment_edges(&[0, 10, 20, 34], 40, 10);
assert_eq!(edges, vec![0, 10, 20, 34, 40]);
}
#[test]
fn test_edges_never_fold_away_the_only_segment() {
let edges = derive_segment_edges(&[0], 1, 40);
assert_eq!(edges, vec![0, 1]);
}
#[test]
fn test_edges_single_segment_for_short_media() {
let edges = derive_segment_edges(&[0], 3, 40);
assert_eq!(edges, vec![0, 3]);
}
#[test]
fn test_edges_ignore_keyframes_past_the_end() {
let edges = derive_segment_edges(&[0, 10, 500], 20, 10);
assert_eq!(edges, vec![0, 10, 20]);
}
#[test]
fn test_edges_empty_for_zero_duration() {
assert!(derive_segment_edges(&[0], 0, 10).is_empty());
assert!(derive_segment_edges(&[0], -5, 10).is_empty());
}
#[test]
fn test_edges_are_strictly_increasing() {
let edges = derive_segment_edges(&[0, 0, 10, 10, 10, 20], 25, 5);
assert!(
edges.windows(2).all(|pair| pair[0] < pair[1]),
"edges must be strictly increasing: {edges:?}"
);
}
fn segmentation(edges: Vec<i64>, den: i32) -> RemuxSegmentation {
RemuxSegmentation {
edges,
time_base: ffmpeg::Rational::new(1, den),
}
}
#[test]
fn test_segmentation_windows_tile_without_gaps_or_overlap() {
let seg = segmentation(vec![0, 1000, 2500, 4000], 1000);
assert_eq!(seg.segment_count(), 3);
assert_eq!(seg.window(0), Some((0, 1000)));
assert_eq!(seg.window(1), Some((1000, 2500)));
assert_eq!(seg.window(2), Some((2500, 4000)));
assert_eq!(seg.window(3), None);
}
#[test]
fn test_segmentation_durations_in_seconds() {
let seg = segmentation(vec![0, 1000, 2500, 4000], 1000);
let durations = seg.durations_secs();
assert_eq!(durations.len(), 3);
assert!((durations[0] - 1.0).abs() < 1e-9);
assert!((durations[1] - 1.5).abs() < 1e-9);
assert!((durations[2] - 1.5).abs() < 1e-9);
assert!((durations.iter().sum::<f64>() - 4.0).abs() < 1e-9);
}
#[test]
fn test_segmentation_last_mux_window_is_unbounded() {
let seg = segmentation(vec![0, 1000, 2500, 4000], 1000);
assert_eq!(seg.mux_window(0), Some((0, Some(1000))));
assert_eq!(seg.mux_window(1), Some((1000, Some(2500))));
assert_eq!(seg.mux_window(2), Some((2500, None)));
assert_eq!(seg.mux_window(3), None);
assert_eq!(seg.mux_window(u32::MAX), None);
assert_eq!(seg.window(u32::MAX), None);
}
#[test]
fn test_segmentation_origin_is_the_first_edge() {
assert_eq!(segmentation(vec![0, 1000], 1000).origin(), 0);
assert_eq!(segmentation(vec![-4096, 40960], 10240).origin(), -4096);
}
#[test]
fn test_segmentation_rejects_missing_keyframe_index() {
let error = segmentation_from_keyframes(&[], ffmpeg::Rational::new(1, 10_240), 245_760)
.expect_err("an empty keyframe index must be rejected");
assert!(
matches!(error, TranscodeError::NoKeyframeIndex),
"expected NoKeyframeIndex, got {error:?}"
);
let message = error.to_string();
assert!(message.contains("keyframe index"), "{message}");
}
#[test]
fn test_segmentation_rejects_zero_duration() {
let error = segmentation_from_keyframes(&[0], ffmpeg::Rational::new(1, 10_240), 0)
.expect_err("zero-length media must be rejected");
assert!(
matches!(error, TranscodeError::RemuxFailed(_)),
"expected RemuxFailed, got {error:?}"
);
}
#[test]
fn test_segmentation_from_keyframes_anchors_duration_at_the_first_keyframe() {
let time_base = ffmpeg::Rational::new(1, 10_240);
let keyframes: Vec<i64> = (0..12).map(|i| -4096 + i * 20_480).collect();
let seg =
segmentation_from_keyframes(&keyframes, time_base, 245_760).expect("segmentation");
assert_eq!(seg.origin(), -4096);
let total: f64 = seg.durations_secs().iter().sum();
assert!(
(total - 24.0).abs() < 1e-6,
"segments must sum to the 24s media duration, got {total}"
);
for (index, edge) in seg.edges.iter().enumerate().skip(1) {
if index + 1 < seg.edges.len() {
assert!(
keyframes.contains(edge),
"edge {edge} is not a keyframe: {keyframes:?}"
);
}
}
}
#[test]
fn test_playlist_is_well_formed() {
let playlist = build_remux_playlist("demo.mp4", &[4.0, 4.0, 2.5]);
let lines: Vec<&str> = playlist.lines().collect();
assert_eq!(lines[0], "#EXTM3U");
assert!(playlist.contains("#EXT-X-VERSION:7"));
assert!(playlist.contains("#EXT-X-PLAYLIST-TYPE:VOD"));
assert!(playlist.contains("#EXT-X-MAP:URI=\"demo.mp4-remux-init.mp4\""));
assert!(playlist.contains("#EXT-X-ENDLIST"));
assert!(playlist.ends_with('\n'));
}
#[test]
fn test_playlist_version_supports_ext_x_map() {
let playlist = build_remux_playlist("demo.mp4", &[4.0]);
let version: u32 = playlist
.lines()
.find_map(|line| line.strip_prefix("#EXT-X-VERSION:"))
.expect("version tag")
.parse()
.expect("numeric version");
assert!(version >= 7, "version {version} is too low for EXT-X-MAP");
}
#[test]
fn test_playlist_target_duration_covers_every_extinf() {
let playlist = build_remux_playlist("demo.mp4", &[3.2, 9.7, 4.0]);
let target: f64 = playlist
.lines()
.find_map(|line| line.strip_prefix("#EXT-X-TARGETDURATION:"))
.expect("target duration")
.parse()
.expect("numeric target");
let extinfs: Vec<f64> = playlist
.lines()
.filter_map(|line| line.strip_prefix("#EXTINF:"))
.map(|value| value.trim_end_matches(',').parse().expect("numeric EXTINF"))
.collect();
assert_eq!(extinfs.len(), 3);
assert_eq!(target, 10.0, "must be the ceiling of the longest segment");
for extinf in extinfs {
assert!(
extinf <= target,
"EXTINF {extinf} exceeds TARGETDURATION {target}"
);
}
}
#[test]
fn test_playlist_segment_uris_are_monotonic_and_complete() {
let playlist = build_remux_playlist("demo.mp4", &[1.0; 12]);
let uris: Vec<&str> = playlist
.lines()
.filter(|line| line.ends_with(".m4s"))
.collect();
assert_eq!(uris.len(), 12);
for (index, uri) in uris.iter().enumerate() {
assert_eq!(*uri, format!("demo.mp4-remux-{index:03}.m4s"));
}
let mut sorted = uris.clone();
sorted.sort_unstable();
assert_eq!(sorted, uris);
}
#[test]
fn test_playlist_pairs_each_extinf_with_a_uri() {
let playlist = build_remux_playlist("demo.mp4", &[2.0, 3.0, 4.0]);
let lines: Vec<&str> = playlist.lines().collect();
let extinf_positions: Vec<usize> = lines
.iter()
.enumerate()
.filter(|(_, line)| line.starts_with("#EXTINF:"))
.map(|(index, _)| index)
.collect();
assert_eq!(extinf_positions.len(), 3);
for position in extinf_positions {
let uri = lines[position + 1];
assert!(
uri.ends_with(".m4s") && !uri.starts_with('#'),
"line after #EXTINF must be a segment URI, got {uri:?}"
);
}
}
#[test]
fn test_playlist_uris_are_percent_encoded() {
let playlist = build_remux_playlist("Eric Jones - Metal 3.mp4", &[4.0]);
assert!(playlist.contains("Eric%20Jones%20-%20Metal%203.mp4-remux-000.m4s"));
assert!(playlist.contains("URI=\"Eric%20Jones%20-%20Metal%203.mp4-remux-init.mp4\""));
assert!(
!playlist.contains(' '),
"no line may contain a raw space: {playlist}"
);
}
#[test]
fn test_playlist_target_duration_never_zero() {
let playlist = build_remux_playlist("demo.mp4", &[0.2]);
assert!(playlist.contains("#EXT-X-TARGETDURATION:1"));
}
fn mp4_box(kind: &[u8; 4], payload: &[u8]) -> Vec<u8> {
let size = (BOX_HEADER_LEN + payload.len()) as u32;
let mut out = size.to_be_bytes().to_vec();
out.extend_from_slice(kind);
out.extend_from_slice(payload);
out
}
fn tfhd(track_id: u32) -> Vec<u8> {
let mut payload = 0u32.to_be_bytes().to_vec(); payload.extend_from_slice(&track_id.to_be_bytes());
mp4_box(b"tfhd", &payload)
}
fn tfdt_v1(base: u64) -> Vec<u8> {
let mut payload = vec![1u8, 0, 0, 0]; payload.extend_from_slice(&base.to_be_bytes());
mp4_box(b"tfdt", &payload)
}
fn tfdt_v0(base: u32) -> Vec<u8> {
let mut payload = vec![0u8, 0, 0, 0]; payload.extend_from_slice(&base.to_be_bytes());
mp4_box(b"tfdt", &payload)
}
fn traf(children: &[Vec<u8>]) -> Vec<u8> {
mp4_box(b"traf", &children.concat())
}
fn moof(trafs: &[Vec<u8>]) -> Vec<u8> {
let mut payload = mp4_box(b"mfhd", &[0, 0, 0, 0, 0, 0, 0, 1]);
payload.extend_from_slice(&trafs.concat());
mp4_box(b"moof", &payload)
}
#[test]
fn test_scan_boxes_reads_top_level_order() {
let data = [
mp4_box(b"ftyp", b"isom"),
mp4_box(b"moov", b"xx"),
mp4_box(b"mdat", b"payload"),
]
.concat();
let boxes = scan_boxes(&data, 0..data.len()).expect("scan");
let kinds: Vec<String> = boxes
.iter()
.map(|span| String::from_utf8_lossy(&span.kind).into_owned())
.collect();
assert_eq!(kinds, vec!["ftyp", "moov", "mdat"]);
}
#[test]
fn test_scan_boxes_rejects_bogus_size() {
let mut data = 4u32.to_be_bytes().to_vec();
data.extend_from_slice(b"junk");
assert!(scan_boxes(&data, 0..data.len()).is_err());
let mut data = 999u32.to_be_bytes().to_vec();
data.extend_from_slice(b"moov");
assert!(scan_boxes(&data, 0..data.len()).is_err());
}
#[test]
fn test_split_separates_init_from_media_and_drops_mfra() {
let data = [
mp4_box(b"ftyp", b"isom"),
mp4_box(b"moov", b"MOOV"),
moof(&[traf(&[tfhd(1), tfdt_v1(0)])]),
mp4_box(b"mdat", b"frames"),
mp4_box(b"mfra", b"index"),
]
.concat();
let (init, media) = split_fragmented_output(&data).expect("split");
let init_kinds: Vec<[u8; 4]> = scan_boxes(&init, 0..init.len())
.unwrap()
.iter()
.map(|span| span.kind)
.collect();
assert_eq!(init_kinds, vec![*b"ftyp", *b"moov"]);
let media_kinds: Vec<[u8; 4]> = scan_boxes(&media, 0..media.len())
.unwrap()
.iter()
.map(|span| span.kind)
.collect();
assert_eq!(
media_kinds,
vec![*b"moof", *b"mdat"],
"media segment must start at 'moof' and carry no 'mfra'"
);
}
#[test]
fn test_split_requires_a_moov() {
let data = [
moof(&[traf(&[tfhd(1), tfdt_v1(0)])]),
mp4_box(b"mdat", b"x"),
]
.concat();
assert!(split_fragmented_output(&data).is_err());
}
#[test]
fn test_patch_tfdt_makes_decode_times_absolute() {
let mut media = [
moof(&[traf(&[tfhd(1), tfdt_v1(0)]), traf(&[tfhd(2), tfdt_v1(0)])]),
mp4_box(b"mdat", b"frames"),
moof(&[
traf(&[tfhd(1), tfdt_v1(3_000)]),
traf(&[tfhd(2), tfdt_v1(2_000)]),
]),
mp4_box(b"mdat", b"frames"),
]
.concat();
let patched = patch_tfdt_bases(&mut media, &[Some(90_000), Some(48_000)]).expect("patch");
assert_eq!(patched, 4);
let bases = collect_tfdt_bases(&media);
assert_eq!(
bases,
vec![(1, 90_000), (2, 48_000), (1, 93_000), (2, 50_000)]
);
}
#[test]
fn test_patch_tfdt_bases_are_monotonic_per_track() {
let mut media = [
moof(&[traf(&[tfhd(1), tfdt_v1(0)])]),
mp4_box(b"mdat", b"a"),
moof(&[traf(&[tfhd(1), tfdt_v1(1_024)])]),
mp4_box(b"mdat", b"b"),
moof(&[traf(&[tfhd(1), tfdt_v1(2_048)])]),
mp4_box(b"mdat", b"c"),
]
.concat();
patch_tfdt_bases(&mut media, &[Some(500_000)]).expect("patch");
let bases: Vec<u64> = collect_tfdt_bases(&media)
.into_iter()
.map(|(_, base)| base)
.collect();
assert!(
bases.windows(2).all(|pair| pair[0] < pair[1]),
"decode times must increase across fragments: {bases:?}"
);
assert_eq!(bases[0], 500_000);
}
#[test]
fn test_patch_tfdt_clamps_negative_start() {
let mut media = [
moof(&[traf(&[tfhd(1), tfdt_v1(0)])]),
mp4_box(b"mdat", b"a"),
]
.concat();
patch_tfdt_bases(&mut media, &[Some(-2_048)]).expect("patch");
assert_eq!(collect_tfdt_bases(&media), vec![(1, 0)]);
}
#[test]
fn test_patch_tfdt_handles_version_zero() {
let mut media = [
moof(&[traf(&[tfhd(1), tfdt_v0(10)])]),
mp4_box(b"mdat", b"a"),
]
.concat();
patch_tfdt_bases(&mut media, &[Some(1_000)]).expect("patch");
let spans = scan_boxes(&media, 0..media.len()).unwrap();
let moof_span = spans.iter().find(|s| s.is(b"moof")).unwrap();
let traf_span = *scan_boxes(&media, moof_span.payload..moof_span.end)
.unwrap()
.iter()
.find(|s| s.is(b"traf"))
.unwrap();
let tfdt_span = *scan_boxes(&media, traf_span.payload..traf_span.end)
.unwrap()
.iter()
.find(|s| s.is(b"tfdt"))
.unwrap();
assert_eq!(media[tfdt_span.payload], 0, "version must be preserved");
assert_eq!(read_u32(&media, tfdt_span.payload + 4), Some(1_010));
}
#[test]
fn test_patch_tfdt_rejects_version_zero_overflow() {
let mut media = [
moof(&[traf(&[tfhd(1), tfdt_v0(u32::MAX)])]),
mp4_box(b"mdat", b"a"),
]
.concat();
assert!(patch_tfdt_bases(&mut media, &[Some(1_000)]).is_err());
}
#[test]
fn test_patch_tfdt_leaves_unknown_tracks_alone() {
let mut media = [
moof(&[traf(&[tfhd(9), tfdt_v1(7)])]),
mp4_box(b"mdat", b"a"),
]
.concat();
patch_tfdt_bases(&mut media, &[Some(500)]).expect("patch");
assert_eq!(collect_tfdt_bases(&media), vec![(9, 7)]);
}
#[test]
fn test_patch_tfdt_requires_tfhd() {
let mut media = [moof(&[traf(&[tfdt_v1(0)])]), mp4_box(b"mdat", b"a")].concat();
assert!(patch_tfdt_bases(&mut media, &[Some(0)]).is_err());
}
fn collect_tfdt_bases(media: &[u8]) -> Vec<(u32, u64)> {
let mut out = Vec::new();
for moof_span in scan_boxes(media, 0..media.len())
.unwrap()
.into_iter()
.filter(|span| span.is(b"moof"))
{
for traf_span in scan_boxes(media, moof_span.payload..moof_span.end)
.unwrap()
.into_iter()
.filter(|span| span.is(b"traf"))
{
let children = scan_boxes(media, traf_span.payload..traf_span.end).unwrap();
let track_id = children
.iter()
.find(|span| span.is(b"tfhd"))
.and_then(|span| read_u32(media, span.payload + 4))
.unwrap();
for tfdt in children.iter().filter(|span| span.is(b"tfdt")) {
let base = match media[tfdt.payload] {
0 => u64::from(read_u32(media, tfdt.payload + 4).unwrap()),
_ => read_u64(media, tfdt.payload + 4).unwrap(),
};
out.push((track_id, base));
}
}
}
out
}
}