use std::path::{Path, PathBuf};
use std::time::Duration;
use crate::color::{ColorSpace, Matrix, Range};
use crate::error::{Error, Result};
use crate::identify::{identify_path, Container, Encoding};
use crate::media::{MediaInfo, Packet, Rational, Rotation, TrackInfo, TrackKind};
mod matroska;
mod mp4;
mod quicktime;
pub trait Demuxer: Send {
fn info(&self) -> &MediaInfo;
fn next_packet(&mut self) -> Result<Option<Packet>>;
fn seek(&mut self, position: Duration) -> Result<()>;
}
pub fn open(path: impl AsRef<Path>) -> Result<Box<dyn Demuxer>> {
let path = path.as_ref();
let container = identify_path(path)?;
match container {
Container::Mp4 | Container::Avif | Container::Heif => {
Ok(Box::new(mp4::Mp4Demuxer::open(path, container)?))
}
Container::Matroska | Container::WebM => {
Ok(Box::new(matroska::MatroskaDemuxer::open(path, container)?))
}
_ if container.is_image() => Err(Error::unsupported(format!(
"{} is a {container}, which is a still image — use the `image` feature \
and `vtome::load_image` rather than a demuxer",
path.display()
))),
_ => Err(Error::unsupported(format!(
"{} is a {container}, which vtome identifies but does not open. \
Transcode it to WebM or MP4 first",
path.display()
))),
}
}
pub fn open_reader(mut reader: Box<dyn crate::ReadSeek>, name: &str) -> Result<Box<dyn Demuxer>> {
use std::io::{Read, Seek, SeekFrom};
let path = Path::new(name);
let mut probe = Vec::with_capacity(crate::identify::PROBE_BYTES);
reader
.by_ref()
.take(crate::identify::PROBE_BYTES as u64)
.read_to_end(&mut probe)
.map_err(|error| Error::io(path, error))?;
reader.seek(SeekFrom::Start(0)).map_err(|error| Error::io(path, error))?;
let container = crate::identify::identify_bytes(&probe).ok_or_else(|| Error::UnknownContainer {
path: path.to_path_buf(),
found: crate::identify::describe_unknown(&probe),
})?;
match container {
Container::Mp4 | Container::Avif | Container::Heif => Ok(Box::new(mp4::Mp4Demuxer::open_reader(reader, path, container)?)),
Container::Matroska | Container::WebM => Ok(Box::new(matroska::MatroskaDemuxer::open_reader(reader, path, container)?)),
_ => Err(Error::unsupported(format!(
"{name} is a {container}, which vtome does not open from a reader. Transcode it to WebM or MP4 first"
))),
}
}
fn scaled(ticks: u64, per_second: u64) -> Duration {
if per_second == 0 {
return Duration::ZERO;
}
let nanoseconds = u128::from(ticks) * 1_000_000_000 / u128::from(per_second);
Duration::from_nanos(nanoseconds.min(u128::from(u64::MAX)) as u64)
}
fn color_from(matrix: Option<Matrix>, range: Option<Range>, width: u32, height: u32) -> ColorSpace {
let mut color = ColorSpace::guess_for(width, height);
if let Some(matrix) = matrix {
color.matrix = matrix;
}
if let Some(range) = range {
color.range = range;
}
color
}
#[allow(clippy::too_many_arguments)]
fn video_track(
id: u32,
codec_id: String,
width: u32,
height: u32,
frame_rate: Option<Rational>,
bit_depth: u32,
color: ColorSpace,
rotation: Rotation,
duration: Duration,
extra_data: Vec<u8>,
) -> TrackInfo {
TrackInfo {
id,
kind: TrackKind::Video,
encoding: Encoding::from_codec_id(&codec_id),
codec_id,
width,
height,
frame_rate,
bit_depth,
color,
rotation,
duration,
extra_data,
}
}
fn demux_error(path: &PathBuf, reason: impl std::fmt::Display) -> Error {
Error::demux(path, reason.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn timescales_convert_without_overflowing() {
assert_eq!(scaled(90_000, 90_000), Duration::from_secs(1));
assert_eq!(scaled(1_500, 1_000), Duration::from_millis(1_500));
let two_hours = 7_200 * 1_000_000_000_u64;
assert_eq!(scaled(two_hours, 1_000_000_000), Duration::from_secs(7_200));
}
#[test]
fn a_zero_timescale_does_not_divide_by_zero() {
assert_eq!(scaled(1_000, 0), Duration::ZERO);
}
#[test]
fn container_metadata_wins_over_the_guess() {
let guessed = color_from(None, None, 3840, 2160);
assert_eq!(guessed.matrix, Matrix::Bt2020Ncl);
let stated = color_from(Some(Matrix::Bt709), Some(Range::Full), 3840, 2160);
assert_eq!(stated.matrix, Matrix::Bt709);
assert_eq!(stated.range, Range::Full);
}
#[test]
fn a_still_image_is_pointed_at_the_image_loader_rather_than_a_demuxer() {
let temp = tempfile::TempDir::new().unwrap();
let path = temp.path().join("picture.png");
let mut bytes = vec![0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A];
bytes.extend_from_slice(&[0; 32]);
std::fs::write(&path, bytes).unwrap();
let Err(Error::Unsupported { what }) = open(&path) else {
panic!("a PNG is not a container to demux");
};
assert!(what.contains("load_image"), "{what}");
}
#[test]
fn a_container_we_identify_but_do_not_open_says_so() {
let temp = tempfile::TempDir::new().unwrap();
let path = temp.path().join("old.avi");
let mut bytes = b"RIFF".to_vec();
bytes.extend_from_slice(&[0; 4]);
bytes.extend_from_slice(b"AVI ");
bytes.extend_from_slice(&[0; 32]);
std::fs::write(&path, bytes).unwrap();
let Err(Error::Unsupported { what }) = open(&path) else {
panic!("AVI is identified and not opened");
};
assert!(what.contains("Transcode"), "{what}");
}
#[test]
fn a_file_that_is_not_media_at_all_is_reported_as_such() {
let temp = tempfile::TempDir::new().unwrap();
let path = temp.path().join("notes.txt");
std::fs::write(&path, b"just some text").unwrap();
assert!(matches!(open(&path), Err(Error::UnknownContainer { .. })));
}
}