use anyhow::Context;
use serde::Serialize;
use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::path::Path;
#[derive(Debug, Clone, Serialize)]
pub struct SampleInfo {
pub index: u32,
pub dts: u64,
pub pts: u64,
pub start_time: f64,
pub duration: u32,
pub rendered_offset: i64,
pub file_offset: u64,
pub size: u32,
pub is_sync: bool,
}
#[derive(Debug, Clone, Serialize)]
pub struct TrackSamples {
pub track_id: u32,
pub handler_type: String, pub timescale: u32,
pub duration: u64, pub sample_count: u32,
pub samples: Vec<SampleInfo>,
}
pub fn track_samples_from_reader<R: Read + Seek>(
mut reader: R,
) -> anyhow::Result<Vec<TrackSamples>> {
let file_size = reader.seek(SeekFrom::End(0))?;
reader.seek(SeekFrom::Start(0))?;
let boxes = crate::get_boxes(&mut reader, file_size, true)
.context("getting boxes from reader")?;
let mut result = Vec::new();
for moov_box in boxes.iter().filter(|b| b.typ == "moov") {
if let Some(children) = &moov_box.children {
for trak_box in children.iter().filter(|b| b.typ == "trak") {
if let Some(track_samples) =
crate::samples::extract_track_samples(trak_box, &mut reader)?
{
result.push(track_samples);
}
}
}
}
extract_fragment_samples(&boxes, &mut result);
Ok(result)
}
fn extract_fragment_samples(boxes: &[crate::Box], tracks: &mut [TrackSamples]) {
use crate::registry::{StructuredData, TrexData};
use std::collections::HashMap;
let mut trex_map: HashMap<u32, TrexData> = HashMap::new();
for moov in boxes.iter().filter(|b| b.typ == "moov") {
let Some(kids) = &moov.children else { continue };
for mvex in kids.iter().filter(|b| b.typ == "mvex") {
let Some(mkids) = &mvex.children else {
continue;
};
for trex in mkids.iter().filter(|b| b.typ == "trex") {
if let Some(StructuredData::TrackExtends(d)) = &trex.structured_data {
trex_map.insert(d.track_id, d.clone());
}
}
}
}
let mut next_dts: HashMap<u32, u64> = tracks
.iter()
.map(|t| {
let end = t
.samples
.last()
.map(|s| s.dts + s.duration as u64)
.unwrap_or(0);
(t.track_id, end)
})
.collect();
let original_counts: Vec<usize> = tracks.iter().map(|t| t.samples.len()).collect();
for moof in boxes.iter().filter(|b| b.typ == "moof") {
let moof_start = moof.offset;
let Some(kids) = &moof.children else { continue };
for traf in kids.iter().filter(|b| b.typ == "traf") {
let Some(tkids) = &traf.children else {
continue;
};
let Some(tfhd) = tkids.iter().find_map(|b| match &b.structured_data {
Some(StructuredData::TrackFragmentHeader(d)) => Some(d),
_ => None,
}) else {
continue;
};
let tfdt = tkids.iter().find_map(|b| match &b.structured_data {
Some(StructuredData::TrackFragmentDecodeTime(d)) => Some(d),
_ => None,
});
let Some(track) = tracks.iter_mut().find(|t| t.track_id == tfhd.track_id) else {
continue;
};
let trex = trex_map.get(&tfhd.track_id);
let mut dts = tfdt
.map(|d| d.base_media_decode_time)
.unwrap_or_else(|| next_dts.get(&tfhd.track_id).copied().unwrap_or(0));
let base = tfhd.base_data_offset.unwrap_or(moof_start);
let mut cursor: Option<u64> = None;
for trun_box in tkids.iter().filter(|b| b.typ == "trun") {
let Some(StructuredData::TrackFragmentRun(trun)) = &trun_box.structured_data else {
continue;
};
let mut offset = match trun.data_offset {
Some(d) => base.saturating_add_signed(d as i64),
None => cursor.unwrap_or(base),
};
for (j, s) in trun.samples.iter().enumerate() {
let duration = s
.duration
.or(tfhd.default_sample_duration)
.or(trex.map(|t| t.default_sample_duration))
.unwrap_or(0);
let size = s
.size
.or(tfhd.default_sample_size)
.or(trex.map(|t| t.default_sample_size))
.unwrap_or(0);
let flags = s
.flags
.or(if j == 0 {
trun.first_sample_flags
} else {
None
})
.or(tfhd.default_sample_flags)
.or(trex.map(|t| t.default_sample_flags))
.unwrap_or(0);
let cto = s.composition_time_offset.unwrap_or(0);
let pts = dts.saturating_add_signed(cto as i64);
track.samples.push(SampleInfo {
index: track.samples.len() as u32,
dts,
pts,
start_time: if track.timescale > 0 {
pts as f64 / track.timescale as f64
} else {
0.0
},
duration,
rendered_offset: cto as i64,
file_offset: offset,
size,
is_sync: flags & 0x0001_0000 == 0,
});
offset += size as u64;
dts += duration as u64;
}
cursor = Some(offset);
}
next_dts.insert(tfhd.track_id, dts);
}
}
for (track, original) in tracks.iter_mut().zip(original_counts) {
if track.samples.len() == original {
continue;
}
track.sample_count = track.samples.len() as u32;
let end = track
.samples
.last()
.map(|s| s.dts + s.duration as u64)
.unwrap_or(0);
if track.duration < end {
track.duration = end;
}
}
}
pub fn track_samples_from_path(path: impl AsRef<Path>) -> anyhow::Result<Vec<TrackSamples>> {
let file = File::open(path)?;
track_samples_from_reader(file)
}
pub fn extract_track_samples<R: Read + Seek>(
trak_box: &crate::Box,
reader: &mut R,
) -> anyhow::Result<Option<TrackSamples>> {
let track_id = find_track_id(trak_box)?;
let (handler_type, timescale, duration) = find_media_info(trak_box)?;
let stbl_box = find_stbl_box(trak_box)?;
let sample_tables = extract_sample_tables(stbl_box)?;
let _ = reader;
let samples = build_sample_info(&sample_tables, timescale)?;
let sample_count = samples.len() as u32;
Ok(Some(TrackSamples {
track_id,
handler_type,
timescale,
duration,
sample_count,
samples,
}))
}
fn find_track_id(trak_box: &crate::Box) -> anyhow::Result<u32> {
use crate::registry::StructuredData;
if let Some(children) = &trak_box.children {
for child in children {
if child.typ == "tkhd" {
if let Some(StructuredData::TrackHeader(tkhd_data)) = &child.structured_data {
return Ok(tkhd_data.track_id);
}
}
}
}
anyhow::bail!("No tkhd box found or track ID could not be parsed")
}
fn find_media_info(trak_box: &crate::Box) -> anyhow::Result<(String, u32, u64)> {
use crate::registry::StructuredData;
if let Some(children) = &trak_box.children {
for child in children {
if child.typ == "mdia"
&& let Some(mdia_children) = &child.children
{
let mut timescale = 1000; let mut duration = 0; let mut handler_type = String::from("vide");
for mdia_child in mdia_children {
if mdia_child.typ == "mdhd" {
if let Some(StructuredData::MediaHeader(mdhd_data)) =
&mdia_child.structured_data
{
timescale = mdhd_data.timescale;
duration = mdhd_data.duration;
}
}
if mdia_child.typ == "hdlr" {
if let Some(StructuredData::HandlerReference(hdlr_data)) =
&mdia_child.structured_data
{
handler_type = hdlr_data.handler_type.clone();
}
}
}
return Ok((handler_type, timescale, duration));
}
}
}
Ok((String::from("vide"), 1000, 0))
}
fn find_stbl_box(trak_box: &crate::Box) -> anyhow::Result<&crate::Box> {
if let Some(children) = &trak_box.children {
for child in children {
if child.typ == "mdia"
&& let Some(mdia_children) = &child.children
{
for mdia_child in mdia_children {
if mdia_child.typ == "minf"
&& let Some(minf_children) = &mdia_child.children
{
for minf_child in minf_children {
if minf_child.typ == "stbl" {
return Ok(minf_child);
}
}
}
}
}
}
}
anyhow::bail!("stbl box not found")
}
#[derive(Debug)]
struct SampleTables {
stsd: Option<crate::registry::StsdData>,
stts: Option<crate::registry::SttsData>,
ctts: Option<crate::registry::CttsData>,
stsc: Option<crate::registry::StscData>,
stsz: Option<crate::registry::StszData>,
stss: Option<crate::registry::StssData>,
stco: Option<crate::registry::StcoData>,
co64: Option<crate::registry::Co64Data>,
}
fn extract_sample_tables(stbl_box: &crate::Box) -> anyhow::Result<SampleTables> {
let mut tables = SampleTables {
stsd: None,
stts: None,
ctts: None,
stsc: None,
stsz: None,
stss: None,
stco: None,
co64: None,
};
if let Some(children) = &stbl_box.children {
for child in children {
if let Some(structured_data) = &child.structured_data {
match structured_data {
crate::registry::StructuredData::SampleDescription(data) => {
tables.stsd = Some(data.clone());
}
crate::registry::StructuredData::DecodingTimeToSample(data) => {
tables.stts = Some(data.clone());
}
crate::registry::StructuredData::CompositionTimeToSample(data) => {
tables.ctts = Some(data.clone());
}
crate::registry::StructuredData::SampleToChunk(data) => {
tables.stsc = Some(data.clone());
}
crate::registry::StructuredData::SampleSize(data) => {
tables.stsz = Some(data.clone());
}
crate::registry::StructuredData::SyncSample(data) => {
tables.stss = Some(data.clone());
}
crate::registry::StructuredData::ChunkOffset(data) => {
tables.stco = Some(data.clone());
}
crate::registry::StructuredData::ChunkOffset64(data) => {
tables.co64 = Some(data.clone());
}
_ => {}
}
}
}
}
Ok(tables)
}
struct RunCursor<'a, T> {
entries: &'a [T],
idx: usize,
used: u32,
}
impl<'a, T> RunCursor<'a, T> {
fn new(entries: &'a [T]) -> Self {
Self {
entries,
idx: 0,
used: 0,
}
}
fn next(&mut self, count_of: impl Fn(&T) -> u32) -> Option<&'a T> {
while let Some(e) = self.entries.get(self.idx) {
if self.used < count_of(e) {
self.used += 1;
return Some(e);
}
self.idx += 1;
self.used = 0;
}
None
}
}
fn build_sample_info(tables: &SampleTables, timescale: u32) -> anyhow::Result<Vec<SampleInfo>> {
let Some(stsz) = &tables.stsz else {
return Ok(Vec::new());
};
let sample_count = stsz.sample_count;
let size_of = |i: u32| -> u32 {
if stsz.sample_size > 0 {
stsz.sample_size
} else {
stsz.sample_sizes.get(i as usize).copied().unwrap_or(0)
}
};
let chunk_offset_at = |index: usize| -> Option<u64> {
if let Some(co64) = &tables.co64 {
co64.chunk_offsets.get(index).copied()
} else if let Some(stco) = &tables.stco {
stco.chunk_offsets.get(index).copied().map(u64::from)
} else {
None
}
};
let chunk_count = if let Some(co64) = &tables.co64 {
co64.chunk_offsets.len()
} else if let Some(stco) = &tables.stco {
stco.chunk_offsets.len()
} else {
0
};
let mut file_offsets = vec![0u64; sample_count as usize];
if let Some(stsc) = &tables.stsc {
let mut sample_idx = 0u32;
'chunks: for (i, entry) in stsc.entries.iter().enumerate() {
let first = (entry.first_chunk.max(1) - 1) as usize;
let next_first = stsc
.entries
.get(i + 1)
.map(|e| (e.first_chunk.max(1) - 1) as usize)
.unwrap_or(chunk_count);
for chunk in first..next_first.min(chunk_count) {
let Some(base) = chunk_offset_at(chunk) else {
break 'chunks;
};
let mut offset = base;
for _ in 0..entry.samples_per_chunk {
if sample_idx >= sample_count {
break 'chunks;
}
file_offsets[sample_idx as usize] = offset;
offset += size_of(sample_idx) as u64;
sample_idx += 1;
}
}
}
}
let empty_stts = Vec::new();
let mut stts_cursor = RunCursor::new(
tables
.stts
.as_ref()
.map(|t| t.entries.as_slice())
.unwrap_or(&empty_stts),
);
let empty_ctts = Vec::new();
let mut ctts_cursor = RunCursor::new(
tables
.ctts
.as_ref()
.map(|t| t.entries.as_slice())
.unwrap_or(&empty_ctts),
);
let stss_set: Option<std::collections::HashSet<u32>> = tables
.stss
.as_ref()
.map(|s| s.sample_numbers.iter().copied().collect());
let last_stts_delta = tables
.stts
.as_ref()
.and_then(|t| t.entries.last())
.map(|e| e.sample_delta)
.unwrap_or(0);
let mut samples = Vec::with_capacity(sample_count as usize);
let mut current_dts = 0u64;
for i in 0..sample_count {
let duration = stts_cursor
.next(|e| e.sample_count)
.map(|e| e.sample_delta)
.unwrap_or(last_stts_delta);
let composition_offset = ctts_cursor
.next(|e| e.sample_count)
.map(|e| e.sample_offset)
.unwrap_or(0);
let pts = current_dts.saturating_add_signed(composition_offset as i64);
samples.push(SampleInfo {
index: i,
dts: current_dts,
pts,
start_time: if timescale > 0 {
pts as f64 / timescale as f64
} else {
0.0
},
duration,
rendered_offset: composition_offset as i64,
file_offset: file_offsets[i as usize],
size: size_of(i),
is_sync: stss_set.as_ref().is_none_or(|set| set.contains(&(i + 1))),
});
current_dts += duration as u64;
}
Ok(samples)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::registry::{StructuredData, TkhdData};
#[test]
fn test_find_track_id_from_structured_data() {
let tkhd_data = TkhdData {
version: 0,
flags: 0,
track_id: 42,
duration: 48000,
width: 1920.0,
height: 1080.0,
};
let tkhd_box = crate::Box {
offset: 0,
size: 0,
header_size: 0,
payload_offset: None,
payload_size: None,
typ: "tkhd".to_string(),
uuid: None,
version: Some(0),
flags: Some(0),
kind: "full".to_string(),
full_name: "Track Header Box".to_string(),
decoded: None,
structured_data: Some(StructuredData::TrackHeader(tkhd_data)),
children: None,
};
let trak_box = crate::Box {
offset: 0,
size: 0,
header_size: 0,
payload_offset: None,
payload_size: None,
typ: "trak".to_string(),
uuid: None,
version: None,
flags: None,
kind: "container".to_string(),
full_name: "Track Box".to_string(),
decoded: None,
structured_data: None,
children: Some(vec![tkhd_box]),
};
let track_id = find_track_id(&trak_box).unwrap();
assert_eq!(track_id, 42);
}
#[test]
fn test_find_track_id_multiple_tracks() {
for expected_id in [1, 3, 7, 255] {
let tkhd_data = TkhdData {
version: 0,
flags: 0,
track_id: expected_id,
duration: 24000,
width: 0.0,
height: 0.0,
};
let tkhd_box = crate::Box {
offset: 0,
size: 0,
header_size: 0,
payload_offset: None,
payload_size: None,
typ: "tkhd".to_string(),
uuid: None,
version: Some(0),
flags: Some(0),
kind: "full".to_string(),
full_name: "Track Header Box".to_string(),
decoded: None,
structured_data: Some(StructuredData::TrackHeader(tkhd_data)),
children: None,
};
let trak_box = crate::Box {
offset: 0,
size: 0,
header_size: 0,
payload_offset: None,
payload_size: None,
typ: "trak".to_string(),
uuid: None,
version: None,
flags: None,
kind: "container".to_string(),
full_name: "Track Box".to_string(),
decoded: None,
structured_data: None,
children: Some(vec![tkhd_box]),
};
let track_id = find_track_id(&trak_box).unwrap();
assert_eq!(track_id, expected_id);
}
}
#[test]
fn test_find_track_id_no_tkhd_box() {
let trak_box = crate::Box {
offset: 0,
size: 0,
header_size: 0,
payload_offset: None,
payload_size: None,
typ: "trak".to_string(),
uuid: None,
version: None,
flags: None,
kind: "container".to_string(),
full_name: "Track Box".to_string(),
decoded: None,
structured_data: None,
children: Some(vec![]),
};
let result = find_track_id(&trak_box);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("No tkhd box found")
);
}
}