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);
}
}
}
}
Ok(result)
}
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 samples = build_sample_info(&sample_tables, timescale, reader)?;
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 as u64;
}
}
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());
}
crate::registry::StructuredData::MediaHeader(_) => {}
crate::registry::StructuredData::HandlerReference(_) => {}
crate::registry::StructuredData::TrackHeader(_) => {}
crate::registry::StructuredData::TrackFragmentRun(_) => {}
}
}
}
}
Ok(tables)
}
fn build_sample_info<R: Read + Seek>(
tables: &SampleTables,
timescale: u32,
_reader: &mut R,
) -> anyhow::Result<Vec<SampleInfo>> {
let mut samples = Vec::new();
let sample_count = if let Some(stsz) = &tables.stsz {
stsz.sample_count
} else {
return Ok(samples);
};
let mut current_dts = 0u64;
let default_duration = if timescale > 0 { timescale / 24 } else { 1000 };
for i in 0..sample_count {
let duration = if let Some(stts) = &tables.stts {
get_sample_duration_from_stts(stts, i).unwrap_or(default_duration)
} else {
default_duration
};
let composition_offset = if let Some(ctts) = &tables.ctts {
get_composition_offset_from_ctts(ctts, i).unwrap_or(0)
} else {
0
};
let pts = current_dts.saturating_add_signed(composition_offset as i64);
let sample = SampleInfo {
index: i,
dts: current_dts,
pts,
start_time: pts as f64 / timescale as f64,
duration,
rendered_offset: composition_offset as i64,
file_offset: get_sample_file_offset(tables, i),
size: get_sample_size(&tables.stsz, i),
is_sync: is_sync_sample(&tables.stss, i + 1), };
current_dts += duration as u64;
samples.push(sample);
}
Ok(samples)
}
fn get_sample_size(stsz: &Option<crate::registry::StszData>, index: u32) -> u32 {
if let Some(stsz) = stsz {
if stsz.sample_size > 0 {
stsz.sample_size
} else if let Some(size) = stsz.sample_sizes.get(index as usize) {
*size
} else {
0
}
} else {
0
}
}
fn is_sync_sample(stss: &Option<crate::registry::StssData>, sample_number: u32) -> bool {
if let Some(stss) = stss {
stss.sample_numbers.contains(&sample_number)
} else {
true
}
}
fn get_sample_duration_from_stts(
stts: &crate::registry::SttsData,
sample_index: u32,
) -> Option<u32> {
let mut current_sample = 0;
for entry in &stts.entries {
if sample_index < current_sample + entry.sample_count {
return Some(entry.sample_delta);
}
current_sample += entry.sample_count;
}
stts.entries.last().map(|entry| entry.sample_delta)
}
fn get_composition_offset_from_ctts(
ctts: &crate::registry::CttsData,
sample_index: u32,
) -> Option<i32> {
let mut current_sample = 0;
for entry in &ctts.entries {
if sample_index < current_sample + entry.sample_count {
return Some(entry.sample_offset);
}
current_sample += entry.sample_count;
}
Some(0)
}
fn get_sample_file_offset(tables: &SampleTables, sample_index: u32) -> u64 {
let stsc = match &tables.stsc {
Some(data) => data,
None => return 0, };
let stsz = match &tables.stsz {
Some(data) => data,
None => return 0, };
let (chunk_offsets_64, chunk_offsets_32) = if let Some(co64) = &tables.co64 {
(Some(&co64.chunk_offsets), None)
} else if let Some(stco) = &tables.stco {
(None, Some(&stco.chunk_offsets))
} else {
return 0; };
let get_chunk_offset = |index: usize| -> u64 {
if let Some(offsets_64) = chunk_offsets_64 {
offsets_64.get(index).copied().unwrap_or(0)
} else if let Some(offsets_32) = chunk_offsets_32 {
offsets_32.get(index).copied().unwrap_or(0) as u64
} else {
0
}
};
let chunk_count = if let Some(offsets_64) = chunk_offsets_64 {
offsets_64.len()
} else if let Some(offsets_32) = chunk_offsets_32 {
offsets_32.len()
} else {
0
};
let target_sample = sample_index + 1;
let mut current_sample = 1u32;
let mut chunk_index = 0usize;
let mut samples_per_chunk = 0u32;
let mut sample_offset_in_range = 0u32;
let mut chunk_offset_in_range = 0u32;
for (i, entry) in stsc.entries.iter().enumerate() {
let next_first_chunk = if i + 1 < stsc.entries.len() {
stsc.entries[i + 1].first_chunk
} else {
chunk_count as u32 + 1 };
samples_per_chunk = entry.samples_per_chunk;
let chunks_with_this_config = next_first_chunk - entry.first_chunk;
let samples_in_this_range =
(chunks_with_this_config as u64).saturating_mul(samples_per_chunk as u64);
if (current_sample as u64) + samples_in_this_range > target_sample as u64 {
sample_offset_in_range = target_sample - current_sample;
chunk_offset_in_range = sample_offset_in_range / samples_per_chunk;
chunk_index = (entry.first_chunk - 1) as usize + chunk_offset_in_range as usize;
break;
}
current_sample =
(current_sample as u64 + samples_in_this_range).min(u32::MAX as u64) as u32;
}
if chunk_index >= chunk_count {
return 0; }
let chunk_offset = get_chunk_offset(chunk_index);
let sample_in_chunk = (sample_offset_in_range % samples_per_chunk) as usize;
let mut offset_in_chunk = 0u64;
let chunk_start_sample =
(current_sample - 1 + chunk_offset_in_range * samples_per_chunk) as usize;
if stsz.sample_size > 0 {
offset_in_chunk = sample_in_chunk as u64 * stsz.sample_size as u64;
} else if !stsz.sample_sizes.is_empty() {
for i in 0..sample_in_chunk {
let sample_idx = chunk_start_sample + i;
if sample_idx < stsz.sample_sizes.len() {
offset_in_chunk += stsz.sample_sizes[sample_idx] as u64;
}
}
}
chunk_offset + offset_in_chunk
}
#[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")
);
}
}