mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-29 04:56:58 -06:00
Media: use craft-media for bitstream, isobmff and matroska (#1034)
* Media: use craft-media for bitstream, isobmff and matroska
deckcraft-bitstream, deckcraft-isobmff and deckcraft-matroska re-export
craft_media::{bitstream, isobmff, matroska} from craft-libs. The packages
and their public paths stay, so the codec crates build unchanged.
deckcraft-bitstream keeps the decoder memory budget as its own module
(budget). deckcraft-media names the new CodecConfig::Apv entry.
* Media: pin craft-media to storytold/craft-libs at f97c84d2 (#245)
* Media: pin craft-media to storytold/craft-libs at f0ad6c1e (#246)
This commit is contained in:
Generated
+20
-136
@@ -444,30 +444,15 @@ version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "bit-set"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
|
||||
dependencies = [
|
||||
"bit-vec 0.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit-set"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2f926cc3060f09db9ebc5b52823d85268d24bb917e472c0c4bea35780a7d"
|
||||
dependencies = [
|
||||
"bit-vec 0.9.1",
|
||||
"bit-vec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit-vec"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
|
||||
|
||||
[[package]]
|
||||
name = "bit-vec"
|
||||
version = "0.9.1"
|
||||
@@ -820,6 +805,14 @@ dependencies = [
|
||||
"windows 0.54.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-media"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=f0ad6c1e1d297908915f4be315b0fcfc9be7b5d4#f0ad6c1e1d297908915f4be315b0fcfc9be7b5d4"
|
||||
dependencies = [
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-pdf"
|
||||
version = "0.1.0"
|
||||
@@ -933,7 +926,7 @@ dependencies = [
|
||||
name = "deckcraft-bitstream"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"proptest",
|
||||
"craft-media",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1032,13 +1025,15 @@ dependencies = [
|
||||
name = "deckcraft-isobmff"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"deckcraft-bitstream",
|
||||
"thiserror 2.0.21",
|
||||
"craft-media",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-matroska"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"craft-media",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-mcp"
|
||||
@@ -1645,12 +1640,6 @@ dependencies = [
|
||||
"objc2-foundation 0.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.2.0"
|
||||
@@ -2262,7 +2251,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3"
|
||||
dependencies = [
|
||||
"byteorder-lite",
|
||||
"quick-error 2.0.1",
|
||||
"quick-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2649,7 +2638,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a616d2fb8c89516ac2723a581f69d6c18576046bed761bd6b305e5618e6ae130"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"bit-set 0.10.0",
|
||||
"bit-set",
|
||||
"bitflags 2.13.2",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
@@ -3400,15 +3389,6 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "presser"
|
||||
version = "0.3.1"
|
||||
@@ -3439,37 +3419,12 @@ version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5"
|
||||
|
||||
[[package]]
|
||||
name = "proptest"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744"
|
||||
dependencies = [
|
||||
"bit-set 0.8.0",
|
||||
"bit-vec 0.8.0",
|
||||
"bitflags 2.13.2",
|
||||
"num-traits",
|
||||
"rand",
|
||||
"rand_chacha",
|
||||
"rand_xorshift",
|
||||
"regex-syntax",
|
||||
"rusty-fork",
|
||||
"tempfile",
|
||||
"unarray",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pxfm"
|
||||
version = "0.1.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea"
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "1.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "2.0.1"
|
||||
@@ -3506,44 +3461,6 @@ version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
|
||||
dependencies = [
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_xorshift"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a"
|
||||
dependencies = [
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "range-alloc"
|
||||
version = "0.1.5"
|
||||
@@ -3658,12 +3575,6 @@ dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||
|
||||
[[package]]
|
||||
name = "renderdoc-sys"
|
||||
version = "1.1.0"
|
||||
@@ -3750,18 +3661,6 @@ version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||
|
||||
[[package]]
|
||||
name = "rusty-fork"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"quick-error 1.2.3",
|
||||
"tempfile",
|
||||
"wait-timeout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustybuzz"
|
||||
version = "0.20.1"
|
||||
@@ -4399,7 +4298,7 @@ dependencies = [
|
||||
"fax",
|
||||
"flate2",
|
||||
"half",
|
||||
"quick-error 2.0.1",
|
||||
"quick-error",
|
||||
"weezl",
|
||||
"zune-jpeg",
|
||||
]
|
||||
@@ -4546,12 +4445,6 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unarray"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.18"
|
||||
@@ -4707,15 +4600,6 @@ version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wait-timeout"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
@@ -5044,8 +4928,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14c018fce9b6270aa203c2fdd56f3cce996713534bd757e4ea58c8560b121f14"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"bit-set 0.10.0",
|
||||
"bit-vec 0.9.1",
|
||||
"bit-set",
|
||||
"bit-vec",
|
||||
"bitflags 2.13.2",
|
||||
"bytemuck",
|
||||
"cfg_aliases",
|
||||
@@ -5117,7 +5001,7 @@ dependencies = [
|
||||
"android_system_properties",
|
||||
"arrayvec",
|
||||
"ash",
|
||||
"bit-set 0.10.0",
|
||||
"bit-set",
|
||||
"bitflags 2.13.2",
|
||||
"block2 0.6.2",
|
||||
"bytemuck",
|
||||
|
||||
@@ -59,6 +59,7 @@ harfrust = "0.13"
|
||||
vello_cpu = "0.2"
|
||||
vello_common = { version = "0.2", default-features = false }
|
||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif", "tiff", "bmp"] }
|
||||
craft-media = { git = "https://github.com/storytold/craft-libs", rev = "f0ad6c1e1d297908915f4be315b0fcfc9be7b5d4", version = "=0.1.0", default-features = false }
|
||||
craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "c3487320b2d1de657b860b3c8abb44e1f2b32bd8", version = "=0.1.0", default-features = false, features = ["write"] }
|
||||
i_overlay = "9"
|
||||
quick-xml = "0.41"
|
||||
|
||||
@@ -9,6 +9,4 @@ rust-version.workspace = true
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
|
||||
[dev-dependencies]
|
||||
proptest = { workspace = true }
|
||||
craft-media = { workspace = true, features = ["bitstream"] }
|
||||
|
||||
+4
-485
@@ -1,495 +1,14 @@
|
||||
//! Bit-level reading and writing shared by DeckCraft's codec crates (ported from FilmCraft).
|
||||
//! Bit-level reading and writing for DeckCraft's codec crates: a re-export of
|
||||
//! [`craft_media::bitstream`] from craft-libs, plus [`budget`].
|
||||
//!
|
||||
//! - [`BitReader`]: MSB-first reader with Exp-Golomb (`ue(v)`, `se(v)`), as used by H.264/HEVC/AV1 headers.
|
||||
//! - [`BitWriter`]: the matching writer (for encoders and header rewriting).
|
||||
//! - [`unescape_rbsp`] / [`escape_rbsp`]: remove/insert H.264/HEVC emulation-prevention bytes.
|
||||
//! - [`budget`]: the memory budget video decoders check their picture store against.
|
||||
//! - [`annexb_nals`] / [`length_prefixed_nals`]: split Annex-B byte streams and AVCC/HVCC samples into NAL units.
|
||||
//! - [`budget`]: the memory budget video decoders check their picture store against.
|
||||
|
||||
#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable))]
|
||||
|
||||
use std::fmt;
|
||||
pub use craft_media::bitstream::*;
|
||||
|
||||
pub mod budget;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BitError {
|
||||
/// Tried to read past the end of the buffer.
|
||||
Eof,
|
||||
/// An Exp-Golomb code longer than 32 leading zeros.
|
||||
InvalidExpGolomb,
|
||||
/// A NAL length prefix width outside 1..=4 bytes.
|
||||
InvalidLengthSize,
|
||||
}
|
||||
|
||||
impl fmt::Display for BitError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
BitError::Eof => f.write_str("unexpected end of bitstream"),
|
||||
BitError::InvalidExpGolomb => f.write_str("invalid Exp-Golomb code"),
|
||||
BitError::InvalidLengthSize => f.write_str("unsupported NAL length prefix size"),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl std::error::Error for BitError {}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, BitError>;
|
||||
|
||||
/// MSB-first bit reader over a byte slice.
|
||||
#[derive(Clone)]
|
||||
pub struct BitReader<'a> {
|
||||
data: &'a [u8],
|
||||
/// Position in bits.
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
impl<'a> BitReader<'a> {
|
||||
pub fn new(data: &'a [u8]) -> Self {
|
||||
Self { data, pos: 0 }
|
||||
}
|
||||
pub fn data(&self) -> &'a [u8] {
|
||||
self.data
|
||||
}
|
||||
/// Bit position from the start.
|
||||
pub fn position(&self) -> usize {
|
||||
self.pos
|
||||
}
|
||||
pub fn bits_left(&self) -> usize {
|
||||
(self.data.len() * 8).saturating_sub(self.pos)
|
||||
}
|
||||
pub fn is_byte_aligned(&self) -> bool {
|
||||
self.pos.is_multiple_of(8)
|
||||
}
|
||||
pub fn byte_align(&mut self) {
|
||||
self.pos = self.pos.div_ceil(8) * 8;
|
||||
}
|
||||
/// Byte offset of the current position (rounded down).
|
||||
pub fn byte_pos(&self) -> usize {
|
||||
self.pos / 8
|
||||
}
|
||||
pub fn seek_bits(&mut self, pos: usize) {
|
||||
self.pos = pos;
|
||||
}
|
||||
|
||||
/// Peek up to 32 bits without consuming (zero-padded past the end).
|
||||
#[inline]
|
||||
pub fn peek(&self, n: u32) -> u32 {
|
||||
debug_assert!(n <= 32);
|
||||
if n == 0 {
|
||||
return 0;
|
||||
}
|
||||
let byte = self.pos / 8;
|
||||
let shift = self.pos % 8;
|
||||
let mut v: u64 = 0;
|
||||
for i in 0..5 {
|
||||
v = (v << 8) | *self.data.get(byte + i).unwrap_or(&0) as u64;
|
||||
}
|
||||
// v holds 40 bits starting at `byte`
|
||||
((v << (24 + shift)) >> (64 - n)) as u32
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn read_bits(&mut self, n: u32) -> Result<u32> {
|
||||
if n as usize > self.bits_left() {
|
||||
return Err(BitError::Eof);
|
||||
}
|
||||
let v = self.peek(n);
|
||||
self.pos += n as usize;
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
pub fn read_bits_u64(&mut self, n: u32) -> Result<u64> {
|
||||
if n <= 32 {
|
||||
return self.read_bits(n).map(u64::from);
|
||||
}
|
||||
let hi = self.read_bits(n - 32)? as u64;
|
||||
let lo = self.read_bits(32)? as u64;
|
||||
Ok((hi << 32) | lo)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn read_bit(&mut self) -> Result<bool> {
|
||||
self.read_bits(1).map(|b| b == 1)
|
||||
}
|
||||
pub fn read_flag(&mut self) -> Result<bool> {
|
||||
self.read_bit()
|
||||
}
|
||||
|
||||
pub fn skip(&mut self, n: usize) -> Result<()> {
|
||||
if n > self.bits_left() {
|
||||
return Err(BitError::Eof);
|
||||
}
|
||||
self.pos += n;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Unsigned Exp-Golomb `ue(v)`.
|
||||
pub fn read_ue(&mut self) -> Result<u32> {
|
||||
let mut zeros = 0u32;
|
||||
while !self.read_bit()? {
|
||||
zeros += 1;
|
||||
if zeros > 32 {
|
||||
return Err(BitError::InvalidExpGolomb);
|
||||
}
|
||||
}
|
||||
if zeros == 0 {
|
||||
return Ok(0);
|
||||
}
|
||||
let v = self.read_bits_u64(zeros)?;
|
||||
let r = (1u64 << zeros) - 1 + v;
|
||||
u32::try_from(r).map_err(|_| BitError::InvalidExpGolomb)
|
||||
}
|
||||
|
||||
/// `ue(v) + add` (the `..._minus1` / `..._minus8` fields), failing instead of overflowing on a
|
||||
/// hostile value near `u32::MAX`.
|
||||
pub fn read_ue_plus(&mut self, add: u32) -> Result<u32> {
|
||||
self.read_ue()?.checked_add(add).ok_or(BitError::InvalidExpGolomb)
|
||||
}
|
||||
|
||||
/// Signed Exp-Golomb `se(v)`. Codes whose value does not fit an `i32` (the code for `2^31`) are rejected.
|
||||
pub fn read_se(&mut self) -> Result<i32> {
|
||||
let k = self.read_ue()? as i64;
|
||||
let v = if k & 1 == 1 { (k + 1) / 2 } else { -(k / 2) };
|
||||
i32::try_from(v).map_err(|_| BitError::InvalidExpGolomb)
|
||||
}
|
||||
|
||||
/// Truncated Exp-Golomb `te(v)` with range `max`.
|
||||
pub fn read_te(&mut self, max: u32) -> Result<u32> {
|
||||
if max > 1 { self.read_ue() } else { Ok(!self.read_bit()? as u32) }
|
||||
}
|
||||
|
||||
/// H.264/HEVC `more_rbsp_data()`: true if there is data before the rbsp trailing bits.
|
||||
pub fn more_rbsp_data(&self) -> bool {
|
||||
let total = self.data.len() * 8;
|
||||
if self.pos >= total {
|
||||
return false;
|
||||
}
|
||||
// find last set bit (the rbsp_stop_one_bit)
|
||||
let mut last = None;
|
||||
for (i, &b) in self.data.iter().enumerate().rev() {
|
||||
if b != 0 {
|
||||
last = Some(i * 8 + 7 - b.trailing_zeros() as usize);
|
||||
break;
|
||||
}
|
||||
}
|
||||
match last {
|
||||
Some(stop) => self.pos < stop,
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// MSB-first bit writer.
|
||||
#[derive(Default, Clone)]
|
||||
pub struct BitWriter {
|
||||
buf: Vec<u8>,
|
||||
acc: u64,
|
||||
nbits: u32,
|
||||
}
|
||||
|
||||
impl BitWriter {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
pub fn write_bits(&mut self, value: u32, n: u32) {
|
||||
debug_assert!(n <= 32);
|
||||
if n == 0 {
|
||||
return;
|
||||
}
|
||||
let v = (value as u64) & ((1u64 << n) - 1);
|
||||
self.acc = (self.acc << n) | v;
|
||||
self.nbits += n;
|
||||
while self.nbits >= 8 {
|
||||
self.nbits -= 8;
|
||||
self.buf.push((self.acc >> self.nbits) as u8);
|
||||
}
|
||||
self.acc &= (1u64 << self.nbits) - 1;
|
||||
}
|
||||
pub fn write_bit(&mut self, b: bool) {
|
||||
self.write_bits(b as u32, 1);
|
||||
}
|
||||
pub fn write_ue(&mut self, v: u32) {
|
||||
self.write_ue_u64(v as u64);
|
||||
}
|
||||
fn write_ue_u64(&mut self, v: u64) {
|
||||
let x = v + 1;
|
||||
let len = 64 - x.leading_zeros();
|
||||
let zeros = len - 1;
|
||||
for _ in 0..zeros {
|
||||
self.write_bit(false);
|
||||
}
|
||||
if len > 32 {
|
||||
self.write_bits((x >> 32) as u32, len - 32);
|
||||
self.write_bits(x as u32, 32);
|
||||
} else {
|
||||
self.write_bits(x as u32, len);
|
||||
}
|
||||
}
|
||||
/// Signed Exp-Golomb `se(v)`. `i32::MIN` needs a code above `u32::MAX`, which `read_se` rejects.
|
||||
pub fn write_se(&mut self, v: i32) {
|
||||
let v = v as i64;
|
||||
let k = if v > 0 { (v as u64) * 2 - 1 } else { ((-v) as u64) * 2 };
|
||||
self.write_ue_u64(k);
|
||||
}
|
||||
pub fn bit_len(&self) -> usize {
|
||||
self.buf.len() * 8 + self.nbits as usize
|
||||
}
|
||||
pub fn is_byte_aligned(&self) -> bool {
|
||||
self.nbits == 0
|
||||
}
|
||||
/// Pad with zero bits to a byte boundary.
|
||||
pub fn align_zero(&mut self) {
|
||||
if self.nbits > 0 {
|
||||
self.write_bits(0, 8 - self.nbits);
|
||||
}
|
||||
}
|
||||
/// `rbsp_trailing_bits()`: a one bit then zeros to alignment.
|
||||
pub fn rbsp_trailing(&mut self) {
|
||||
self.write_bit(true);
|
||||
self.align_zero();
|
||||
}
|
||||
pub fn write_bytes(&mut self, bytes: &[u8]) {
|
||||
if self.nbits == 0 {
|
||||
self.buf.extend_from_slice(bytes);
|
||||
} else {
|
||||
for &b in bytes {
|
||||
self.write_bits(b as u32, 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn finish(mut self) -> Vec<u8> {
|
||||
self.align_zero();
|
||||
self.buf
|
||||
}
|
||||
pub fn bytes(&self) -> &[u8] {
|
||||
&self.buf
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove emulation-prevention bytes (`00 00 03` → `00 00`) from a NAL payload.
|
||||
pub fn unescape_rbsp(data: &[u8]) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity(data.len());
|
||||
let mut zeros = 0;
|
||||
for &b in data {
|
||||
if zeros >= 2 && b == 3 {
|
||||
zeros = 0;
|
||||
continue;
|
||||
}
|
||||
out.push(b);
|
||||
zeros = if b == 0 { zeros + 1 } else { 0 };
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Insert emulation-prevention bytes so no `00 00 0x` (x ≤ 3) sequence appears.
|
||||
pub fn escape_rbsp(data: &[u8]) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity(data.len() + data.len() / 64);
|
||||
let mut zeros = 0;
|
||||
for &b in data {
|
||||
if zeros >= 2 && b <= 3 {
|
||||
out.push(3);
|
||||
zeros = 0;
|
||||
}
|
||||
out.push(b);
|
||||
zeros = if b == 0 { zeros + 1 } else { 0 };
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Split an Annex-B byte stream (start codes `00 00 01` / `00 00 00 01`) into NAL units (escaped payloads).
|
||||
pub fn annexb_nals(data: &[u8]) -> Vec<&[u8]> {
|
||||
let mut starts = Vec::new();
|
||||
let mut i = 0;
|
||||
while i + 3 <= data.len() {
|
||||
if data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1 {
|
||||
starts.push(i + 3);
|
||||
i += 3;
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
let mut out = Vec::with_capacity(starts.len());
|
||||
for (k, &s) in starts.iter().enumerate() {
|
||||
let mut e = if k + 1 < starts.len() { starts[k + 1] - 3 } else { data.len() };
|
||||
// trim trailing zero bytes (part of a 4-byte start code or trailing_zero_8bits)
|
||||
while e > s && data[e - 1] == 0 {
|
||||
e -= 1;
|
||||
}
|
||||
if e > s {
|
||||
out.push(&data[s..e]);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Split a length-prefixed sample (AVCC/HVCC, `length_size` 1..=4 bytes, as the 2-bit `lengthSizeMinusOne` allows) into NAL units.
|
||||
pub fn length_prefixed_nals(data: &[u8], length_size: usize) -> Result<Vec<&[u8]>> {
|
||||
if !(1..=4).contains(&length_size) {
|
||||
return Err(BitError::InvalidLengthSize);
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
let mut i = 0;
|
||||
while i < data.len() {
|
||||
if length_size > data.len() - i {
|
||||
return Err(BitError::Eof);
|
||||
}
|
||||
let mut len = 0usize;
|
||||
for k in 0..length_size {
|
||||
len = (len << 8) | data[i + k] as usize;
|
||||
}
|
||||
i += length_size;
|
||||
if len > data.len() - i {
|
||||
return Err(BitError::Eof);
|
||||
}
|
||||
out.push(&data[i..i + len]);
|
||||
i += len;
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// CRC-32 (IEEE, reflected) — used by containers (e.g. Matroska CRC-32 elements, PNG).
|
||||
pub fn crc32(data: &[u8]) -> u32 {
|
||||
let mut crc = 0xFFFF_FFFFu32;
|
||||
for &b in data {
|
||||
crc ^= b as u32;
|
||||
for _ in 0..8 {
|
||||
crc = if crc & 1 != 0 { (crc >> 1) ^ 0xEDB8_8320 } else { crc >> 1 };
|
||||
}
|
||||
}
|
||||
!crc
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use proptest::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn read_ue_plus_rejects_overflow() {
|
||||
let mut w = BitWriter::new();
|
||||
w.write_ue(7);
|
||||
w.write_ue(u32::MAX);
|
||||
w.rbsp_trailing();
|
||||
let b = w.finish();
|
||||
let mut r = BitReader::new(&b);
|
||||
assert_eq!(r.read_ue_plus(1).unwrap(), 8);
|
||||
assert!(r.read_ue_plus(1).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signed_exp_golomb_extremes() {
|
||||
let mut w = BitWriter::new();
|
||||
for v in [i32::MAX, i32::MIN + 1, i32::MIN] {
|
||||
w.write_se(v);
|
||||
}
|
||||
w.rbsp_trailing();
|
||||
let b = w.finish();
|
||||
let mut r = BitReader::new(&b);
|
||||
assert_eq!(r.read_se().unwrap(), i32::MAX);
|
||||
assert_eq!(r.read_se().unwrap(), i32::MIN + 1);
|
||||
assert_eq!(r.read_se(), Err(BitError::InvalidExpGolomb));
|
||||
// ue code 0xFFFF_FFFF maps to +2^31, which must not wrap to a negative i32.
|
||||
let mut w = BitWriter::new();
|
||||
w.write_ue(u32::MAX);
|
||||
w.rbsp_trailing();
|
||||
let b = w.finish();
|
||||
assert_eq!(BitReader::new(&b).read_se(), Err(BitError::InvalidExpGolomb));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn length_prefixed_rejects_bad_width_and_oversized_length() {
|
||||
assert_eq!(length_prefixed_nals(&[1, 2, 3, 4, 5, 6], 5), Err(BitError::InvalidLengthSize));
|
||||
assert_eq!(length_prefixed_nals(&[1, 2, 3], 0), Err(BitError::InvalidLengthSize));
|
||||
assert_eq!(length_prefixed_nals(&[0, 0, 1, 7], 3).unwrap(), vec![&[7u8][..]]);
|
||||
assert_eq!(length_prefixed_nals(&[0xFF, 0xFF, 0xFF, 0xFF, 1], 4), Err(BitError::Eof));
|
||||
assert_eq!(length_prefixed_nals(&[0, 0, 0, 1, 9], 4).unwrap(), vec![&[9u8][..]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reads_bits() {
|
||||
let d = [0b1010_1100, 0b0101_0011];
|
||||
let mut r = BitReader::new(&d);
|
||||
assert_eq!(r.read_bits(1).unwrap(), 1);
|
||||
assert_eq!(r.read_bits(3).unwrap(), 0b010);
|
||||
assert_eq!(r.read_bits(8).unwrap(), 0b1100_0101);
|
||||
assert_eq!(r.bits_left(), 4);
|
||||
assert_eq!(r.read_bits(5), Err(BitError::Eof));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exp_golomb_known() {
|
||||
// 1 | 010 | 011 | 00100 | 00101 -> 0,1,2,3,4
|
||||
let mut w = BitWriter::new();
|
||||
for v in 0..5 {
|
||||
w.write_ue(v);
|
||||
}
|
||||
let b = w.finish();
|
||||
let mut r = BitReader::new(&b);
|
||||
for v in 0..5 {
|
||||
assert_eq!(r.read_ue().unwrap(), v);
|
||||
}
|
||||
let mut w = BitWriter::new();
|
||||
for v in [0, 1, -1, 2, -2, 1000, -1000] {
|
||||
w.write_se(v);
|
||||
}
|
||||
let b = w.finish();
|
||||
let mut r = BitReader::new(&b);
|
||||
for v in [0, 1, -1, 2, -2, 1000, -1000] {
|
||||
assert_eq!(r.read_se().unwrap(), v);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emulation_prevention() {
|
||||
let raw = [0, 0, 1, 0, 0, 0, 5, 0, 0, 3];
|
||||
let esc = escape_rbsp(&raw);
|
||||
assert_eq!(esc, vec![0, 0, 3, 1, 0, 0, 3, 0, 5, 0, 0, 3, 3]);
|
||||
assert_eq!(unescape_rbsp(&esc), raw);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn annexb_split() {
|
||||
let s = [0, 0, 0, 1, 0x67, 1, 2, 0, 0, 1, 0x68, 3, 0, 0, 0, 1, 0x65, 9, 9];
|
||||
let n = annexb_nals(&s);
|
||||
assert_eq!(n, vec![&[0x67, 1, 2][..], &[0x68, 3][..], &[0x65, 9, 9][..]]);
|
||||
let l = [0, 0, 0, 2, 0x67, 1, 0, 0, 0, 1, 0x68];
|
||||
assert_eq!(length_prefixed_nals(&l, 4).unwrap(), vec![&[0x67, 1][..], &[0x68][..]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn more_rbsp() {
|
||||
let d = [0b1100_0000];
|
||||
let mut r = BitReader::new(&d);
|
||||
assert!(r.more_rbsp_data());
|
||||
r.read_bit().unwrap();
|
||||
assert!(!r.more_rbsp_data());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crc() {
|
||||
assert_eq!(crc32(b"123456789"), 0xCBF4_3926);
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn roundtrip(vals in proptest::collection::vec((0u32..u32::MAX, 1u32..=32), 0..200), ues in proptest::collection::vec(0u32..u32::MAX - 1, 0..50)) {
|
||||
let mut w = BitWriter::new();
|
||||
for &(v, n) in &vals { w.write_bits(v, n); }
|
||||
for &u in &ues { w.write_ue(u); }
|
||||
let b = w.finish();
|
||||
let mut r = BitReader::new(&b);
|
||||
for &(v, n) in &vals {
|
||||
let mask = if n == 32 { u32::MAX } else { (1 << n) - 1 };
|
||||
prop_assert_eq!(r.read_bits(n).unwrap(), v & mask);
|
||||
}
|
||||
for &u in &ues { prop_assert_eq!(r.read_ue().unwrap(), u); }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escape_roundtrip(data in proptest::collection::vec(0u8..4, 0..300)) {
|
||||
let e = escape_rbsp(&data);
|
||||
prop_assert_eq!(unescape_rbsp(&e), data);
|
||||
for w in e.windows(3) { prop_assert!(!(w[0] == 0 && w[1] == 0 && w[2] < 3)); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,5 +10,4 @@ rust-version.workspace = true
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
deckcraft-bitstream = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
craft-media = { workspace = true, features = ["isobmff"] }
|
||||
|
||||
@@ -1,361 +0,0 @@
|
||||
//! Low-level helpers: four-character codes, a bounds-checked big-endian cursor, box iteration
|
||||
//! over in-memory payloads, and a box builder for the muxers.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use std::fmt;
|
||||
|
||||
/// A four-character code (box type, sample entry format, brand, handler type…).
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
|
||||
pub struct FourCc(pub [u8; 4]);
|
||||
|
||||
impl FourCc {
|
||||
pub const fn new(b: &[u8; 4]) -> Self {
|
||||
FourCc(*b)
|
||||
}
|
||||
pub fn as_u32(self) -> u32 {
|
||||
u32::from_be_bytes(self.0)
|
||||
}
|
||||
/// Lossy string form; `©` (0xA9) is mapped to U+00A9, other non-printables to `?`.
|
||||
pub fn to_string_lossy(self) -> String {
|
||||
self.0
|
||||
.iter()
|
||||
.map(|&b| match b {
|
||||
0xA9 => '©',
|
||||
0x20..=0x7E => b as char,
|
||||
_ => '?',
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for FourCc {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&self.to_string_lossy())
|
||||
}
|
||||
}
|
||||
impl fmt::Debug for FourCc {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "FourCc({:?})", self.to_string_lossy())
|
||||
}
|
||||
}
|
||||
impl From<&[u8; 4]> for FourCc {
|
||||
fn from(b: &[u8; 4]) -> Self {
|
||||
FourCc(*b)
|
||||
}
|
||||
}
|
||||
impl PartialEq<[u8; 4]> for FourCc {
|
||||
fn eq(&self, other: &[u8; 4]) -> bool {
|
||||
&self.0 == other
|
||||
}
|
||||
}
|
||||
impl PartialEq<&[u8; 4]> for FourCc {
|
||||
fn eq(&self, other: &&[u8; 4]) -> bool {
|
||||
&self.0 == *other
|
||||
}
|
||||
}
|
||||
impl PartialEq<&&[u8; 4]> for FourCc {
|
||||
fn eq(&self, other: &&&[u8; 4]) -> bool {
|
||||
&self.0 == **other
|
||||
}
|
||||
}
|
||||
|
||||
/// Bounds-checked big-endian cursor over a byte slice.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Cur<'a> {
|
||||
pub data: &'a [u8],
|
||||
pub pos: usize,
|
||||
what: &'static str,
|
||||
}
|
||||
|
||||
impl<'a> Cur<'a> {
|
||||
pub fn new(data: &'a [u8], what: &'static str) -> Self {
|
||||
Cur { data, pos: 0, what }
|
||||
}
|
||||
pub fn remaining(&self) -> usize {
|
||||
self.data.len().saturating_sub(self.pos)
|
||||
}
|
||||
pub fn rest(&self) -> &'a [u8] {
|
||||
self.data.get(self.pos..).unwrap_or(&[])
|
||||
}
|
||||
pub fn bytes(&mut self, n: usize) -> Result<&'a [u8]> {
|
||||
let end = self.pos.checked_add(n).ok_or(Error::Truncated(self.what))?;
|
||||
let s = self.data.get(self.pos..end).ok_or(Error::Truncated(self.what))?;
|
||||
self.pos = end;
|
||||
Ok(s)
|
||||
}
|
||||
pub fn skip(&mut self, n: usize) -> Result<()> {
|
||||
self.bytes(n).map(|_| ())
|
||||
}
|
||||
pub fn array<const N: usize>(&mut self) -> Result<[u8; N]> {
|
||||
let s = self.bytes(N)?;
|
||||
let mut a = [0u8; N];
|
||||
a.copy_from_slice(s);
|
||||
Ok(a)
|
||||
}
|
||||
pub fn u8(&mut self) -> Result<u8> {
|
||||
Ok(self.array::<1>()?[0])
|
||||
}
|
||||
pub fn u16(&mut self) -> Result<u16> {
|
||||
Ok(u16::from_be_bytes(self.array()?))
|
||||
}
|
||||
pub fn i16(&mut self) -> Result<i16> {
|
||||
Ok(i16::from_be_bytes(self.array()?))
|
||||
}
|
||||
pub fn u24(&mut self) -> Result<u32> {
|
||||
let a = self.array::<3>()?;
|
||||
Ok(((a[0] as u32) << 16) | ((a[1] as u32) << 8) | a[2] as u32)
|
||||
}
|
||||
pub fn u32(&mut self) -> Result<u32> {
|
||||
Ok(u32::from_be_bytes(self.array()?))
|
||||
}
|
||||
pub fn i32(&mut self) -> Result<i32> {
|
||||
Ok(i32::from_be_bytes(self.array()?))
|
||||
}
|
||||
pub fn u64(&mut self) -> Result<u64> {
|
||||
Ok(u64::from_be_bytes(self.array()?))
|
||||
}
|
||||
pub fn i64(&mut self) -> Result<i64> {
|
||||
Ok(i64::from_be_bytes(self.array()?))
|
||||
}
|
||||
pub fn f64(&mut self) -> Result<f64> {
|
||||
Ok(f64::from_be_bytes(self.array()?))
|
||||
}
|
||||
pub fn fourcc(&mut self) -> Result<FourCc> {
|
||||
Ok(FourCc(self.array()?))
|
||||
}
|
||||
/// Full-box header: (version, flags).
|
||||
pub fn full_header(&mut self) -> Result<(u8, u32)> {
|
||||
let v = self.u32()?;
|
||||
Ok(((v >> 24) as u8, v & 0x00FF_FFFF))
|
||||
}
|
||||
/// Read a count and verify that `count * entry_size` bytes remain (allocation guard).
|
||||
pub fn count(&mut self, entry_size: usize) -> Result<usize> {
|
||||
let n = self.u32()? as usize;
|
||||
self.check_count(n, entry_size)?;
|
||||
Ok(n)
|
||||
}
|
||||
pub fn check_count(&self, n: usize, entry_size: usize) -> Result<()> {
|
||||
match n.checked_mul(entry_size) {
|
||||
Some(b) if b <= self.remaining() => Ok(()),
|
||||
_ => Err(Error::Truncated(self.what)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Header of one box as found in a byte stream.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct BoxHeader {
|
||||
pub kind: FourCc,
|
||||
/// Header length including largesize and uuid extended type.
|
||||
pub header_len: u64,
|
||||
/// Total box size including header; `None` means "extends to end of the enclosing container".
|
||||
pub size: Option<u64>,
|
||||
pub uuid: Option<[u8; 16]>,
|
||||
}
|
||||
|
||||
/// Parse a box header from `buf` (needs up to 32 bytes; fewer if available).
|
||||
pub(crate) fn parse_box_header(buf: &[u8]) -> Result<BoxHeader> {
|
||||
let mut c = Cur::new(buf, "box header");
|
||||
let size32 = c.u32()?;
|
||||
let kind = c.fourcc()?;
|
||||
let mut header_len = 8u64;
|
||||
let size = match size32 {
|
||||
0 => None,
|
||||
1 => {
|
||||
header_len += 8;
|
||||
let s = c.u64()?;
|
||||
Some(s)
|
||||
}
|
||||
s => Some(s as u64),
|
||||
};
|
||||
let uuid = if kind == b"uuid" {
|
||||
header_len += 16;
|
||||
Some(c.array::<16>()?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(s) = size
|
||||
&& s < header_len
|
||||
{
|
||||
return Err(Error::Invalid(format!("box '{kind}' size {s} smaller than its header")));
|
||||
}
|
||||
Ok(BoxHeader { kind, header_len, size, uuid })
|
||||
}
|
||||
|
||||
/// One child box inside an in-memory payload.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct RawBox<'a> {
|
||||
pub kind: FourCc,
|
||||
pub payload: &'a [u8],
|
||||
#[allow(dead_code)]
|
||||
pub uuid: Option<[u8; 16]>,
|
||||
}
|
||||
|
||||
/// Iterator over the child boxes of a payload. Yields an error for a malformed box and then stops.
|
||||
pub(crate) struct Boxes<'a> {
|
||||
data: &'a [u8],
|
||||
pos: usize,
|
||||
done: bool,
|
||||
}
|
||||
|
||||
pub(crate) fn boxes(data: &[u8]) -> Boxes<'_> {
|
||||
Boxes { data, pos: 0, done: false }
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Boxes<'a> {
|
||||
type Item = Result<RawBox<'a>>;
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.done {
|
||||
return None;
|
||||
}
|
||||
let rest = &self.data[self.pos..];
|
||||
if rest.is_empty() {
|
||||
self.done = true;
|
||||
return None;
|
||||
}
|
||||
// QuickTime allows a 32-bit zero terminator at the end of some containers.
|
||||
if rest.len() < 8 {
|
||||
self.done = true;
|
||||
if rest.iter().all(|&b| b == 0) {
|
||||
return None;
|
||||
}
|
||||
return Some(Err(Error::Truncated("box header")));
|
||||
}
|
||||
let h = match parse_box_header(rest) {
|
||||
Ok(h) => h,
|
||||
Err(e) => {
|
||||
self.done = true;
|
||||
return Some(Err(e));
|
||||
}
|
||||
};
|
||||
let size = h.size.unwrap_or(rest.len() as u64);
|
||||
if size > rest.len() as u64 {
|
||||
self.done = true;
|
||||
return Some(Err(Error::Invalid(format!("box '{}' size {} exceeds its container ({} bytes left)", h.kind, size, rest.len()))));
|
||||
}
|
||||
let size = size as usize;
|
||||
let payload = &rest[h.header_len as usize..size];
|
||||
self.pos += size;
|
||||
Some(Ok(RawBox { kind: h.kind, payload, uuid: h.uuid }))
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the first child of the given type (malformed trailing children are ignored).
|
||||
pub(crate) fn find<'a>(data: &'a [u8], kind: &[u8; 4]) -> Option<&'a [u8]> {
|
||||
boxes(data).map_while(|b| b.ok()).find(|b| b.kind == kind).map(|b| b.payload)
|
||||
}
|
||||
|
||||
/// Growable buffer for building boxes.
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub(crate) struct BoxBuf {
|
||||
pub buf: Vec<u8>,
|
||||
}
|
||||
|
||||
impl BoxBuf {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
/// Begin a box; returns a marker to pass to [`end`](Self::end).
|
||||
pub fn start(&mut self, kind: &[u8; 4]) -> usize {
|
||||
let at = self.buf.len();
|
||||
self.buf.extend_from_slice(&[0, 0, 0, 0]);
|
||||
self.buf.extend_from_slice(kind);
|
||||
at
|
||||
}
|
||||
pub fn start_full(&mut self, kind: &[u8; 4], version: u8, flags: u32) -> usize {
|
||||
let at = self.start(kind);
|
||||
self.u32(((version as u32) << 24) | (flags & 0xFF_FFFF));
|
||||
at
|
||||
}
|
||||
pub fn end(&mut self, at: usize) {
|
||||
let size = (self.buf.len() - at) as u32;
|
||||
self.buf[at..at + 4].copy_from_slice(&size.to_be_bytes());
|
||||
}
|
||||
/// Append a complete box with the given payload.
|
||||
pub fn leaf(&mut self, kind: &[u8; 4], payload: &[u8]) {
|
||||
let at = self.start(kind);
|
||||
self.bytes(payload);
|
||||
self.end(at);
|
||||
}
|
||||
pub fn u8(&mut self, v: u8) {
|
||||
self.buf.push(v);
|
||||
}
|
||||
pub fn u16(&mut self, v: u16) {
|
||||
self.buf.extend_from_slice(&v.to_be_bytes());
|
||||
}
|
||||
pub fn i16(&mut self, v: i16) {
|
||||
self.buf.extend_from_slice(&v.to_be_bytes());
|
||||
}
|
||||
pub fn u32(&mut self, v: u32) {
|
||||
self.buf.extend_from_slice(&v.to_be_bytes());
|
||||
}
|
||||
pub fn i32(&mut self, v: i32) {
|
||||
self.buf.extend_from_slice(&v.to_be_bytes());
|
||||
}
|
||||
pub fn u64(&mut self, v: u64) {
|
||||
self.buf.extend_from_slice(&v.to_be_bytes());
|
||||
}
|
||||
pub fn i64(&mut self, v: i64) {
|
||||
self.buf.extend_from_slice(&v.to_be_bytes());
|
||||
}
|
||||
pub fn f64(&mut self, v: f64) {
|
||||
self.buf.extend_from_slice(&v.to_be_bytes());
|
||||
}
|
||||
pub fn bytes(&mut self, b: &[u8]) {
|
||||
self.buf.extend_from_slice(b);
|
||||
}
|
||||
pub fn zeros(&mut self, n: usize) {
|
||||
self.buf.resize(self.buf.len() + n, 0);
|
||||
}
|
||||
pub fn len(&self) -> usize {
|
||||
self.buf.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn header_forms() {
|
||||
let h = parse_box_header(&[0, 0, 0, 16, b'f', b'r', b'e', b'e']).unwrap();
|
||||
assert_eq!((h.kind, h.header_len, h.size), (FourCc(*b"free"), 8, Some(16)));
|
||||
let h = parse_box_header(&[0, 0, 0, 0, b'm', b'd', b'a', b't']).unwrap();
|
||||
assert_eq!(h.size, None);
|
||||
let mut v = vec![0, 0, 0, 1, b'm', b'd', b'a', b't'];
|
||||
v.extend_from_slice(&100u64.to_be_bytes());
|
||||
let h = parse_box_header(&v).unwrap();
|
||||
assert_eq!((h.header_len, h.size), (16, Some(100)));
|
||||
let mut v = vec![0, 0, 0, 30, b'u', b'u', b'i', b'd'];
|
||||
v.extend_from_slice(&[7u8; 16]);
|
||||
let h = parse_box_header(&v).unwrap();
|
||||
assert_eq!((h.header_len, h.uuid), (24, Some([7u8; 16])));
|
||||
assert!(parse_box_header(&[0, 0, 0, 4, b'f', b'r', b'e', b'e']).is_err());
|
||||
assert!(parse_box_header(&[0, 0, 0]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iterate_children() {
|
||||
let mut b = BoxBuf::new();
|
||||
b.leaf(b"aaaa", &[1, 2]);
|
||||
let m = b.start(b"bbbb");
|
||||
b.leaf(b"cccc", &[]);
|
||||
b.end(m);
|
||||
b.u32(0); // QT terminator
|
||||
let kids: Vec<_> = boxes(&b.buf).collect::<Result<Vec<_>>>().unwrap();
|
||||
assert_eq!(kids.len(), 2);
|
||||
assert_eq!(kids[0].payload, &[1, 2]);
|
||||
assert_eq!(find(&b.buf, b"bbbb").map(|p| p.len()), Some(8));
|
||||
// Oversized child → error, no panic.
|
||||
let bad = [0u8, 0, 0, 99, b'x', b'x', b'x', b'x', 0];
|
||||
assert!(boxes(&bad).next().unwrap().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cursor_bounds() {
|
||||
let mut c = Cur::new(&[0, 0, 0, 5, 1], "t");
|
||||
assert!(c.count(1).is_err());
|
||||
let mut c = Cur::new(&[0xFF, 0xFF, 0xFF, 0xFF], "t");
|
||||
assert!(c.count(usize::MAX).is_err());
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,38 +0,0 @@
|
||||
use std::io;
|
||||
|
||||
/// Errors produced by the demuxer and muxer.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("I/O error: {0}")]
|
||||
Io(#[from] io::Error),
|
||||
/// A box or structure ended before all required fields were read.
|
||||
#[error("truncated data while reading {0}")]
|
||||
Truncated(&'static str),
|
||||
/// Structurally invalid data.
|
||||
#[error("invalid data: {0}")]
|
||||
Invalid(String),
|
||||
/// Valid but unsupported feature (e.g. compressed `cmov` movie header).
|
||||
#[error("unsupported: {0}")]
|
||||
Unsupported(String),
|
||||
/// A count or size exceeds sanity limits relative to the file size.
|
||||
#[error("size limit exceeded: {0}")]
|
||||
TooLarge(&'static str),
|
||||
/// No `moov` box was found.
|
||||
#[error("no moov box found")]
|
||||
NoMoov,
|
||||
/// A track index was out of range.
|
||||
#[error("track index {0} out of range")]
|
||||
NoSuchTrack(usize),
|
||||
/// A sample index was out of range.
|
||||
#[error("sample index {0} out of range")]
|
||||
NoSuchSample(usize),
|
||||
/// Muxer usage error (e.g. invalid track config, bad sample).
|
||||
#[error("muxer: {0}")]
|
||||
Mux(String),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
pub(crate) fn invalid<T>(msg: impl Into<String>) -> Result<T> {
|
||||
Err(Error::Invalid(msg.into()))
|
||||
}
|
||||
@@ -1,28 +1,6 @@
|
||||
//! Clean-room ISO Base Media File Format (MP4, ISO/IEC 14496-12/14/15) and QuickTime (MOV) support.
|
||||
//!
|
||||
//! - **Demux:** [`open`] parses box headers plus `moov` (and `moof` for fragmented files) from a
|
||||
//! [`ByteSource`] and builds per-track [`Sample`] tables; media data is read on demand with
|
||||
//! [`Mp4File::read_sample`].
|
||||
//! - **Mux:** [`Mp4Writer`] writes progressive MP4/MOV (optionally faststart);
|
||||
//! [`FragmentedWriter`] writes fragmented MP4.
|
||||
//!
|
||||
//! See the crate README for supported boxes and limitations.
|
||||
//! ISO BMFF (MP4) and QuickTime (MOV) demuxer and muxer: a re-export of [`craft_media::isobmff`]
|
||||
//! from craft-libs. Supported boxes and limitations are in the craft-media README.
|
||||
|
||||
#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable))]
|
||||
|
||||
mod bytes;
|
||||
mod codec;
|
||||
mod demux;
|
||||
mod error;
|
||||
mod mux;
|
||||
mod source;
|
||||
|
||||
pub use bytes::FourCc;
|
||||
pub use codec::{
|
||||
AacConfig, AudioParams, Av1Config, AvcConfig, BitRate, CleanAperture, CodecConfig, ColorInfo, FieldInfo, FlacConfig, HevcConfig, HevcNalArray,
|
||||
MasteringDisplay, OpusConfig, PcmConfig, SampleEntry, TimecodeConfig, VideoParams, VpcConfig, parse_asc,
|
||||
};
|
||||
pub use demux::{Edit, Metadata, Mp4File, Sample, SampleGroup, Track, TrackKind, open};
|
||||
pub use error::{Error, Result};
|
||||
pub use mux::{Brand, FragmentedWriter, Mp4Writer, TrackConfig, WriteSample, WriterOptions};
|
||||
pub use source::ByteSource;
|
||||
pub use craft_media::isobmff::*;
|
||||
|
||||
@@ -1,969 +0,0 @@
|
||||
//! Muxers: progressive MP4/MOV ([`Mp4Writer`], optional faststart) and fragmented MP4 ([`FragmentedWriter`]).
|
||||
|
||||
use crate::bytes::BoxBuf;
|
||||
use crate::codec::{CodecConfig, SampleEntry, TimecodeConfig};
|
||||
use crate::demux::Edit;
|
||||
use crate::error::{Error, Result};
|
||||
use std::io::{Read, Seek, SeekFrom, Write};
|
||||
|
||||
/// Output flavour: determines brands and some sample-entry encodings.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Brand {
|
||||
/// ISO MP4: `isom` major brand, compatible `isom iso2 avc1 mp41 mp42`.
|
||||
Mp4,
|
||||
/// QuickTime MOV: `qt ` major brand.
|
||||
Mov,
|
||||
}
|
||||
|
||||
/// Movie-level writer options.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct WriterOptions {
|
||||
pub brand: Brand,
|
||||
/// Movie timescale (`mvhd`); default 1000.
|
||||
pub movie_timescale: u32,
|
||||
/// Text metadata written to `moov/udta` (keys like `©nam`, `©ART`, `©cmt`, `©too`).
|
||||
pub metadata: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
impl WriterOptions {
|
||||
pub fn new(brand: Brand) -> Self {
|
||||
WriterOptions { brand, movie_timescale: 1000, metadata: Vec::new() }
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-track configuration.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TrackConfig {
|
||||
pub entry: SampleEntry,
|
||||
/// Media timescale. For PCM it must equal the sample rate (one tick per audio frame).
|
||||
pub timescale: u32,
|
||||
/// ISO 639-2/T language (`und` by default).
|
||||
pub language: String,
|
||||
/// Explicit edit list (segment durations in the movie timescale). Takes precedence over `media_start`.
|
||||
pub edits: Vec<Edit>,
|
||||
/// Convenience: create one edit starting at this media time (e.g. AAC encoder delay / priming,
|
||||
/// or the first pts of a B-frame stream) spanning the rest of the media.
|
||||
pub media_start: Option<i64>,
|
||||
/// `hdlr` name; a sensible default is used when `None`.
|
||||
pub handler_name: Option<String>,
|
||||
}
|
||||
|
||||
impl TrackConfig {
|
||||
pub fn new(entry: SampleEntry, timescale: u32) -> Self {
|
||||
TrackConfig { entry, timescale, language: "und".into(), edits: Vec::new(), media_start: None, handler_name: None }
|
||||
}
|
||||
}
|
||||
|
||||
/// A sample handed to the muxers.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct WriteSample<'a> {
|
||||
pub data: &'a [u8],
|
||||
/// Duration in the media timescale. Ignored for PCM (each frame lasts one tick).
|
||||
pub duration: u32,
|
||||
/// pts − dts in the media timescale.
|
||||
pub composition_offset: i32,
|
||||
pub is_sync: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Handler {
|
||||
Video,
|
||||
Audio,
|
||||
Timecode,
|
||||
Other,
|
||||
}
|
||||
|
||||
fn handler_of(e: &SampleEntry) -> Handler {
|
||||
if matches!(e.codec, CodecConfig::Timecode(_)) {
|
||||
Handler::Timecode
|
||||
} else if e.video.is_some() {
|
||||
Handler::Video
|
||||
} else if e.audio.is_some() {
|
||||
Handler::Audio
|
||||
} else {
|
||||
Handler::Other
|
||||
}
|
||||
}
|
||||
|
||||
/// Accumulated sample tables for one track.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct Tables {
|
||||
stts: Vec<(u32, u32)>,
|
||||
ctts: Vec<(u32, i32)>,
|
||||
sizes: Vec<u32>,
|
||||
/// Constant sample size (PCM) — then `sizes` is unused.
|
||||
const_size: Option<u32>,
|
||||
sample_count: u64,
|
||||
/// 1-based sync sample numbers.
|
||||
sync: Vec<u32>,
|
||||
all_sync: bool,
|
||||
/// (offset relative to file start before shifting, sample count)
|
||||
chunks: Vec<(u64, u32)>,
|
||||
media_duration: u64,
|
||||
/// End of the last sample on the composition timeline (decode time + composition offset +
|
||||
/// duration, maximum over samples).
|
||||
comp_end: u64,
|
||||
}
|
||||
|
||||
impl Tables {
|
||||
fn new() -> Self {
|
||||
Tables { all_sync: true, ..Default::default() }
|
||||
}
|
||||
fn push_run<T: PartialEq + Copy>(runs: &mut Vec<(u32, T)>, count: u32, v: T) {
|
||||
if let Some(last) = runs.last_mut()
|
||||
&& last.1 == v
|
||||
&& last.0.checked_add(count).is_some()
|
||||
{
|
||||
last.0 += count;
|
||||
return;
|
||||
}
|
||||
runs.push((count, v));
|
||||
}
|
||||
}
|
||||
|
||||
struct WTrack {
|
||||
cfg: TrackConfig,
|
||||
id: u32,
|
||||
handler: Handler,
|
||||
pcm_frame_bytes: Option<u32>,
|
||||
t: Tables,
|
||||
/// Track id of a linked timecode track (`tref/tmcd`).
|
||||
tmcd_ref: Option<u32>,
|
||||
/// For timecode tracks: (video track index, start frame).
|
||||
tmcd_source: Option<(usize, u32)>,
|
||||
}
|
||||
|
||||
fn pack_language(lang: &str) -> u16 {
|
||||
let b = lang.as_bytes();
|
||||
if b.len() == 3 && b.iter().all(|c| c.is_ascii_lowercase()) {
|
||||
(((b[0] - 0x60) as u16) << 10) | (((b[1] - 0x60) as u16) << 5) | ((b[2] - 0x60) as u16)
|
||||
} else {
|
||||
0x55C4 // "und"
|
||||
}
|
||||
}
|
||||
|
||||
fn rescale(v: u64, from: u32, to: u32) -> u64 {
|
||||
if from == 0 {
|
||||
return 0;
|
||||
}
|
||||
(v as u128 * to as u128 / from as u128).min(u64::MAX as u128) as u64
|
||||
}
|
||||
|
||||
const MATRIX: [i32; 9] = [0x10000, 0, 0, 0, 0x10000, 0, 0, 0, 0x4000_0000];
|
||||
|
||||
fn write_ftyp(b: &mut BoxBuf, brand: Brand) {
|
||||
let m = b.start(b"ftyp");
|
||||
match brand {
|
||||
Brand::Mp4 => {
|
||||
b.bytes(b"isom");
|
||||
b.u32(0x200);
|
||||
for c in [b"isom", b"iso2", b"avc1", b"mp41", b"mp42"] {
|
||||
b.bytes(c);
|
||||
}
|
||||
}
|
||||
Brand::Mov => {
|
||||
b.bytes(b"qt ");
|
||||
b.u32(0x200);
|
||||
b.bytes(b"qt ");
|
||||
}
|
||||
}
|
||||
b.end(m);
|
||||
}
|
||||
|
||||
fn movie_duration(tracks: &[WTrack], movie_ts: u32) -> u64 {
|
||||
tracks.iter().map(|t| presentation_duration(t, movie_ts)).max().unwrap_or(0)
|
||||
}
|
||||
|
||||
fn effective_edits(t: &WTrack, movie_ts: u32) -> Vec<Edit> {
|
||||
if !t.cfg.edits.is_empty() {
|
||||
return t.cfg.edits.clone();
|
||||
}
|
||||
if let Some(start) = t.cfg.media_start {
|
||||
// presentation runs from `start` to the end of the last sample in composition order (with
|
||||
// B-frame reordering that is past the end of the decode timeline)
|
||||
let rest = t.t.comp_end.max(t.t.media_duration).saturating_sub(start.max(0) as u64);
|
||||
return vec![Edit { segment_duration: rescale(rest, t.cfg.timescale, movie_ts), media_time: start, media_rate: 0x10000 }];
|
||||
}
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn presentation_duration(t: &WTrack, movie_ts: u32) -> u64 {
|
||||
let e = effective_edits(t, movie_ts);
|
||||
if e.is_empty() { rescale(t.t.media_duration, t.cfg.timescale, movie_ts) } else { e.iter().map(|e| e.segment_duration).sum() }
|
||||
}
|
||||
|
||||
fn write_mvhd(b: &mut BoxBuf, timescale: u32, duration: u64, next_id: u32) {
|
||||
let v1 = duration > u32::MAX as u64;
|
||||
let m = b.start_full(b"mvhd", v1 as u8, 0);
|
||||
if v1 {
|
||||
b.u64(0);
|
||||
b.u64(0);
|
||||
b.u32(timescale);
|
||||
b.u64(duration);
|
||||
} else {
|
||||
b.u32(0);
|
||||
b.u32(0);
|
||||
b.u32(timescale);
|
||||
b.u32(duration as u32);
|
||||
}
|
||||
b.u32(0x10000);
|
||||
b.u16(0x100);
|
||||
b.zeros(10);
|
||||
for v in MATRIX {
|
||||
b.i32(v);
|
||||
}
|
||||
b.zeros(24);
|
||||
b.u32(next_id);
|
||||
b.end(m);
|
||||
}
|
||||
|
||||
fn write_trak(b: &mut BoxBuf, t: &WTrack, movie_ts: u32, qt: bool, shift: u64, force_co64: bool) -> Result<()> {
|
||||
let trak = b.start(b"trak");
|
||||
let dur = presentation_duration(t, movie_ts);
|
||||
let v1 = dur > u32::MAX as u64;
|
||||
let m = b.start_full(b"tkhd", v1 as u8, 3);
|
||||
if v1 {
|
||||
b.u64(0);
|
||||
b.u64(0);
|
||||
b.u32(t.id);
|
||||
b.u32(0);
|
||||
b.u64(dur);
|
||||
} else {
|
||||
b.u32(0);
|
||||
b.u32(0);
|
||||
b.u32(t.id);
|
||||
b.u32(0);
|
||||
b.u32(dur as u32);
|
||||
}
|
||||
b.zeros(8);
|
||||
b.i16(0);
|
||||
b.u16(0);
|
||||
b.i16(if t.handler == Handler::Audio { 0x100 } else { 0 });
|
||||
b.u16(0);
|
||||
for v in MATRIX {
|
||||
b.i32(v);
|
||||
}
|
||||
let (w, h) = match &t.cfg.entry.video {
|
||||
Some(v) => {
|
||||
let w = match v.pixel_aspect {
|
||||
Some((hs, vs)) if hs > 0 && vs > 0 => (v.width as u64 * hs as u64 / vs as u64) as u32,
|
||||
_ => v.width as u32,
|
||||
};
|
||||
(w, v.height as u32)
|
||||
}
|
||||
None => (0, 0),
|
||||
};
|
||||
b.u32(w << 16);
|
||||
b.u32(h << 16);
|
||||
b.end(m);
|
||||
let edits = effective_edits(t, movie_ts);
|
||||
if !edits.is_empty() {
|
||||
let e = b.start(b"edts");
|
||||
let v1 = edits.iter().any(|e| e.segment_duration > u32::MAX as u64 || e.media_time > i32::MAX as i64 || e.media_time < i32::MIN as i64);
|
||||
let l = b.start_full(b"elst", v1 as u8, 0);
|
||||
b.u32(edits.len() as u32);
|
||||
for ed in &edits {
|
||||
if v1 {
|
||||
b.u64(ed.segment_duration);
|
||||
b.i64(ed.media_time);
|
||||
} else {
|
||||
b.u32(ed.segment_duration as u32);
|
||||
b.i32(ed.media_time as i32);
|
||||
}
|
||||
b.i32(ed.media_rate);
|
||||
}
|
||||
b.end(l);
|
||||
b.end(e);
|
||||
}
|
||||
if let Some(id) = t.tmcd_ref {
|
||||
let r = b.start(b"tref");
|
||||
let m = b.start(b"tmcd");
|
||||
b.u32(id);
|
||||
b.end(m);
|
||||
b.end(r);
|
||||
}
|
||||
let mdia = b.start(b"mdia");
|
||||
let mv1 = t.t.media_duration > u32::MAX as u64;
|
||||
let m = b.start_full(b"mdhd", mv1 as u8, 0);
|
||||
if mv1 {
|
||||
b.u64(0);
|
||||
b.u64(0);
|
||||
b.u32(t.cfg.timescale);
|
||||
b.u64(t.t.media_duration);
|
||||
} else {
|
||||
b.u32(0);
|
||||
b.u32(0);
|
||||
b.u32(t.cfg.timescale);
|
||||
b.u32(t.t.media_duration as u32);
|
||||
}
|
||||
b.u16(pack_language(&t.cfg.language));
|
||||
b.u16(0);
|
||||
b.end(m);
|
||||
let (htype, default_name) = match t.handler {
|
||||
Handler::Video => (b"vide", "VideoHandler"),
|
||||
Handler::Audio => (b"soun", "SoundHandler"),
|
||||
Handler::Timecode => (b"tmcd", "TimeCodeHandler"),
|
||||
Handler::Other => (b"meta", "MetaHandler"),
|
||||
};
|
||||
let name = t.cfg.handler_name.clone().unwrap_or_else(|| default_name.into());
|
||||
let m = b.start_full(b"hdlr", 0, 0);
|
||||
b.bytes(if qt { b"mhlr" } else { &[0, 0, 0, 0] });
|
||||
b.bytes(htype);
|
||||
b.zeros(12);
|
||||
if qt {
|
||||
let n = name.len().min(255);
|
||||
b.u8(n as u8);
|
||||
b.bytes(&name.as_bytes()[..n]);
|
||||
} else {
|
||||
b.bytes(name.as_bytes());
|
||||
b.u8(0);
|
||||
}
|
||||
b.end(m);
|
||||
let minf = b.start(b"minf");
|
||||
match t.handler {
|
||||
Handler::Video => {
|
||||
let m = b.start_full(b"vmhd", 0, 1);
|
||||
b.zeros(8);
|
||||
b.end(m);
|
||||
}
|
||||
Handler::Audio => {
|
||||
let m = b.start_full(b"smhd", 0, 0);
|
||||
b.zeros(4);
|
||||
b.end(m);
|
||||
}
|
||||
Handler::Timecode => {
|
||||
let g = b.start(b"gmhd");
|
||||
let m = b.start_full(b"gmin", 0, 0);
|
||||
b.u16(0x40);
|
||||
b.u16(0x8000);
|
||||
b.u16(0x8000);
|
||||
b.u16(0x8000);
|
||||
b.i16(0);
|
||||
b.u16(0);
|
||||
b.end(m);
|
||||
let tm = b.start(b"tmcd");
|
||||
let m = b.start_full(b"tcmi", 0, 0);
|
||||
b.u16(0);
|
||||
b.u16(0);
|
||||
b.u16(12);
|
||||
b.u16(0);
|
||||
b.bytes(&[0xFF; 6]);
|
||||
b.zeros(6);
|
||||
b.u8(0);
|
||||
b.end(m);
|
||||
b.end(tm);
|
||||
b.end(g);
|
||||
}
|
||||
Handler::Other => {
|
||||
let m = b.start_full(b"nmhd", 0, 0);
|
||||
b.end(m);
|
||||
}
|
||||
}
|
||||
if qt {
|
||||
// QuickTime data handler reference.
|
||||
let m = b.start_full(b"hdlr", 0, 0);
|
||||
b.bytes(b"dhlr");
|
||||
b.bytes(b"url ");
|
||||
b.zeros(12);
|
||||
b.u8(0);
|
||||
b.end(m);
|
||||
}
|
||||
let d = b.start(b"dinf");
|
||||
let r = b.start_full(b"dref", 0, 0);
|
||||
b.u32(1);
|
||||
let u = b.start_full(b"url ", 0, 1);
|
||||
b.end(u);
|
||||
b.end(r);
|
||||
b.end(d);
|
||||
let stbl = b.start(b"stbl");
|
||||
let m = b.start_full(b"stsd", 0, 0);
|
||||
b.u32(1);
|
||||
t.cfg.entry.write(b, qt)?;
|
||||
b.end(m);
|
||||
let tb = &t.t;
|
||||
let m = b.start_full(b"stts", 0, 0);
|
||||
b.u32(tb.stts.len() as u32);
|
||||
for &(c, d) in &tb.stts {
|
||||
b.u32(c);
|
||||
b.u32(d);
|
||||
}
|
||||
b.end(m);
|
||||
if tb.ctts.iter().any(|&(_, o)| o != 0) {
|
||||
let neg = tb.ctts.iter().any(|&(_, o)| o < 0);
|
||||
let m = b.start_full(b"ctts", neg as u8, 0);
|
||||
b.u32(tb.ctts.len() as u32);
|
||||
for &(c, o) in &tb.ctts {
|
||||
b.u32(c);
|
||||
b.i32(o);
|
||||
}
|
||||
b.end(m);
|
||||
}
|
||||
if !tb.all_sync {
|
||||
let m = b.start_full(b"stss", 0, 0);
|
||||
b.u32(tb.sync.len() as u32);
|
||||
for &s in &tb.sync {
|
||||
b.u32(s);
|
||||
}
|
||||
b.end(m);
|
||||
}
|
||||
let m = b.start_full(b"stsc", 0, 0);
|
||||
let mut runs: Vec<(u32, u32)> = Vec::new();
|
||||
for (i, &(_, n)) in tb.chunks.iter().enumerate() {
|
||||
if runs.last().is_none_or(|r| r.1 != n) {
|
||||
runs.push((i as u32 + 1, n));
|
||||
}
|
||||
}
|
||||
b.u32(runs.len() as u32);
|
||||
for (first, n) in runs {
|
||||
b.u32(first);
|
||||
b.u32(n);
|
||||
b.u32(1);
|
||||
}
|
||||
b.end(m);
|
||||
let m = b.start_full(b"stsz", 0, 0);
|
||||
match tb.const_size {
|
||||
Some(s) => {
|
||||
b.u32(s);
|
||||
b.u32(tb.sample_count.min(u32::MAX as u64) as u32);
|
||||
}
|
||||
None => {
|
||||
let first = tb.sizes.first().copied().unwrap_or(0);
|
||||
// A uniform size of 0 would mean "table follows" (ISO/IEC 14496-12 §8.7.3), so
|
||||
// all-empty samples must still be written as a table.
|
||||
if first != 0 && !tb.sizes.is_empty() && tb.sizes.iter().all(|&s| s == first) {
|
||||
b.u32(first);
|
||||
b.u32(tb.sizes.len() as u32);
|
||||
} else {
|
||||
b.u32(0);
|
||||
b.u32(tb.sizes.len() as u32);
|
||||
for &s in &tb.sizes {
|
||||
b.u32(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
b.end(m);
|
||||
let co64 = force_co64 || tb.chunks.iter().any(|&(o, _)| o + shift > u32::MAX as u64);
|
||||
let m = b.start_full(if co64 { b"co64" } else { b"stco" }, 0, 0);
|
||||
b.u32(tb.chunks.len() as u32);
|
||||
for &(o, _) in &tb.chunks {
|
||||
if co64 {
|
||||
b.u64(o + shift);
|
||||
} else {
|
||||
b.u32((o + shift) as u32);
|
||||
}
|
||||
}
|
||||
b.end(m);
|
||||
b.end(stbl);
|
||||
b.end(minf);
|
||||
b.end(mdia);
|
||||
b.end(trak);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_udta(b: &mut BoxBuf, md: &[(String, String)]) {
|
||||
let items: Vec<([u8; 4], &str)> = md
|
||||
.iter()
|
||||
.filter_map(|(k, v)| {
|
||||
let mut chars = k.chars();
|
||||
if chars.next()? != '©' {
|
||||
return None;
|
||||
}
|
||||
let rest: Vec<u8> = chars.map(|c| c as u32 as u8).collect();
|
||||
(rest.len() == 3).then(|| ([0xA9, rest[0], rest[1], rest[2]], v.as_str()))
|
||||
})
|
||||
.collect();
|
||||
if items.is_empty() {
|
||||
return;
|
||||
}
|
||||
let u = b.start(b"udta");
|
||||
for (k, v) in items {
|
||||
let m = b.start(&k);
|
||||
b.u16(v.len() as u16);
|
||||
b.u16(0x55C4);
|
||||
b.bytes(v.as_bytes());
|
||||
b.end(m);
|
||||
}
|
||||
b.end(u);
|
||||
}
|
||||
|
||||
fn validate_track(cfg: &TrackConfig) -> Result<Option<u32>> {
|
||||
if cfg.timescale == 0 {
|
||||
return Err(Error::Mux("timescale must be non-zero".into()));
|
||||
}
|
||||
if let CodecConfig::Pcm(p) = &cfg.entry.codec {
|
||||
if p.bits == 0 || p.channels == 0 {
|
||||
return Err(Error::Mux("PCM needs bits and channels".into()));
|
||||
}
|
||||
if (p.sample_rate - cfg.timescale as f64).abs() > 0.5 {
|
||||
return Err(Error::Mux("PCM track timescale must equal the sample rate".into()));
|
||||
}
|
||||
let Some(n) = p.bytes_per_frame() else {
|
||||
return Err(Error::Mux("PCM frame size overflows".into()));
|
||||
};
|
||||
return Ok(Some(n));
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Progressive MP4/MOV writer: `ftyp`, then one `mdat` streamed to `W`, then `moov` at [`finish`](Self::finish)
|
||||
/// (or moved in front of `mdat` by [`finish_faststart`](Self::finish_faststart)).
|
||||
pub struct Mp4Writer<W: Write + Seek> {
|
||||
w: W,
|
||||
opts: WriterOptions,
|
||||
tracks: Vec<WTrack>,
|
||||
/// Offset of the placeholder box (8 bytes) preceding the mdat header.
|
||||
pad_pos: u64,
|
||||
mdat_pos: u64,
|
||||
pos: u64,
|
||||
last_track: Option<usize>,
|
||||
}
|
||||
|
||||
impl<W: Write + Seek> Mp4Writer<W> {
|
||||
/// Start a file: writes `ftyp`, a placeholder box and the `mdat` header.
|
||||
pub fn new(mut w: W, opts: WriterOptions) -> Result<Self> {
|
||||
let start = w.stream_position()?;
|
||||
let mut b = BoxBuf::new();
|
||||
write_ftyp(&mut b, opts.brand);
|
||||
let pad_pos = start + b.len() as u64;
|
||||
b.leaf(if opts.brand == Brand::Mov { b"wide" } else { b"free" }, &[]);
|
||||
let mdat_pos = start + b.len() as u64;
|
||||
b.leaf(b"mdat", &[]);
|
||||
w.write_all(&b.buf)?;
|
||||
let pos = start + b.len() as u64;
|
||||
Ok(Mp4Writer { w, opts, tracks: Vec::new(), pad_pos, mdat_pos, pos, last_track: None })
|
||||
}
|
||||
|
||||
/// Add a track; returns its index (track ids are index + 1).
|
||||
pub fn add_track(&mut self, cfg: TrackConfig) -> Result<usize> {
|
||||
let pcm = validate_track(&cfg)?;
|
||||
let handler = handler_of(&cfg.entry);
|
||||
let mut t = Tables::new();
|
||||
t.const_size = pcm;
|
||||
self.tracks.push(WTrack { id: self.tracks.len() as u32 + 1, handler, pcm_frame_bytes: pcm, t, cfg, tmcd_ref: None, tmcd_source: None });
|
||||
Ok(self.tracks.len() - 1)
|
||||
}
|
||||
|
||||
/// Add a QuickTime timecode track referenced by `video_track`. Its single sample (the start
|
||||
/// frame number) is written at finish, spanning the video track's duration.
|
||||
pub fn add_timecode_track(&mut self, video_track: usize, tc: TimecodeConfig, start_frame: u32) -> Result<usize> {
|
||||
if video_track >= self.tracks.len() {
|
||||
return Err(Error::NoSuchTrack(video_track));
|
||||
}
|
||||
if tc.timescale == 0 || tc.frame_duration == 0 || tc.frames_per_second == 0 {
|
||||
return Err(Error::Mux("timecode needs timescale, frame duration and fps".into()));
|
||||
}
|
||||
let entry = SampleEntry {
|
||||
format: crate::bytes::FourCc(*b"tmcd"),
|
||||
data_reference_index: 1,
|
||||
codec: CodecConfig::Timecode(tc.clone()),
|
||||
video: None,
|
||||
audio: None,
|
||||
bitrate: None,
|
||||
};
|
||||
let idx = self.add_track(TrackConfig::new(entry, tc.timescale))?;
|
||||
self.tracks[idx].tmcd_source = Some((video_track, start_frame));
|
||||
let id = self.tracks[idx].id;
|
||||
self.tracks[video_track].tmcd_ref = Some(id);
|
||||
Ok(idx)
|
||||
}
|
||||
|
||||
/// Append one sample to track `track` (decode order). For PCM tracks `data` may hold any whole
|
||||
/// number of audio frames.
|
||||
pub fn write_sample(&mut self, track: usize, s: WriteSample) -> Result<()> {
|
||||
let tr = self.tracks.get(track).ok_or(Error::NoSuchTrack(track))?;
|
||||
if tr.tmcd_source.is_some() {
|
||||
return Err(Error::Mux("timecode track samples are written automatically".into()));
|
||||
}
|
||||
let (count, dur) = match tr.pcm_frame_bytes {
|
||||
Some(fb) => {
|
||||
if !s.data.len().is_multiple_of(fb as usize) {
|
||||
return Err(Error::Mux("PCM data is not a whole number of frames".into()));
|
||||
}
|
||||
((s.data.len() / fb as usize) as u32, 1u32)
|
||||
}
|
||||
None => {
|
||||
if s.data.len() > u32::MAX as usize {
|
||||
return Err(Error::Mux("sample larger than 4 GiB".into()));
|
||||
}
|
||||
(1, s.duration)
|
||||
}
|
||||
};
|
||||
if count == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
self.w.write_all(s.data)?;
|
||||
let offset = self.pos;
|
||||
self.pos += s.data.len() as u64;
|
||||
let contiguous = self.last_track == Some(track);
|
||||
self.last_track = Some(track);
|
||||
let tr = &mut self.tracks[track];
|
||||
let t = &mut tr.t;
|
||||
Tables::push_run(&mut t.stts, count, dur);
|
||||
Tables::push_run(&mut t.ctts, count, if tr.pcm_frame_bytes.is_some() { 0 } else { s.composition_offset });
|
||||
if tr.pcm_frame_bytes.is_none() {
|
||||
t.sizes.push(s.data.len() as u32);
|
||||
if s.is_sync {
|
||||
t.sync.push(t.sample_count as u32 + 1);
|
||||
} else {
|
||||
t.all_sync = false;
|
||||
}
|
||||
}
|
||||
t.sample_count += count as u64;
|
||||
let ct = if tr.pcm_frame_bytes.is_some() { 0 } else { s.composition_offset as i64 };
|
||||
t.comp_end = t.comp_end.max((t.media_duration as i64 + ct + count as i64 * dur as i64).max(0) as u64);
|
||||
t.media_duration += count as u64 * dur as u64;
|
||||
match t.chunks.last_mut() {
|
||||
Some(c) if contiguous => c.1 += count,
|
||||
_ => t.chunks.push((offset, count)),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn finish_mdat(&mut self) -> Result<()> {
|
||||
// Timecode samples.
|
||||
for i in 0..self.tracks.len() {
|
||||
let Some((vi, start)) = self.tracks[i].tmcd_source else { continue };
|
||||
let v = &self.tracks[vi];
|
||||
let vd = v.t.media_duration;
|
||||
let vts = v.cfg.timescale;
|
||||
let tts = self.tracks[i].cfg.timescale;
|
||||
let tc_dur = match &self.tracks[i].cfg.entry.codec {
|
||||
CodecConfig::Timecode(tc) => tc.frame_duration,
|
||||
_ => 1,
|
||||
};
|
||||
let d = rescale(vd, vts, tts).max(tc_dur as u64).min(u32::MAX as u64) as u32;
|
||||
self.w.write_all(&start.to_be_bytes())?;
|
||||
let off = self.pos;
|
||||
self.pos += 4;
|
||||
self.last_track = None;
|
||||
let t = &mut self.tracks[i].t;
|
||||
t.stts.push((1, d));
|
||||
t.sizes.push(4);
|
||||
t.sync.push(1);
|
||||
t.sample_count = 1;
|
||||
t.media_duration = d as u64;
|
||||
t.chunks.push((off, 1));
|
||||
}
|
||||
let size = self.pos - self.mdat_pos;
|
||||
if size <= u32::MAX as u64 {
|
||||
self.w.seek(SeekFrom::Start(self.mdat_pos))?;
|
||||
self.w.write_all(&(size as u32).to_be_bytes())?;
|
||||
} else {
|
||||
// Merge the placeholder into a 16-byte large-size mdat header.
|
||||
let mut h = [0u8; 16];
|
||||
h[..4].copy_from_slice(&1u32.to_be_bytes());
|
||||
h[4..8].copy_from_slice(b"mdat");
|
||||
h[8..].copy_from_slice(&(self.pos - self.pad_pos).to_be_bytes());
|
||||
self.w.seek(SeekFrom::Start(self.pad_pos))?;
|
||||
self.w.write_all(&h)?;
|
||||
}
|
||||
self.w.seek(SeekFrom::Start(self.pos))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_moov(&self, shift: u64, force_co64: bool) -> Result<Vec<u8>> {
|
||||
let qt = self.opts.brand == Brand::Mov;
|
||||
let ts = self.opts.movie_timescale.max(1);
|
||||
let mut b = BoxBuf::new();
|
||||
let m = b.start(b"moov");
|
||||
write_mvhd(&mut b, ts, movie_duration(&self.tracks, ts), self.tracks.len() as u32 + 1);
|
||||
for t in &self.tracks {
|
||||
write_trak(&mut b, t, ts, qt, shift, force_co64)?;
|
||||
}
|
||||
write_udta(&mut b, &self.opts.metadata);
|
||||
b.end(m);
|
||||
Ok(b.buf)
|
||||
}
|
||||
|
||||
/// Finish the file with `moov` after `mdat`. Returns the underlying writer.
|
||||
pub fn finish(mut self) -> Result<W> {
|
||||
self.finish_mdat()?;
|
||||
let moov = self.build_moov(0, false)?;
|
||||
self.w.write_all(&moov)?;
|
||||
self.w.flush()?;
|
||||
Ok(self.w)
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Read + Write + Seek> Mp4Writer<W> {
|
||||
/// Finish with `moov` in front of `mdat` ("fast start"): media data is shifted forward in place.
|
||||
pub fn finish_faststart(mut self) -> Result<W> {
|
||||
self.finish_mdat()?;
|
||||
let mut moov = self.build_moov(0, false)?;
|
||||
let mut force = false;
|
||||
loop {
|
||||
let shift = moov.len() as u64;
|
||||
let next = self.build_moov(shift, force)?;
|
||||
if next.len() == moov.len() {
|
||||
moov = next;
|
||||
break;
|
||||
}
|
||||
// Offsets crossed the 32-bit boundary: switch to co64 (size only grows, so this converges).
|
||||
force = true;
|
||||
moov = next;
|
||||
}
|
||||
let shift = moov.len() as u64;
|
||||
let start = self.pad_pos;
|
||||
let end = self.pos;
|
||||
let mut buf = vec![0u8; 1 << 20];
|
||||
let mut cur = end;
|
||||
while cur > start {
|
||||
let n = (cur - start).min(buf.len() as u64) as usize;
|
||||
cur -= n as u64;
|
||||
self.w.seek(SeekFrom::Start(cur))?;
|
||||
self.w.read_exact(&mut buf[..n])?;
|
||||
self.w.seek(SeekFrom::Start(cur + shift))?;
|
||||
self.w.write_all(&buf[..n])?;
|
||||
}
|
||||
self.w.seek(SeekFrom::Start(start))?;
|
||||
self.w.write_all(&moov)?;
|
||||
self.w.seek(SeekFrom::Start(end + shift))?;
|
||||
self.w.flush()?;
|
||||
Ok(self.w)
|
||||
}
|
||||
}
|
||||
|
||||
/// Buffered fragment sample: (data, duration, composition offset, sync).
|
||||
type PendingSample = (Vec<u8>, u32, i32, bool);
|
||||
|
||||
/// Fragmented MP4 writer: an init segment (`ftyp` + `moov` with `mvex`) followed by
|
||||
/// `moof`+`mdat` fragments. Samples are buffered until [`flush_fragment`](Self::flush_fragment).
|
||||
pub struct FragmentedWriter<W: Write> {
|
||||
w: W,
|
||||
opts: WriterOptions,
|
||||
tracks: Vec<WTrack>,
|
||||
pending: Vec<Vec<PendingSample>>,
|
||||
next_dts: Vec<u64>,
|
||||
seq: u32,
|
||||
init_written: bool,
|
||||
}
|
||||
|
||||
impl<W: Write> FragmentedWriter<W> {
|
||||
pub fn new(w: W, opts: WriterOptions) -> Self {
|
||||
FragmentedWriter { w, opts, tracks: Vec::new(), pending: Vec::new(), next_dts: Vec::new(), seq: 0, init_written: false }
|
||||
}
|
||||
|
||||
/// Add a track (before the first fragment). PCM is not supported in fragments.
|
||||
pub fn add_track(&mut self, cfg: TrackConfig) -> Result<usize> {
|
||||
if self.init_written {
|
||||
return Err(Error::Mux("tracks must be added before the first fragment".into()));
|
||||
}
|
||||
if validate_track(&cfg)?.is_some() {
|
||||
return Err(Error::Mux("PCM is not supported by the fragmented writer".into()));
|
||||
}
|
||||
let handler = handler_of(&cfg.entry);
|
||||
self.tracks.push(WTrack {
|
||||
id: self.tracks.len() as u32 + 1,
|
||||
handler,
|
||||
pcm_frame_bytes: None,
|
||||
t: Tables::new(),
|
||||
cfg,
|
||||
tmcd_ref: None,
|
||||
tmcd_source: None,
|
||||
});
|
||||
self.pending.push(Vec::new());
|
||||
self.next_dts.push(0);
|
||||
Ok(self.tracks.len() - 1)
|
||||
}
|
||||
|
||||
fn write_init(&mut self) -> Result<()> {
|
||||
let qt = self.opts.brand == Brand::Mov;
|
||||
let ts = self.opts.movie_timescale.max(1);
|
||||
let mut b = BoxBuf::new();
|
||||
write_ftyp(&mut b, self.opts.brand);
|
||||
let m = b.start(b"moov");
|
||||
write_mvhd(&mut b, ts, 0, self.tracks.len() as u32 + 1);
|
||||
for t in &self.tracks {
|
||||
write_trak(&mut b, t, ts, qt, 0, false)?;
|
||||
}
|
||||
let mv = b.start(b"mvex");
|
||||
for t in &self.tracks {
|
||||
let x = b.start_full(b"trex", 0, 0);
|
||||
b.u32(t.id);
|
||||
b.u32(1);
|
||||
b.u32(0);
|
||||
b.u32(0);
|
||||
b.u32(0);
|
||||
b.end(x);
|
||||
}
|
||||
b.end(mv);
|
||||
write_udta(&mut b, &self.opts.metadata);
|
||||
b.end(m);
|
||||
self.w.write_all(&b.buf)?;
|
||||
self.init_written = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Buffer a sample for the next fragment.
|
||||
pub fn write_sample(&mut self, track: usize, s: WriteSample) -> Result<()> {
|
||||
let p = self.pending.get_mut(track).ok_or(Error::NoSuchTrack(track))?;
|
||||
if s.data.len() > u32::MAX as usize {
|
||||
return Err(Error::Mux("sample larger than 4 GiB".into()));
|
||||
}
|
||||
p.push((s.data.to_vec(), s.duration, s.composition_offset, s.is_sync));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_moof(&self, data_offsets: &[i32]) -> Vec<u8> {
|
||||
let mut b = BoxBuf::new();
|
||||
let m = b.start(b"moof");
|
||||
let h = b.start_full(b"mfhd", 0, 0);
|
||||
b.u32(self.seq);
|
||||
b.end(h);
|
||||
for (i, t) in self.tracks.iter().enumerate() {
|
||||
let p = &self.pending[i];
|
||||
if p.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let tf = b.start(b"traf");
|
||||
let h = b.start_full(b"tfhd", 0, 0x20000);
|
||||
b.u32(t.id);
|
||||
b.end(h);
|
||||
let h = b.start_full(b"tfdt", 1, 0);
|
||||
b.u64(self.next_dts[i]);
|
||||
b.end(h);
|
||||
let neg = p.iter().any(|s| s.2 < 0);
|
||||
let h = b.start_full(b"trun", neg as u8, 0x1 | 0x100 | 0x200 | 0x400 | 0x800);
|
||||
b.u32(p.len() as u32);
|
||||
b.i32(data_offsets[i]);
|
||||
for (d, dur, cto, sync) in p {
|
||||
b.u32(*dur);
|
||||
b.u32(d.len() as u32);
|
||||
b.u32(if *sync { 0x0200_0000 } else { 0x0101_0000 });
|
||||
b.i32(*cto);
|
||||
}
|
||||
b.end(h);
|
||||
b.end(tf);
|
||||
}
|
||||
b.end(m);
|
||||
b.buf
|
||||
}
|
||||
|
||||
/// Write all buffered samples as one `moof` + `mdat` fragment (no-op if nothing is buffered).
|
||||
pub fn flush_fragment(&mut self) -> Result<()> {
|
||||
if !self.init_written {
|
||||
self.write_init()?;
|
||||
}
|
||||
if self.pending.iter().all(|p| p.is_empty()) {
|
||||
return Ok(());
|
||||
}
|
||||
self.seq += 1;
|
||||
let probe = self.build_moof(&vec![0; self.tracks.len()]);
|
||||
let data_len: u64 = self.pending.iter().flatten().map(|s| s.0.len() as u64).sum();
|
||||
let large = data_len + 8 > u32::MAX as u64;
|
||||
let hdr = if large { 16 } else { 8 };
|
||||
let mut offs = Vec::with_capacity(self.tracks.len());
|
||||
let mut acc = probe.len() as u64 + hdr;
|
||||
for p in &self.pending {
|
||||
offs.push(i32::try_from(acc).map_err(|_| Error::Mux("fragment too large".into()))?);
|
||||
acc += p.iter().map(|s| s.0.len() as u64).sum::<u64>();
|
||||
}
|
||||
let moof = self.build_moof(&offs);
|
||||
self.w.write_all(&moof)?;
|
||||
if large {
|
||||
self.w.write_all(&1u32.to_be_bytes())?;
|
||||
self.w.write_all(b"mdat")?;
|
||||
self.w.write_all(&(data_len + 16).to_be_bytes())?;
|
||||
} else {
|
||||
self.w.write_all(&((data_len + 8) as u32).to_be_bytes())?;
|
||||
self.w.write_all(b"mdat")?;
|
||||
}
|
||||
for (i, p) in self.pending.iter_mut().enumerate() {
|
||||
for (d, dur, _, _) in p.drain(..) {
|
||||
self.w.write_all(&d)?;
|
||||
self.next_dts[i] += dur as u64;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Flush remaining samples and return the writer.
|
||||
pub fn finish(mut self) -> Result<W> {
|
||||
self.flush_fragment()?;
|
||||
self.w.flush()?;
|
||||
Ok(self.w)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::codec::{AvcConfig, PcmConfig};
|
||||
use std::io::Cursor;
|
||||
|
||||
fn avc_entry() -> SampleEntry {
|
||||
SampleEntry::avc(AvcConfig::new(vec![vec![0x67, 0x42, 0, 0x1E]], vec![vec![0x68, 0xCE]], 4), 64, 48)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn language_pack() {
|
||||
assert_eq!(pack_language("und"), 0x55C4);
|
||||
assert_eq!(pack_language("eng"), 0x15C7);
|
||||
assert_eq!(pack_language("EN"), 0x55C4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_roundtrip_and_faststart() {
|
||||
for fast in [false, true] {
|
||||
let mut w = Mp4Writer::new(Cursor::new(Vec::new()), WriterOptions::new(Brand::Mp4)).unwrap();
|
||||
let v = w.add_track(TrackConfig::new(avc_entry(), 12800)).unwrap();
|
||||
let pcm = PcmConfig { bits: 16, float: false, big_endian: false, signed: true, channels: 2, sample_rate: 48000.0 };
|
||||
let a = w.add_track(TrackConfig::new(SampleEntry::pcm(pcm), 48000)).unwrap();
|
||||
for i in 0..10u8 {
|
||||
let data = vec![i; 100 + i as usize];
|
||||
w.write_sample(
|
||||
v,
|
||||
WriteSample { data: &data, duration: 512, composition_offset: if i % 2 == 0 { 1024 } else { 0 }, is_sync: i % 5 == 0 },
|
||||
)
|
||||
.unwrap();
|
||||
w.write_sample(a, WriteSample { data: &[i; 400], duration: 0, composition_offset: 0, is_sync: true }).unwrap();
|
||||
}
|
||||
let out = if fast { w.finish_faststart() } else { w.finish() }.unwrap().into_inner();
|
||||
let f = crate::open(out.as_slice()).unwrap();
|
||||
assert_eq!(f.tracks.len(), 2);
|
||||
let t = &f.tracks[0];
|
||||
assert_eq!(t.samples.len(), 10);
|
||||
for (i, s) in t.samples.iter().enumerate() {
|
||||
assert_eq!(s.size as usize, 100 + i);
|
||||
assert_eq!(s.dts, 512 * i as i64);
|
||||
assert_eq!(s.is_sync, i % 5 == 0);
|
||||
assert_eq!(f.read_sample(out.as_slice(), 0, i).unwrap(), vec![i as u8; 100 + i]);
|
||||
}
|
||||
let at = &f.tracks[1];
|
||||
assert!(at.pcm_chunked);
|
||||
assert_eq!(at.samples.len(), 10);
|
||||
assert_eq!(at.samples[3].size, 400);
|
||||
assert_eq!(at.samples[3].duration, 100);
|
||||
assert_eq!(f.read_sample(out.as_slice(), 1, 3).unwrap(), vec![3u8; 400]);
|
||||
if fast {
|
||||
assert!(f.moov_offset < t.samples[0].offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fragmented_roundtrip() {
|
||||
let mut w = FragmentedWriter::new(Vec::new(), WriterOptions::new(Brand::Mp4));
|
||||
let v = w.add_track(TrackConfig::new(avc_entry(), 1000)).unwrap();
|
||||
for i in 0..6u8 {
|
||||
w.write_sample(v, WriteSample { data: &[i; 10], duration: 40, composition_offset: -(i as i32 % 2), is_sync: i % 3 == 0 }).unwrap();
|
||||
if i % 3 == 2 {
|
||||
w.flush_fragment().unwrap();
|
||||
}
|
||||
}
|
||||
let out = w.finish().unwrap();
|
||||
let f = crate::open(out.as_slice()).unwrap();
|
||||
assert!(f.fragmented);
|
||||
assert_eq!(f.fragment_count, 2);
|
||||
let t = &f.tracks[0];
|
||||
assert_eq!(t.samples.len(), 6);
|
||||
for (i, s) in t.samples.iter().enumerate() {
|
||||
assert_eq!(s.dts, 40 * i as i64);
|
||||
assert_eq!(s.pts, s.dts - (i as i64 % 2));
|
||||
assert_eq!(s.is_sync, i % 3 == 0);
|
||||
assert_eq!(f.read_sample(out.as_slice(), 0, i).unwrap(), vec![i as u8; 10]);
|
||||
}
|
||||
assert_eq!(t.sync_sample_before(5), 3);
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
//! Random-access byte sources for the demuxer.
|
||||
|
||||
use std::io;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Random-access, read-only byte source (file, in-memory buffer, web Blob…).
|
||||
///
|
||||
/// Implementations must be able to serve reads at arbitrary offsets through `&self`.
|
||||
pub trait ByteSource {
|
||||
/// Total length in bytes.
|
||||
fn len(&self) -> u64;
|
||||
/// Fill `buf` entirely with bytes starting at `offset`; fail with `UnexpectedEof` if past the end.
|
||||
fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()>;
|
||||
/// True if the source holds no bytes.
|
||||
fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
}
|
||||
|
||||
fn eof() -> io::Error {
|
||||
io::Error::new(io::ErrorKind::UnexpectedEof, "read past end of byte source")
|
||||
}
|
||||
|
||||
impl ByteSource for [u8] {
|
||||
fn len(&self) -> u64 {
|
||||
<[u8]>::len(self) as u64
|
||||
}
|
||||
fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()> {
|
||||
let start = usize::try_from(offset).map_err(|_| eof())?;
|
||||
let end = start.checked_add(buf.len()).ok_or_else(eof)?;
|
||||
let src = self.get(start..end).ok_or_else(eof)?;
|
||||
buf.copy_from_slice(src);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ByteSource for Vec<u8> {
|
||||
fn len(&self) -> u64 {
|
||||
self.as_slice().len() as u64
|
||||
}
|
||||
fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()> {
|
||||
self.as_slice().read_at(offset, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ByteSource + ?Sized> ByteSource for &T {
|
||||
fn len(&self) -> u64 {
|
||||
(**self).len()
|
||||
}
|
||||
fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()> {
|
||||
(**self).read_at(offset, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ByteSource + ?Sized> ByteSource for Arc<T> {
|
||||
fn len(&self) -> u64 {
|
||||
(**self).len()
|
||||
}
|
||||
fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()> {
|
||||
(**self).read_at(offset, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ByteSource + ?Sized> ByteSource for Box<T> {
|
||||
fn len(&self) -> u64 {
|
||||
(**self).len()
|
||||
}
|
||||
fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()> {
|
||||
(**self).read_at(offset, buf)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl ByteSource for std::fs::File {
|
||||
fn len(&self) -> u64 {
|
||||
self.metadata().map(|m| m.len()).unwrap_or(0)
|
||||
}
|
||||
#[cfg(unix)]
|
||||
fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()> {
|
||||
std::os::unix::fs::FileExt::read_exact_at(self, buf, offset)
|
||||
}
|
||||
#[cfg(windows)]
|
||||
fn read_at(&self, mut offset: u64, mut buf: &mut [u8]) -> io::Result<()> {
|
||||
use std::os::windows::fs::FileExt;
|
||||
while !buf.is_empty() {
|
||||
let n = self.seek_read(buf, offset)?;
|
||||
if n == 0 {
|
||||
return Err(eof());
|
||||
}
|
||||
offset += n as u64;
|
||||
buf = &mut buf[n..];
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(not(any(unix, windows)))]
|
||||
fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()> {
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
let mut f = self;
|
||||
f.seek(SeekFrom::Start(offset))?;
|
||||
f.read_exact(buf)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn slice_reads() {
|
||||
let v = vec![1u8, 2, 3, 4];
|
||||
let mut b = [0u8; 2];
|
||||
v.read_at(1, &mut b).unwrap();
|
||||
assert_eq!(b, [2, 3]);
|
||||
assert!(v.read_at(3, &mut b).is_err());
|
||||
assert!(v.read_at(u64::MAX, &mut b).is_err());
|
||||
let a: Arc<[u8]> = Arc::from(v.clone());
|
||||
a.read_at(2, &mut b).unwrap();
|
||||
assert_eq!(b, [3, 4]);
|
||||
assert_eq!(ByteSource::len(&a), 4);
|
||||
let s: &[u8] = &v;
|
||||
assert_eq!(ByteSource::len(&s), 4);
|
||||
}
|
||||
}
|
||||
@@ -10,3 +10,4 @@ rust-version.workspace = true
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
craft-media = { workspace = true, features = ["matroska"] }
|
||||
|
||||
@@ -1,225 +0,0 @@
|
||||
//! Matroska CodecID → codec mapping (Matroska codec specifications, RFC 9559 §5.1.4.1.21 and the
|
||||
//! codec mappings registry).
|
||||
|
||||
/// Codec of a track, derived from `CodecID` and `CodecPrivate`.
|
||||
///
|
||||
/// Configuration records are passed through as the raw bytes found in `CodecPrivate`, which for
|
||||
/// the ISO codecs are exactly the ISOBMFF configuration boxes' payloads (avcC, hvcC, av1C, ASC…).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Codec {
|
||||
/// `V_MPEG4/ISO/AVC`; payload is an `AVCDecoderConfigurationRecord` (avcC).
|
||||
Avc { avcc: Vec<u8> },
|
||||
/// `V_MPEGH/ISO/HEVC`; payload is an `HEVCDecoderConfigurationRecord` (hvcC).
|
||||
Hevc { hvcc: Vec<u8> },
|
||||
/// `V_VP8`.
|
||||
Vp8,
|
||||
/// `V_VP9`; `CodecPrivate` (optional) holds VP9 codec feature metadata.
|
||||
Vp9 { private: Vec<u8> },
|
||||
/// `V_AV1`; payload is an `AV1CodecConfigurationRecord` (av1C).
|
||||
Av1 { av1c: Vec<u8> },
|
||||
/// `V_PRORES`; `fourcc` from `CodecPrivate` (e.g. `apcn`), if present.
|
||||
ProRes { fourcc: Option<[u8; 4]> },
|
||||
/// `V_MJPEG`, or `V_MS/VFW/FOURCC` with an `MJPG` compression FourCC.
|
||||
Mjpeg,
|
||||
/// `V_MS/VFW/FOURCC` with any other FourCC; `bitmap_info_header` is the whole `CodecPrivate`.
|
||||
VfwFourcc { fourcc: [u8; 4], bitmap_info_header: Vec<u8> },
|
||||
/// `A_AAC` (and the legacy `A_AAC/MPEG{2,4}/<profile>` IDs); `asc` is the
|
||||
/// `AudioSpecificConfig` (synthesised for legacy IDs without `CodecPrivate`).
|
||||
Aac { asc: Vec<u8> },
|
||||
/// `A_OPUS`; `head` is the `OpusHead` identification header.
|
||||
Opus { head: Vec<u8> },
|
||||
/// `A_VORBIS`; the three Vorbis headers (identification, comment, setup), split from the
|
||||
/// Xiph-laced `CodecPrivate`.
|
||||
Vorbis { headers: Vec<Vec<u8>> },
|
||||
/// `A_FLAC`; `private` is `fLaC` + metadata blocks (STREAMINFO first).
|
||||
Flac { private: Vec<u8> },
|
||||
/// `A_PCM/INT/LIT`, `A_PCM/INT/BIG`, `A_PCM/FLOAT/IEEE`. Bit depth comes from the Audio element.
|
||||
Pcm { float: bool, big_endian: bool, bits: u16 },
|
||||
/// `A_AC3`.
|
||||
Ac3,
|
||||
/// `A_EAC3`.
|
||||
Eac3,
|
||||
/// `A_MPEG/L3`.
|
||||
Mp3,
|
||||
/// `A_MPEG/L2`.
|
||||
Mp2,
|
||||
/// `S_TEXT/UTF8` (SubRip-style plain text).
|
||||
SubRip,
|
||||
/// `S_TEXT/WEBVTT`, or WebM's `D_WEBVTT/{SUBTITLES,CAPTIONS,DESCRIPTIONS,METADATA}`.
|
||||
WebVtt,
|
||||
/// `S_TEXT/ASS` / `S_TEXT/SSA`; `header` is the script header from `CodecPrivate`.
|
||||
Ass { header: Vec<u8>, ssa: bool },
|
||||
/// Any other CodecID (kept verbatim along with `Track::codec_private`).
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl Codec {
|
||||
/// Short codec name, matching FFmpeg's `codec_name` where one exists (useful for logs/UI).
|
||||
pub fn name(&self) -> String {
|
||||
match self {
|
||||
Codec::Avc { .. } => "h264".into(),
|
||||
Codec::Hevc { .. } => "hevc".into(),
|
||||
Codec::Vp8 => "vp8".into(),
|
||||
Codec::Vp9 { .. } => "vp9".into(),
|
||||
Codec::Av1 { .. } => "av1".into(),
|
||||
Codec::ProRes { .. } => "prores".into(),
|
||||
Codec::Mjpeg => "mjpeg".into(),
|
||||
Codec::VfwFourcc { fourcc, .. } => String::from_utf8_lossy(fourcc).trim().to_ascii_lowercase(),
|
||||
Codec::Aac { .. } => "aac".into(),
|
||||
Codec::Opus { .. } => "opus".into(),
|
||||
Codec::Vorbis { .. } => "vorbis".into(),
|
||||
Codec::Flac { .. } => "flac".into(),
|
||||
Codec::Pcm { float, big_endian, bits } => {
|
||||
let k = if *float {
|
||||
'f'
|
||||
} else if *bits == 8 {
|
||||
'u'
|
||||
} else {
|
||||
's'
|
||||
};
|
||||
if *bits == 8 { format!("pcm_{k}8") } else { format!("pcm_{k}{bits}{}", if *big_endian { "be" } else { "le" }) }
|
||||
}
|
||||
Codec::Ac3 => "ac3".into(),
|
||||
Codec::Eac3 => "eac3".into(),
|
||||
Codec::Mp3 => "mp3".into(),
|
||||
Codec::Mp2 => "mp2".into(),
|
||||
Codec::SubRip => "subrip".into(),
|
||||
Codec::WebVtt => "webvtt".into(),
|
||||
Codec::Ass { ssa, .. } => if *ssa { "ssa" } else { "ass" }.into(),
|
||||
Codec::Other(id) => id.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const AAC_RATES: [u32; 13] = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350];
|
||||
|
||||
/// Build a 2-byte (or 5-byte with SBR extension) AudioSpecificConfig for legacy `A_AAC/...` IDs.
|
||||
fn synth_asc(object_type: u8, rate: f64, out_rate: Option<f64>, channels: u64, sbr: bool) -> Vec<u8> {
|
||||
let rate_index = |r: f64| AAC_RATES.iter().position(|&x| x as f64 == r).unwrap_or(4) as u8;
|
||||
let ri = rate_index(rate);
|
||||
let ch = channels.min(7) as u8;
|
||||
let mut v = vec![(object_type << 3) | (ri >> 1), ((ri & 1) << 7) | (ch << 3)];
|
||||
if sbr {
|
||||
// backward-compatible explicit signalling: sync 0x2b7, SBR object type 5, flag, ext rate index
|
||||
let ext = rate_index(out_rate.unwrap_or(rate * 2.0));
|
||||
v.push(0x56);
|
||||
v.push(0xE5);
|
||||
v.push(0x80 | (ext << 3));
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
/// Split a Xiph-laced `CodecPrivate` (Vorbis/Theora headers).
|
||||
fn split_xiph(p: &[u8]) -> Vec<Vec<u8>> {
|
||||
let Some(&n) = p.first() else { return Vec::new() };
|
||||
let mut pos = 1;
|
||||
let mut sizes = Vec::new();
|
||||
for _ in 0..n {
|
||||
let mut s = 0usize;
|
||||
loop {
|
||||
let Some(&b) = p.get(pos) else { return Vec::new() };
|
||||
pos += 1;
|
||||
s += b as usize;
|
||||
if b != 255 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
sizes.push(s);
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
for s in sizes {
|
||||
let Some(h) = p.get(pos..pos + s) else { return Vec::new() };
|
||||
out.push(h.to_vec());
|
||||
pos += s;
|
||||
}
|
||||
out.push(p[pos.min(p.len())..].to_vec());
|
||||
out
|
||||
}
|
||||
|
||||
/// Map a CodecID + CodecPrivate (+ audio params for legacy AAC / PCM) to a [`Codec`].
|
||||
pub(crate) fn map_codec(id: &str, private: &[u8], audio: Option<&crate::AudioInfo>) -> Codec {
|
||||
let p = private.to_vec();
|
||||
match id {
|
||||
"V_MPEG4/ISO/AVC" => Codec::Avc { avcc: p },
|
||||
"V_MPEGH/ISO/HEVC" => Codec::Hevc { hvcc: p },
|
||||
"V_VP8" => Codec::Vp8,
|
||||
"V_VP9" => Codec::Vp9 { private: p },
|
||||
"V_AV1" => Codec::Av1 { av1c: p },
|
||||
"V_PRORES" => Codec::ProRes { fourcc: private.get(..4).and_then(|f| f.try_into().ok()) },
|
||||
"V_MJPEG" => Codec::Mjpeg,
|
||||
"V_MS/VFW/FOURCC" => {
|
||||
let fourcc: [u8; 4] = private.get(16..20).and_then(|f| f.try_into().ok()).unwrap_or(*b" ");
|
||||
match &fourcc {
|
||||
b"MJPG" | b"mjpg" | b"AVRn" | b"AVDJ" => Codec::Mjpeg,
|
||||
b"H264" | b"h264" | b"avc1" | b"X264" | b"x264" => Codec::Avc { avcc: Vec::new() },
|
||||
b"apch" | b"apcn" | b"apcs" | b"apco" | b"ap4h" | b"ap4x" => Codec::ProRes { fourcc: Some(fourcc) },
|
||||
_ => Codec::VfwFourcc { fourcc, bitmap_info_header: p },
|
||||
}
|
||||
}
|
||||
"A_AAC" => Codec::Aac { asc: p },
|
||||
"A_OPUS" => Codec::Opus { head: p },
|
||||
"A_VORBIS" => Codec::Vorbis { headers: split_xiph(private) },
|
||||
"A_FLAC" => Codec::Flac { private: p },
|
||||
"A_PCM/INT/LIT" | "A_PCM/INT/BIG" | "A_PCM/FLOAT/IEEE" => {
|
||||
let bits = audio.and_then(|a| a.bit_depth).unwrap_or(if id == "A_PCM/FLOAT/IEEE" { 32 } else { 16 }) as u16;
|
||||
Codec::Pcm { float: id == "A_PCM/FLOAT/IEEE", big_endian: id == "A_PCM/INT/BIG", bits }
|
||||
}
|
||||
"A_AC3" | "A_AC3/BSID9" | "A_AC3/BSID10" => Codec::Ac3,
|
||||
"A_EAC3" => Codec::Eac3,
|
||||
"A_MPEG/L3" => Codec::Mp3,
|
||||
"A_MPEG/L2" => Codec::Mp2,
|
||||
"S_TEXT/UTF8" => Codec::SubRip,
|
||||
"S_TEXT/WEBVTT" | "D_WEBVTT/SUBTITLES" | "D_WEBVTT/CAPTIONS" | "D_WEBVTT/DESCRIPTIONS" | "D_WEBVTT/METADATA" => Codec::WebVtt,
|
||||
"S_TEXT/ASS" | "S_ASS" => Codec::Ass { header: p, ssa: false },
|
||||
"S_TEXT/SSA" | "S_SSA" => Codec::Ass { header: p, ssa: true },
|
||||
_ if id.starts_with("A_AAC/") => {
|
||||
if !private.is_empty() {
|
||||
return Codec::Aac { asc: p };
|
||||
}
|
||||
let (rate, out_rate, ch) = audio.map_or((48000.0, None, 2), |a| (a.sampling_frequency, a.output_sampling_frequency, a.channels));
|
||||
let object_type = if id.ends_with("/MAIN") {
|
||||
1
|
||||
} else if id.ends_with("/SSR") {
|
||||
3
|
||||
} else if id.ends_with("/LTP") {
|
||||
4
|
||||
} else {
|
||||
2
|
||||
};
|
||||
let sbr = id.ends_with("/SBR");
|
||||
Codec::Aac { asc: synth_asc(object_type, rate, out_rate, ch, sbr) }
|
||||
}
|
||||
_ => Codec::Other(id.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn mapping() {
|
||||
assert_eq!(map_codec("V_MPEG4/ISO/AVC", &[1, 2], None), Codec::Avc { avcc: vec![1, 2] });
|
||||
assert_eq!(map_codec("V_PRORES", b"apcn", None), Codec::ProRes { fourcc: Some(*b"apcn") });
|
||||
let mut bih = vec![0u8; 40];
|
||||
bih[16..20].copy_from_slice(b"MJPG");
|
||||
assert_eq!(map_codec("V_MS/VFW/FOURCC", &bih, None), Codec::Mjpeg);
|
||||
bih[16..20].copy_from_slice(b"FFV1");
|
||||
assert!(matches!(map_codec("V_MS/VFW/FOURCC", &bih, None), Codec::VfwFourcc { fourcc, .. } if &fourcc == b"FFV1"));
|
||||
let a = crate::AudioInfo { sampling_frequency: 44100.0, channels: 2, bit_depth: Some(24), output_sampling_frequency: None };
|
||||
assert_eq!(map_codec("A_PCM/INT/LIT", &[], Some(&a)).name(), "pcm_s24le");
|
||||
assert_eq!(map_codec("A_PCM/FLOAT/IEEE", &[], Some(&a)), Codec::Pcm { float: true, big_endian: false, bits: 24 });
|
||||
// legacy AAC LC 44.1k stereo → 0x12 0x10
|
||||
assert_eq!(map_codec("A_AAC/MPEG4/LC", &[], Some(&a)), Codec::Aac { asc: vec![0x12, 0x10] });
|
||||
assert_eq!(map_codec("S_TEXT/UTF8", &[], None).name(), "subrip");
|
||||
assert_eq!(map_codec("X_WHATEVER", &[], None), Codec::Other("X_WHATEVER".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xiph_split() {
|
||||
let mut p = vec![2u8, 3, 2];
|
||||
p.extend_from_slice(&[1, 1, 1, 2, 2, 3, 3, 3, 3]);
|
||||
let h = split_xiph(&p);
|
||||
assert_eq!(h, vec![vec![1, 1, 1], vec![2, 2], vec![3, 3, 3, 3]]);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,476 +0,0 @@
|
||||
//! EBML primitives (RFC 8794): variable-length integers, element headers, typed values, CRC-32,
|
||||
//! and Matroska block lacing (RFC 9559 §10.3).
|
||||
|
||||
use crate::error::{Result, invalid};
|
||||
|
||||
/// Length in bytes of a VINT whose first byte is `b` (1..=8), or `None` for a zero first byte.
|
||||
#[inline]
|
||||
pub fn vint_len(b: u8) -> Option<usize> {
|
||||
if b == 0 { None } else { Some(b.leading_zeros() as usize + 1) }
|
||||
}
|
||||
|
||||
/// Element data size: known, or "unknown" (all value bits set).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Size {
|
||||
Known(u64),
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl Size {
|
||||
pub fn known(self) -> Option<u64> {
|
||||
match self {
|
||||
Size::Known(n) => Some(n),
|
||||
Size::Unknown => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode an unsigned VINT (marker bit stripped). Returns `(value, length, all_ones)`.
|
||||
pub fn read_vint(data: &[u8]) -> Option<(u64, usize, bool)> {
|
||||
let first = *data.first()?;
|
||||
let len = vint_len(first)?;
|
||||
if data.len() < len {
|
||||
return None;
|
||||
}
|
||||
let mask = if len == 8 { 0 } else { 0xFFu8 >> len };
|
||||
let mut v = (first & mask) as u64;
|
||||
for &b in &data[1..len] {
|
||||
v = (v << 8) | b as u64;
|
||||
}
|
||||
let all_ones = v == (1u64 << (7 * len)) - 1;
|
||||
Some((v, len, all_ones))
|
||||
}
|
||||
|
||||
/// Decode an element data size. All-ones is the reserved "unknown size".
|
||||
pub fn read_size(data: &[u8]) -> Option<(Size, usize)> {
|
||||
let (v, len, ones) = read_vint(data)?;
|
||||
Some((if ones { Size::Unknown } else { Size::Known(v) }, len))
|
||||
}
|
||||
|
||||
/// Decode an element ID (marker bits kept, 1..=4 bytes). Rejects all-ones value bits (reserved).
|
||||
/// All-zero value bits are accepted, because Matroska's `ChapterDisplay` uses ID `0x80`.
|
||||
pub fn read_id(data: &[u8]) -> Option<(u32, usize)> {
|
||||
let first = *data.first()?;
|
||||
let len = vint_len(first)?;
|
||||
if len > 4 || data.len() < len {
|
||||
return None;
|
||||
}
|
||||
let (v, _, ones) = read_vint(&data[..len])?;
|
||||
if ones || (v == 0 && len > 1) {
|
||||
return None;
|
||||
}
|
||||
let mut id = 0u32;
|
||||
for &b in &data[..len] {
|
||||
id = (id << 8) | b as u32;
|
||||
}
|
||||
Some((id, len))
|
||||
}
|
||||
|
||||
/// Decode a signed VINT as used by EBML lacing (value − (2^(7·len−1) − 1)).
|
||||
pub fn read_svint(data: &[u8]) -> Option<(i64, usize)> {
|
||||
let (v, len, _) = read_vint(data)?;
|
||||
let bias = (1i64 << (7 * len - 1)) - 1;
|
||||
Some((v as i64 - bias, len))
|
||||
}
|
||||
|
||||
/// Encode an element ID (already carrying its marker bits).
|
||||
pub fn write_id(out: &mut Vec<u8>, id: u32) {
|
||||
let n = if id >= 0x100_0000 {
|
||||
4
|
||||
} else if id >= 0x1_0000 {
|
||||
3
|
||||
} else if id >= 0x100 {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
};
|
||||
out.extend_from_slice(&id.to_be_bytes()[4 - n..]);
|
||||
}
|
||||
|
||||
/// Encode a data size as a VINT using the shortest length (or `min_len` if larger).
|
||||
pub fn write_size(out: &mut Vec<u8>, size: u64, min_len: usize) {
|
||||
let mut len = 1;
|
||||
while len < 8 && size >= (1u64 << (7 * len)) - 1 {
|
||||
len += 1;
|
||||
}
|
||||
let len = len.max(min_len).min(8);
|
||||
let v = size | (1u64 << (7 * len));
|
||||
out.extend_from_slice(&v.to_be_bytes()[8 - len..]);
|
||||
}
|
||||
|
||||
/// Encode the "unknown size" marker of the given length.
|
||||
pub fn write_unknown_size(out: &mut Vec<u8>, len: usize) {
|
||||
out.push(0xFFu8 >> (len - 1));
|
||||
out.extend(std::iter::repeat_n(0xFF, len - 1));
|
||||
}
|
||||
|
||||
/// Big-endian unsigned integer of 0..=8 bytes.
|
||||
pub fn uint(data: &[u8]) -> u64 {
|
||||
data.iter().take(8).fold(0u64, |a, &b| (a << 8) | b as u64)
|
||||
}
|
||||
|
||||
/// Big-endian two's-complement signed integer of 0..=8 bytes.
|
||||
pub fn int(data: &[u8]) -> i64 {
|
||||
let n = data.len().min(8);
|
||||
if n == 0 {
|
||||
return 0;
|
||||
}
|
||||
let v = uint(&data[..n]);
|
||||
let shift = 64 - 8 * n as u32;
|
||||
((v << shift) as i64) >> shift
|
||||
}
|
||||
|
||||
/// IEEE float of 0, 4 or 8 bytes.
|
||||
pub fn float(data: &[u8]) -> f64 {
|
||||
match data.len() {
|
||||
4 => f32::from_be_bytes(data.try_into().unwrap_or([0; 4])) as f64,
|
||||
8 => f64::from_be_bytes(data.try_into().unwrap_or([0; 8])),
|
||||
_ => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// String / UTF-8 value: bytes up to the first NUL, lossily decoded.
|
||||
pub fn string(data: &[u8]) -> String {
|
||||
let end = data.iter().position(|&b| b == 0).unwrap_or(data.len());
|
||||
String::from_utf8_lossy(&data[..end]).into_owned()
|
||||
}
|
||||
|
||||
/// One element inside an in-memory master element.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Elem<'a> {
|
||||
pub id: u32,
|
||||
/// Offset of the element header, relative to the start of the parent's data.
|
||||
pub offset: usize,
|
||||
pub header_len: usize,
|
||||
pub data: &'a [u8],
|
||||
/// The size field was "unknown" (data runs to the end of the parent).
|
||||
pub unknown_size: bool,
|
||||
/// The declared size overran the parent and was truncated.
|
||||
pub truncated: bool,
|
||||
}
|
||||
|
||||
/// Iterator over the children of an in-memory master element.
|
||||
///
|
||||
/// Damaged data ends the iteration (see [`Children::damaged`]); oversize elements are truncated to
|
||||
/// the parent's end.
|
||||
pub struct Children<'a> {
|
||||
data: &'a [u8],
|
||||
pos: usize,
|
||||
damaged: bool,
|
||||
}
|
||||
|
||||
impl<'a> Children<'a> {
|
||||
pub fn new(data: &'a [u8]) -> Self {
|
||||
Self { data, pos: 0, damaged: false }
|
||||
}
|
||||
/// True if iteration stopped on an undecodable header.
|
||||
pub fn damaged(&self) -> bool {
|
||||
self.damaged
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Children<'a> {
|
||||
type Item = Elem<'a>;
|
||||
fn next(&mut self) -> Option<Elem<'a>> {
|
||||
let rest = self.data.get(self.pos..)?;
|
||||
if rest.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let Some((id, il)) = read_id(rest) else {
|
||||
self.damaged = true;
|
||||
self.pos = self.data.len();
|
||||
return None;
|
||||
};
|
||||
let Some((size, sl)) = read_size(&rest[il..]) else {
|
||||
self.damaged = true;
|
||||
self.pos = self.data.len();
|
||||
return None;
|
||||
};
|
||||
let hl = il + sl;
|
||||
let avail = (rest.len() - hl) as u64;
|
||||
let (len, unknown, truncated) = match size {
|
||||
Size::Known(n) if n <= avail => (n as usize, false, false),
|
||||
Size::Known(_) => (avail as usize, false, true),
|
||||
Size::Unknown => (avail as usize, true, false),
|
||||
};
|
||||
let e = Elem { id, offset: self.pos, header_len: hl, data: &rest[hl..hl + len], unknown_size: unknown, truncated };
|
||||
self.pos += hl + len;
|
||||
Some(e)
|
||||
}
|
||||
}
|
||||
|
||||
/// CRC-32 (ISO 3309 / IEEE 802.3, reflected polynomial 0xEDB88320), as used by EBML CRC-32 elements.
|
||||
pub fn crc32(data: &[u8]) -> u32 {
|
||||
crc32_update(0xFFFF_FFFF, data) ^ 0xFFFF_FFFF
|
||||
}
|
||||
|
||||
/// Incremental form: start with `0xFFFF_FFFF`, finish with `^ 0xFFFF_FFFF`.
|
||||
pub fn crc32_update(mut crc: u32, data: &[u8]) -> u32 {
|
||||
static TABLE: std::sync::OnceLock<[u32; 256]> = std::sync::OnceLock::new();
|
||||
let t = TABLE.get_or_init(|| {
|
||||
let mut t = [0u32; 256];
|
||||
for (i, e) in t.iter_mut().enumerate() {
|
||||
let mut c = i as u32;
|
||||
for _ in 0..8 {
|
||||
c = if c & 1 != 0 { 0xEDB8_8320 ^ (c >> 1) } else { c >> 1 };
|
||||
}
|
||||
*e = c;
|
||||
}
|
||||
t
|
||||
});
|
||||
for &b in data {
|
||||
crc = t[((crc ^ b as u32) & 0xFF) as usize] ^ (crc >> 8);
|
||||
}
|
||||
crc
|
||||
}
|
||||
|
||||
/// Check a master element's leading CRC-32 child, if present. `Some(true/false)` if there is one.
|
||||
pub fn verify_crc(master_data: &[u8]) -> Option<bool> {
|
||||
let mut it = Children::new(master_data);
|
||||
let first = it.next()?;
|
||||
if first.id != crate::ids::CRC32 || first.data.len() != 4 {
|
||||
return None;
|
||||
}
|
||||
let stored = u32::from_le_bytes(first.data.try_into().ok()?);
|
||||
let rest = &master_data[first.offset + first.header_len + 4..];
|
||||
Some(crc32(rest) == stored)
|
||||
}
|
||||
|
||||
/// Block lacing mode (flags bits 1–2).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Lacing {
|
||||
None,
|
||||
Xiph,
|
||||
FixedSize,
|
||||
Ebml,
|
||||
}
|
||||
|
||||
impl Lacing {
|
||||
pub fn from_flags(flags: u8) -> Lacing {
|
||||
match (flags >> 1) & 3 {
|
||||
0 => Lacing::None,
|
||||
1 => Lacing::Xiph,
|
||||
2 => Lacing::FixedSize,
|
||||
_ => Lacing::Ebml,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Split laced block payload. `data` starts right after the block's flags byte and runs to the end
|
||||
/// of the block. Returns `(offset, len)` of each frame relative to `data`.
|
||||
pub fn parse_lacing(lacing: Lacing, data: &[u8]) -> Result<Vec<(usize, usize)>> {
|
||||
if lacing == Lacing::None {
|
||||
return Ok(vec![(0, data.len())]);
|
||||
}
|
||||
let Some(&n1) = data.first() else { return invalid("laced block without frame count") };
|
||||
let count = n1 as usize + 1;
|
||||
let mut pos = 1usize;
|
||||
let mut sizes = Vec::with_capacity(count);
|
||||
match lacing {
|
||||
Lacing::None => return Ok(vec![(0, data.len())]),
|
||||
Lacing::Xiph => {
|
||||
for _ in 0..count - 1 {
|
||||
let mut s = 0usize;
|
||||
loop {
|
||||
let Some(&b) = data.get(pos) else { return invalid("truncated Xiph lace header") };
|
||||
pos += 1;
|
||||
s += b as usize;
|
||||
if b != 255 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
sizes.push(s);
|
||||
}
|
||||
}
|
||||
Lacing::Ebml => {
|
||||
if count > 1 {
|
||||
let Some((first, l, _)) = read_vint(&data[pos..]) else { return invalid("bad EBML lace size") };
|
||||
pos += l;
|
||||
let mut prev = first as i64;
|
||||
sizes.push(first as usize);
|
||||
for _ in 1..count - 1 {
|
||||
let Some((d, l)) = read_svint(&data[pos..]) else { return invalid("bad EBML lace delta") };
|
||||
pos += l;
|
||||
prev += d;
|
||||
if prev < 0 {
|
||||
return invalid("negative EBML lace size");
|
||||
}
|
||||
sizes.push(prev as usize);
|
||||
}
|
||||
}
|
||||
}
|
||||
Lacing::FixedSize => {
|
||||
let rest = data.len() - pos;
|
||||
if !rest.is_multiple_of(count) {
|
||||
return invalid("fixed-size lacing does not divide block");
|
||||
}
|
||||
sizes.extend(std::iter::repeat_n(rest / count, count - 1));
|
||||
}
|
||||
}
|
||||
let used: usize = sizes.iter().try_fold(0usize, |a, &s| a.checked_add(s)).unwrap_or(usize::MAX);
|
||||
if pos > data.len() || used > data.len() - pos {
|
||||
return invalid("lace sizes exceed block");
|
||||
}
|
||||
sizes.push(data.len() - pos - used);
|
||||
let mut out = Vec::with_capacity(count);
|
||||
for s in sizes {
|
||||
out.push((pos, s));
|
||||
pos += s;
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn vint_roundtrip() {
|
||||
for &v in &[0u64, 1, 126, 127, 128, 16382, 16383, 16384, 1 << 20, (1 << 56) - 2] {
|
||||
let mut b = Vec::new();
|
||||
write_size(&mut b, v, 1);
|
||||
let (s, l) = read_size(&b).unwrap();
|
||||
assert_eq!(s, Size::Known(v), "value {v}");
|
||||
assert_eq!(l, b.len());
|
||||
}
|
||||
// 127 needs 2 bytes because 0xFF is "unknown"
|
||||
let mut b = Vec::new();
|
||||
write_size(&mut b, 127, 1);
|
||||
assert_eq!(b, [0x40, 0x7F]);
|
||||
// padded length
|
||||
let mut b = Vec::new();
|
||||
write_size(&mut b, 5, 8);
|
||||
assert_eq!(b, [1, 0, 0, 0, 0, 0, 0, 5]);
|
||||
assert_eq!(read_size(&b).unwrap(), (Size::Known(5), 8));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vint_unknown_and_invalid() {
|
||||
assert_eq!(read_size(&[0xFF]).unwrap(), (Size::Unknown, 1));
|
||||
assert_eq!(read_size(&[0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]).unwrap(), (Size::Unknown, 8));
|
||||
for l in 1..=8 {
|
||||
let mut b = Vec::new();
|
||||
write_unknown_size(&mut b, l);
|
||||
assert_eq!(read_size(&b).unwrap(), (Size::Unknown, l));
|
||||
}
|
||||
assert!(read_size(&[0x00, 1, 2]).is_none());
|
||||
assert!(read_size(&[0x40]).is_none()); // truncated
|
||||
assert_eq!(read_vint(&[0x81]).unwrap(), (1, 1, false));
|
||||
assert_eq!(read_vint(&[0x10, 0x00, 0x00, 0x01]).unwrap(), (1, 4, false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ids() {
|
||||
assert_eq!(read_id(&[0x1A, 0x45, 0xDF, 0xA3, 0x99]).unwrap(), (crate::ids::EBML, 4));
|
||||
assert_eq!(read_id(&[0xA3]).unwrap(), (0xA3, 1));
|
||||
assert_eq!(read_id(&[0x42, 0x86]).unwrap(), (0x4286, 2));
|
||||
assert!(read_id(&[0xFF]).is_none()); // reserved
|
||||
assert_eq!(read_id(&[0x80]).unwrap(), (0x80, 1)); // ChapterDisplay
|
||||
assert!(read_id(&[0x40, 0x00]).is_none()); // zero value
|
||||
assert!(read_id(&[0x08, 0, 0, 0, 1]).is_none()); // 5-byte ID
|
||||
for id in [0xA3u32, 0x4286, 0x2AD7B1, 0x1F43B675] {
|
||||
let mut b = Vec::new();
|
||||
write_id(&mut b, id);
|
||||
assert_eq!(read_id(&b).unwrap(), (id, b.len()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signed_vint() {
|
||||
assert_eq!(read_svint(&[0xBF]).unwrap(), (0, 1)); // 63 - 63
|
||||
assert_eq!(read_svint(&[0x80]).unwrap(), (-63, 1));
|
||||
assert_eq!(read_svint(&[0xC0]).unwrap(), (1, 1));
|
||||
assert_eq!(read_svint(&[0x5F, 0xFF]).unwrap(), (0, 2));
|
||||
assert_eq!(read_svint(&[0x60, 0x00]).unwrap(), (1, 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn values() {
|
||||
assert_eq!(uint(&[]), 0);
|
||||
assert_eq!(uint(&[1, 0]), 256);
|
||||
assert_eq!(int(&[0xFF]), -1);
|
||||
assert_eq!(int(&[0xFF, 0xFE]), -2);
|
||||
assert_eq!(int(&[0x7F]), 127);
|
||||
assert_eq!(float(&1.5f32.to_be_bytes()), 1.5);
|
||||
assert_eq!(float(&2.25f64.to_be_bytes()), 2.25);
|
||||
assert_eq!(string(b"abc\0\0"), "abc");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crc() {
|
||||
assert_eq!(crc32(b"123456789"), 0xCBF4_3926);
|
||||
let payload = [0x42u8, 0x86, 0x81, 0x01];
|
||||
let mut m = vec![0xBF, 0x84];
|
||||
m.extend_from_slice(&crc32(&payload).to_le_bytes());
|
||||
m.extend_from_slice(&payload);
|
||||
assert_eq!(verify_crc(&m), Some(true));
|
||||
m[7] ^= 1;
|
||||
assert_eq!(verify_crc(&m), Some(false));
|
||||
assert_eq!(verify_crc(&payload), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn children_iter() {
|
||||
// two elements, then one whose size overruns
|
||||
let d = [0x42, 0x86, 0x81, 0x01, 0xEC, 0x82, 0, 0, 0xA3, 0x85, 1, 2];
|
||||
let v: Vec<_> = Children::new(&d).collect();
|
||||
assert_eq!(v.len(), 3);
|
||||
assert_eq!(v[0].id, 0x4286);
|
||||
assert_eq!(v[0].data, &[1]);
|
||||
assert_eq!(v[1].id, 0xEC);
|
||||
assert_eq!(v[2].data, &[1, 2]);
|
||||
assert!(v[2].truncated);
|
||||
let mut it = Children::new(&[0x00, 0x01]);
|
||||
assert!(it.next().is_none());
|
||||
assert!(it.damaged());
|
||||
}
|
||||
|
||||
fn frames<'a>(d: &'a [u8], f: &[(usize, usize)]) -> Vec<&'a [u8]> {
|
||||
f.iter().map(|&(o, l)| &d[o..o + l]).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lacing_xiph() {
|
||||
// 3 frames: 300, 2, rest(3)
|
||||
let mut d = vec![2u8, 255, 45, 2];
|
||||
d.extend(std::iter::repeat_n(7u8, 300));
|
||||
d.extend_from_slice(&[1, 2]);
|
||||
d.extend_from_slice(&[3, 4, 5]);
|
||||
let f = parse_lacing(Lacing::Xiph, &d).unwrap();
|
||||
assert_eq!(f.iter().map(|x| x.1).collect::<Vec<_>>(), [300, 2, 3]);
|
||||
assert_eq!(frames(&d, &f)[2], &[3, 4, 5]);
|
||||
// exact multiple of 255 needs a terminating 0
|
||||
let mut d = vec![1u8, 255, 0];
|
||||
d.extend(std::iter::repeat_n(1u8, 255));
|
||||
d.push(9);
|
||||
let f = parse_lacing(Lacing::Xiph, &d).unwrap();
|
||||
assert_eq!(f.iter().map(|x| x.1).collect::<Vec<_>>(), [255, 1]);
|
||||
assert!(parse_lacing(Lacing::Xiph, &[1, 200, 1]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lacing_ebml() {
|
||||
// 4 frames: 5, 3 (delta -2), 4 (delta +1), rest 2
|
||||
let mut d = vec![3u8, 0x85, 0xBF - 2, 0xBF + 1];
|
||||
d.extend_from_slice(&[1; 5]);
|
||||
d.extend_from_slice(&[2; 3]);
|
||||
d.extend_from_slice(&[3; 4]);
|
||||
d.extend_from_slice(&[4; 2]);
|
||||
let f = parse_lacing(Lacing::Ebml, &d).unwrap();
|
||||
assert_eq!(f.iter().map(|x| x.1).collect::<Vec<_>>(), [5, 3, 4, 2]);
|
||||
assert_eq!(frames(&d, &f), [&[1u8; 5][..], &[2; 3], &[3; 4], &[4; 2]]);
|
||||
assert!(parse_lacing(Lacing::Ebml, &[1, 0x90]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lacing_fixed_and_none() {
|
||||
let d = [2u8, 1, 1, 2, 2, 3, 3];
|
||||
let f = parse_lacing(Lacing::FixedSize, &d).unwrap();
|
||||
assert_eq!(frames(&d, &f), [&[1u8, 1][..], &[2, 2], &[3, 3]]);
|
||||
assert!(parse_lacing(Lacing::FixedSize, &[2, 1, 1]).is_err());
|
||||
assert_eq!(parse_lacing(Lacing::None, &[9, 9]).unwrap(), [(0, 2)]);
|
||||
assert_eq!(Lacing::from_flags(0x80 | 0x06), Lacing::Ebml);
|
||||
assert_eq!(Lacing::from_flags(0x02), Lacing::Xiph);
|
||||
assert_eq!(Lacing::from_flags(0x04), Lacing::FixedSize);
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
|
||||
/// Errors produced by the demuxer.
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Underlying I/O failure.
|
||||
Io(io::Error),
|
||||
/// Structurally invalid data that could not be recovered from.
|
||||
Invalid(String),
|
||||
/// Valid but unsupported feature (e.g. zlib-compressed or encrypted tracks when reading data).
|
||||
Unsupported(String),
|
||||
/// Not an EBML file, or an EBML file whose DocType is not `matroska`/`webm`.
|
||||
NotMatroska(String),
|
||||
/// A track index was out of range.
|
||||
NoSuchTrack(usize),
|
||||
/// A sample index was out of range.
|
||||
NoSuchSample(usize),
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Error::Io(e) => write!(f, "I/O error: {e}"),
|
||||
Error::Invalid(s) => write!(f, "invalid data: {s}"),
|
||||
Error::Unsupported(s) => write!(f, "unsupported: {s}"),
|
||||
Error::NotMatroska(s) => write!(f, "not a Matroska/WebM file: {s}"),
|
||||
Error::NoSuchTrack(i) => write!(f, "track index {i} out of range"),
|
||||
Error::NoSuchSample(i) => write!(f, "sample index {i} out of range"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Error::Io(e) => Some(e),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(e: io::Error) -> Self {
|
||||
Error::Io(e)
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
pub(crate) fn invalid<T>(msg: impl Into<String>) -> Result<T> {
|
||||
Err(Error::Invalid(msg.into()))
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
//! EBML / Matroska element IDs (RFC 8794, RFC 9559). IDs keep their VINT marker bits.
|
||||
#![allow(dead_code)]
|
||||
|
||||
// EBML header + global elements
|
||||
pub const EBML: u32 = 0x1A45_DFA3;
|
||||
pub const EBML_VERSION: u32 = 0x4286;
|
||||
pub const EBML_READ_VERSION: u32 = 0x42F7;
|
||||
pub const EBML_MAX_ID_LENGTH: u32 = 0x42F2;
|
||||
pub const EBML_MAX_SIZE_LENGTH: u32 = 0x42F3;
|
||||
pub const DOC_TYPE: u32 = 0x4282;
|
||||
pub const DOC_TYPE_VERSION: u32 = 0x4287;
|
||||
pub const DOC_TYPE_READ_VERSION: u32 = 0x4285;
|
||||
pub const CRC32: u32 = 0xBF;
|
||||
pub const VOID: u32 = 0xEC;
|
||||
|
||||
// Segment and level-1 elements
|
||||
pub const SEGMENT: u32 = 0x1853_8067;
|
||||
pub const SEEK_HEAD: u32 = 0x114D_9B74;
|
||||
pub const INFO: u32 = 0x1549_A966;
|
||||
pub const TRACKS: u32 = 0x1654_AE6B;
|
||||
pub const CLUSTER: u32 = 0x1F43_B675;
|
||||
pub const CUES: u32 = 0x1C53_BB6B;
|
||||
pub const ATTACHMENTS: u32 = 0x1941_A469;
|
||||
pub const CHAPTERS: u32 = 0x1043_A770;
|
||||
pub const TAGS: u32 = 0x1254_C367;
|
||||
|
||||
/// True for IDs that can only appear directly inside a Segment (or are the EBML/Segment headers):
|
||||
/// used to find the end of unknown-size elements.
|
||||
pub fn is_top_level(id: u32) -> bool {
|
||||
matches!(id, EBML | SEGMENT | SEEK_HEAD | INFO | TRACKS | CLUSTER | CUES | ATTACHMENTS | CHAPTERS | TAGS)
|
||||
}
|
||||
|
||||
// SeekHead
|
||||
pub const SEEK: u32 = 0x4DBB;
|
||||
pub const SEEK_ID: u32 = 0x53AB;
|
||||
pub const SEEK_POSITION: u32 = 0x53AC;
|
||||
|
||||
// Info
|
||||
pub const SEGMENT_UUID: u32 = 0x73A4;
|
||||
pub const SEGMENT_FILENAME: u32 = 0x7384;
|
||||
pub const TIMESTAMP_SCALE: u32 = 0x2A_D7B1;
|
||||
pub const DURATION: u32 = 0x4489;
|
||||
pub const DATE_UTC: u32 = 0x4461;
|
||||
pub const TITLE: u32 = 0x7BA9;
|
||||
pub const MUXING_APP: u32 = 0x4D80;
|
||||
pub const WRITING_APP: u32 = 0x5741;
|
||||
|
||||
// Cluster
|
||||
pub const TIMESTAMP: u32 = 0xE7;
|
||||
pub const POSITION: u32 = 0xA7;
|
||||
pub const PREV_SIZE: u32 = 0xAB;
|
||||
pub const SIMPLE_BLOCK: u32 = 0xA3;
|
||||
pub const BLOCK_GROUP: u32 = 0xA0;
|
||||
pub const ENCRYPTED_BLOCK: u32 = 0xAF;
|
||||
pub const SILENT_TRACKS: u32 = 0x5854;
|
||||
pub const BLOCK: u32 = 0xA1;
|
||||
pub const BLOCK_ADDITIONS: u32 = 0x75A1;
|
||||
pub const BLOCK_DURATION: u32 = 0x9B;
|
||||
pub const REFERENCE_PRIORITY: u32 = 0xFA;
|
||||
pub const REFERENCE_BLOCK: u32 = 0xFB;
|
||||
pub const CODEC_STATE: u32 = 0xA4;
|
||||
pub const DISCARD_PADDING: u32 = 0x75A2;
|
||||
|
||||
pub fn is_cluster_child(id: u32) -> bool {
|
||||
matches!(id, TIMESTAMP | POSITION | PREV_SIZE | SIMPLE_BLOCK | BLOCK_GROUP | ENCRYPTED_BLOCK | SILENT_TRACKS | CRC32 | VOID)
|
||||
}
|
||||
|
||||
// Tracks
|
||||
pub const TRACK_ENTRY: u32 = 0xAE;
|
||||
pub const TRACK_NUMBER: u32 = 0xD7;
|
||||
pub const TRACK_UID: u32 = 0x73C5;
|
||||
pub const TRACK_TYPE: u32 = 0x83;
|
||||
pub const FLAG_ENABLED: u32 = 0xB9;
|
||||
pub const FLAG_DEFAULT: u32 = 0x88;
|
||||
pub const FLAG_FORCED: u32 = 0x55AA;
|
||||
pub const FLAG_HEARING_IMPAIRED: u32 = 0x55AB;
|
||||
pub const FLAG_VISUAL_IMPAIRED: u32 = 0x55AC;
|
||||
pub const FLAG_ORIGINAL: u32 = 0x55AE;
|
||||
pub const FLAG_COMMENTARY: u32 = 0x55AF;
|
||||
pub const FLAG_LACING: u32 = 0x9C;
|
||||
pub const DEFAULT_DURATION: u32 = 0x23_E383;
|
||||
pub const TRACK_TIMESTAMP_SCALE: u32 = 0x23_314F;
|
||||
pub const NAME: u32 = 0x536E;
|
||||
pub const LANGUAGE: u32 = 0x22_B59C;
|
||||
pub const LANGUAGE_BCP47: u32 = 0x22_B59D;
|
||||
pub const CODEC_ID: u32 = 0x86;
|
||||
pub const CODEC_PRIVATE: u32 = 0x63A2;
|
||||
pub const CODEC_NAME: u32 = 0x25_8688;
|
||||
pub const CODEC_DELAY: u32 = 0x56AA;
|
||||
pub const SEEK_PRE_ROLL: u32 = 0x56BB;
|
||||
|
||||
// Video
|
||||
pub const VIDEO: u32 = 0xE0;
|
||||
pub const FLAG_INTERLACED: u32 = 0x9A;
|
||||
pub const FIELD_ORDER: u32 = 0x9D;
|
||||
pub const STEREO_MODE: u32 = 0x53B8;
|
||||
pub const ALPHA_MODE: u32 = 0x53C0;
|
||||
pub const PIXEL_WIDTH: u32 = 0xB0;
|
||||
pub const PIXEL_HEIGHT: u32 = 0xBA;
|
||||
pub const PIXEL_CROP_BOTTOM: u32 = 0x54AA;
|
||||
pub const PIXEL_CROP_TOP: u32 = 0x54BB;
|
||||
pub const PIXEL_CROP_LEFT: u32 = 0x54CC;
|
||||
pub const PIXEL_CROP_RIGHT: u32 = 0x54DD;
|
||||
pub const DISPLAY_WIDTH: u32 = 0x54B0;
|
||||
pub const DISPLAY_HEIGHT: u32 = 0x54BA;
|
||||
pub const DISPLAY_UNIT: u32 = 0x54B2;
|
||||
pub const COLOUR_SPACE: u32 = 0x2E_B524;
|
||||
pub const COLOUR: u32 = 0x55B0;
|
||||
pub const PROJECTION: u32 = 0x7670;
|
||||
pub const PROJECTION_TYPE: u32 = 0x7671;
|
||||
pub const PROJECTION_POSE_YAW: u32 = 0x7673;
|
||||
pub const PROJECTION_POSE_PITCH: u32 = 0x7674;
|
||||
pub const PROJECTION_POSE_ROLL: u32 = 0x7675;
|
||||
|
||||
// Colour
|
||||
pub const MATRIX_COEFFICIENTS: u32 = 0x55B1;
|
||||
pub const BITS_PER_CHANNEL: u32 = 0x55B2;
|
||||
pub const CHROMA_SUBSAMPLING_HORZ: u32 = 0x55B3;
|
||||
pub const CHROMA_SUBSAMPLING_VERT: u32 = 0x55B4;
|
||||
pub const CB_SUBSAMPLING_HORZ: u32 = 0x55B5;
|
||||
pub const CB_SUBSAMPLING_VERT: u32 = 0x55B6;
|
||||
pub const CHROMA_SITING_HORZ: u32 = 0x55B7;
|
||||
pub const CHROMA_SITING_VERT: u32 = 0x55B8;
|
||||
pub const RANGE: u32 = 0x55B9;
|
||||
pub const TRANSFER_CHARACTERISTICS: u32 = 0x55BA;
|
||||
pub const PRIMARIES: u32 = 0x55BB;
|
||||
pub const MAX_CLL: u32 = 0x55BC;
|
||||
pub const MAX_FALL: u32 = 0x55BD;
|
||||
pub const MASTERING_METADATA: u32 = 0x55D0;
|
||||
pub const PRIMARY_R_X: u32 = 0x55D1;
|
||||
pub const PRIMARY_R_Y: u32 = 0x55D2;
|
||||
pub const PRIMARY_G_X: u32 = 0x55D3;
|
||||
pub const PRIMARY_G_Y: u32 = 0x55D4;
|
||||
pub const PRIMARY_B_X: u32 = 0x55D5;
|
||||
pub const PRIMARY_B_Y: u32 = 0x55D6;
|
||||
pub const WHITE_POINT_X: u32 = 0x55D7;
|
||||
pub const WHITE_POINT_Y: u32 = 0x55D8;
|
||||
pub const LUMINANCE_MAX: u32 = 0x55D9;
|
||||
pub const LUMINANCE_MIN: u32 = 0x55DA;
|
||||
|
||||
// Audio
|
||||
pub const AUDIO: u32 = 0xE1;
|
||||
pub const SAMPLING_FREQUENCY: u32 = 0xB5;
|
||||
pub const OUTPUT_SAMPLING_FREQUENCY: u32 = 0x78B5;
|
||||
pub const CHANNELS: u32 = 0x9F;
|
||||
pub const BIT_DEPTH: u32 = 0x6264;
|
||||
|
||||
// ContentEncodings
|
||||
pub const CONTENT_ENCODINGS: u32 = 0x6D80;
|
||||
pub const CONTENT_ENCODING: u32 = 0x6240;
|
||||
pub const CONTENT_ENCODING_ORDER: u32 = 0x5031;
|
||||
pub const CONTENT_ENCODING_SCOPE: u32 = 0x5032;
|
||||
pub const CONTENT_ENCODING_TYPE: u32 = 0x5033;
|
||||
pub const CONTENT_COMPRESSION: u32 = 0x5034;
|
||||
pub const CONTENT_COMP_ALGO: u32 = 0x4254;
|
||||
pub const CONTENT_COMP_SETTINGS: u32 = 0x4255;
|
||||
pub const CONTENT_ENCRYPTION: u32 = 0x5035;
|
||||
|
||||
// Cues
|
||||
pub const CUE_POINT: u32 = 0xBB;
|
||||
pub const CUE_TIME: u32 = 0xB3;
|
||||
pub const CUE_TRACK_POSITIONS: u32 = 0xB7;
|
||||
pub const CUE_TRACK: u32 = 0xF7;
|
||||
pub const CUE_CLUSTER_POSITION: u32 = 0xF1;
|
||||
pub const CUE_RELATIVE_POSITION: u32 = 0xF0;
|
||||
pub const CUE_DURATION: u32 = 0xB2;
|
||||
pub const CUE_BLOCK_NUMBER: u32 = 0x5378;
|
||||
|
||||
// Attachments
|
||||
pub const ATTACHED_FILE: u32 = 0x61A7;
|
||||
pub const FILE_DESCRIPTION: u32 = 0x467E;
|
||||
pub const FILE_NAME: u32 = 0x466E;
|
||||
pub const FILE_MEDIA_TYPE: u32 = 0x4660;
|
||||
pub const FILE_DATA: u32 = 0x465C;
|
||||
pub const FILE_UID: u32 = 0x46AE;
|
||||
|
||||
// Chapters
|
||||
pub const EDITION_ENTRY: u32 = 0x45B9;
|
||||
pub const EDITION_UID: u32 = 0x45BC;
|
||||
pub const EDITION_FLAG_HIDDEN: u32 = 0x45BD;
|
||||
pub const EDITION_FLAG_DEFAULT: u32 = 0x45DB;
|
||||
pub const EDITION_FLAG_ORDERED: u32 = 0x45DD;
|
||||
pub const CHAPTER_ATOM: u32 = 0xB6;
|
||||
pub const CHAPTER_UID: u32 = 0x73C4;
|
||||
pub const CHAPTER_STRING_UID: u32 = 0x5654;
|
||||
pub const CHAPTER_TIME_START: u32 = 0x91;
|
||||
pub const CHAPTER_TIME_END: u32 = 0x92;
|
||||
pub const CHAPTER_FLAG_HIDDEN: u32 = 0x98;
|
||||
pub const CHAPTER_FLAG_ENABLED: u32 = 0x4598;
|
||||
pub const CHAPTER_DISPLAY: u32 = 0x80;
|
||||
pub const CHAP_STRING: u32 = 0x85;
|
||||
pub const CHAP_LANGUAGE: u32 = 0x437C;
|
||||
pub const CHAP_LANGUAGE_BCP47: u32 = 0x437D;
|
||||
pub const CHAP_COUNTRY: u32 = 0x437E;
|
||||
|
||||
// Tags
|
||||
pub const TAG: u32 = 0x7373;
|
||||
pub const TARGETS: u32 = 0x63C0;
|
||||
pub const TARGET_TYPE_VALUE: u32 = 0x68CA;
|
||||
pub const TARGET_TYPE: u32 = 0x63CA;
|
||||
pub const TAG_TRACK_UID: u32 = 0x63C5;
|
||||
pub const TAG_EDITION_UID: u32 = 0x63C9;
|
||||
pub const TAG_CHAPTER_UID: u32 = 0x63C4;
|
||||
pub const TAG_ATTACHMENT_UID: u32 = 0x63C6;
|
||||
pub const SIMPLE_TAG: u32 = 0x67C8;
|
||||
pub const TAG_NAME: u32 = 0x45A3;
|
||||
pub const TAG_LANGUAGE: u32 = 0x447A;
|
||||
pub const TAG_LANGUAGE_BCP47: u32 = 0x447B;
|
||||
pub const TAG_DEFAULT: u32 = 0x4484;
|
||||
pub const TAG_STRING: u32 = 0x4487;
|
||||
pub const TAG_BINARY: u32 = 0x4485;
|
||||
@@ -1,40 +1,6 @@
|
||||
//! Clean-room Matroska (MKV) and WebM demuxer, written from RFC 8794 (EBML), RFC 9559 (Matroska)
|
||||
//! and the WebM container guidelines.
|
||||
//!
|
||||
//! - [`open`] / [`open_with`] parse the EBML header and the first Segment (Info, Tracks, SeekHead,
|
||||
//! Cues, Chapters, Tags, Attachments) and, by default, scan every Cluster's block headers to build
|
||||
//! per-track [`Sample`] tables — the same shape as `deckcraft-isobmff`, so callers can treat both
|
||||
//! containers alike. Frame data is read on demand with [`MkvFile::read_sample`].
|
||||
//! - [`Demuxer`] iterates [`Packet`]s in file order (from any [`ByteSource`], `Read + Seek`, or a
|
||||
//! byte slice) and seeks to the keyframe preceding a time, via the index, Cues, or a cluster scan.
|
||||
//!
|
||||
//! Layer L0: no dependencies beyond `std`, no `unsafe`, builds for `wasm32-unknown-unknown`.
|
||||
//! Matroska (MKV) and WebM demuxer and muxer: a re-export of [`craft_media::matroska`] from
|
||||
//! craft-libs. Supported elements and limitations are in the craft-media README.
|
||||
|
||||
#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable))]
|
||||
|
||||
mod codec;
|
||||
mod demux;
|
||||
pub mod ebml;
|
||||
mod error;
|
||||
mod ids;
|
||||
mod meta;
|
||||
mod mux;
|
||||
mod source;
|
||||
#[cfg(test)]
|
||||
mod synthetic_tests;
|
||||
mod track;
|
||||
|
||||
pub use codec::Codec;
|
||||
pub use demux::{Demuxer, Keyframe, MkvFile, OpenOptions, Packet, SeekPoint, open, open_with};
|
||||
pub use error::{Error, Result};
|
||||
pub use meta::{
|
||||
Attachment, Chapter, ChapterDisplay, ClusterInfo, CuePoint, CuePosition, Edition, SeekEntry, SegmentInfo, SimpleTag, Tag, TagTargets,
|
||||
};
|
||||
pub use mux::{LacingMode, MkvWriter, MuxOptions, TrackSpec};
|
||||
pub use source::{ByteSource, ReadSeekSource};
|
||||
pub use track::{AudioInfo, Colour, ContentEncoding, MasteringMetadata, Projection, Sample, Track, TrackKind, VideoInfo};
|
||||
|
||||
/// Element IDs used by the demuxer (marker bits included), for callers inspecting raw elements.
|
||||
pub mod element_ids {
|
||||
pub use crate::ids::*;
|
||||
}
|
||||
pub use craft_media::matroska::*;
|
||||
|
||||
@@ -1,612 +0,0 @@
|
||||
//! Segment-level metadata: Info, SeekHead, Cues, Chapters, Tags, Attachments — types and parsers
|
||||
//! for their in-memory element data.
|
||||
|
||||
use crate::codec::map_codec;
|
||||
use crate::ebml::{Children, float, int, string, uint};
|
||||
use crate::ids::*;
|
||||
use crate::track::{AudioInfo, Colour, ContentEncoding, MasteringMetadata, Projection, Track, TrackKind, VideoInfo, gcd};
|
||||
|
||||
/// `Info` element.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct SegmentInfo {
|
||||
/// Nanoseconds per timestamp tick (default 1 000 000 = 1 ms).
|
||||
pub timestamp_scale: u64,
|
||||
/// `Duration` in timestamp ticks (float), if present.
|
||||
pub duration: Option<f64>,
|
||||
pub title: Option<String>,
|
||||
pub muxing_app: String,
|
||||
pub writing_app: String,
|
||||
/// `DateUTC`: nanoseconds since 2001-01-01T00:00:00 UTC.
|
||||
pub date_utc: Option<i64>,
|
||||
pub segment_uuid: Option<[u8; 16]>,
|
||||
}
|
||||
|
||||
impl Default for SegmentInfo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
timestamp_scale: 1_000_000,
|
||||
duration: None,
|
||||
title: None,
|
||||
muxing_app: String::new(),
|
||||
writing_app: String::new(),
|
||||
date_utc: None,
|
||||
segment_uuid: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One `SeekHead` entry: level-1 element ID and its position relative to the Segment data start.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct SeekEntry {
|
||||
pub id: u32,
|
||||
pub position: u64,
|
||||
}
|
||||
|
||||
/// One `CueTrackPositions` of a `CuePoint`.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct CuePosition {
|
||||
pub track: u64,
|
||||
/// Cluster position relative to the Segment data start.
|
||||
pub cluster_position: u64,
|
||||
/// Block position relative to the Cluster data start.
|
||||
pub relative_position: Option<u64>,
|
||||
pub block_number: Option<u64>,
|
||||
pub duration: Option<u64>,
|
||||
}
|
||||
|
||||
/// One `CuePoint`.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CuePoint {
|
||||
/// In timestamp ticks.
|
||||
pub time: u64,
|
||||
pub positions: Vec<CuePosition>,
|
||||
}
|
||||
|
||||
/// A cluster found while scanning.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct ClusterInfo {
|
||||
/// Absolute offset of the Cluster element header.
|
||||
pub offset: u64,
|
||||
/// Absolute offset of the first child.
|
||||
pub data_start: u64,
|
||||
/// Absolute end offset (for unknown-size clusters, where the next top-level element starts).
|
||||
pub end: u64,
|
||||
/// Cluster `Timestamp` in ticks.
|
||||
pub timestamp: u64,
|
||||
}
|
||||
|
||||
/// One `ChapterDisplay`.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct ChapterDisplay {
|
||||
pub string: String,
|
||||
pub languages: Vec<String>,
|
||||
pub languages_bcp47: Vec<String>,
|
||||
pub countries: Vec<String>,
|
||||
}
|
||||
|
||||
/// `ChapterAtom` (possibly nested).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Chapter {
|
||||
pub uid: u64,
|
||||
pub string_uid: Option<String>,
|
||||
pub start_ns: u64,
|
||||
pub end_ns: Option<u64>,
|
||||
pub hidden: bool,
|
||||
pub enabled: bool,
|
||||
pub displays: Vec<ChapterDisplay>,
|
||||
pub children: Vec<Chapter>,
|
||||
}
|
||||
|
||||
impl Chapter {
|
||||
/// First display string, if any.
|
||||
pub fn title(&self) -> Option<&str> {
|
||||
self.displays.first().map(|d| d.string.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
/// `EditionEntry`.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Edition {
|
||||
pub uid: Option<u64>,
|
||||
pub hidden: bool,
|
||||
pub default: bool,
|
||||
pub ordered: bool,
|
||||
pub chapters: Vec<Chapter>,
|
||||
}
|
||||
|
||||
/// `Targets` of a `Tag`. Empty UID lists mean "the whole segment".
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct TagTargets {
|
||||
pub type_value: u64,
|
||||
pub type_name: Option<String>,
|
||||
pub track_uids: Vec<u64>,
|
||||
pub edition_uids: Vec<u64>,
|
||||
pub chapter_uids: Vec<u64>,
|
||||
pub attachment_uids: Vec<u64>,
|
||||
}
|
||||
|
||||
impl Default for TagTargets {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
type_value: 50,
|
||||
type_name: None,
|
||||
track_uids: Vec::new(),
|
||||
edition_uids: Vec::new(),
|
||||
chapter_uids: Vec::new(),
|
||||
attachment_uids: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `SimpleTag` (possibly nested).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct SimpleTag {
|
||||
pub name: String,
|
||||
pub language: String,
|
||||
pub default: bool,
|
||||
pub string: Option<String>,
|
||||
pub binary: Option<Vec<u8>>,
|
||||
pub children: Vec<SimpleTag>,
|
||||
}
|
||||
|
||||
/// `Tag`.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Tag {
|
||||
pub targets: TagTargets,
|
||||
pub simple: Vec<SimpleTag>,
|
||||
}
|
||||
|
||||
/// `AttachedFile` (listed only; the payload is located by `data_offset`/`data_size`).
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Attachment {
|
||||
pub uid: u64,
|
||||
pub name: String,
|
||||
pub media_type: String,
|
||||
pub description: Option<String>,
|
||||
pub data_offset: u64,
|
||||
pub data_size: u64,
|
||||
}
|
||||
|
||||
pub(crate) fn parse_info(d: &[u8]) -> SegmentInfo {
|
||||
let mut info = SegmentInfo::default();
|
||||
for e in Children::new(d) {
|
||||
match e.id {
|
||||
TIMESTAMP_SCALE => {
|
||||
let v = uint(e.data);
|
||||
if v > 0 {
|
||||
info.timestamp_scale = v;
|
||||
}
|
||||
}
|
||||
DURATION => info.duration = Some(float(e.data)).filter(|v| v.is_finite() && *v >= 0.0),
|
||||
TITLE => info.title = Some(string(e.data)),
|
||||
MUXING_APP => info.muxing_app = string(e.data),
|
||||
WRITING_APP => info.writing_app = string(e.data),
|
||||
DATE_UTC => info.date_utc = Some(int(e.data)),
|
||||
SEGMENT_UUID => info.segment_uuid = e.data.try_into().ok(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
info
|
||||
}
|
||||
|
||||
pub(crate) fn parse_seekhead(d: &[u8]) -> Vec<SeekEntry> {
|
||||
let mut out = Vec::new();
|
||||
for s in Children::new(d).filter(|e| e.id == SEEK) {
|
||||
let (mut id, mut pos) = (None, None);
|
||||
for e in Children::new(s.data) {
|
||||
match e.id {
|
||||
SEEK_ID => id = Some(uint(e.data) as u32),
|
||||
SEEK_POSITION => pos = Some(uint(e.data)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if let (Some(id), Some(position)) = (id, pos) {
|
||||
out.push(SeekEntry { id, position });
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub(crate) fn parse_cues(d: &[u8], out: &mut Vec<CuePoint>) {
|
||||
for cp in Children::new(d).filter(|e| e.id == CUE_POINT) {
|
||||
let mut time = None;
|
||||
let mut positions = Vec::new();
|
||||
for e in Children::new(cp.data) {
|
||||
match e.id {
|
||||
CUE_TIME => time = Some(uint(e.data)),
|
||||
CUE_TRACK_POSITIONS => {
|
||||
let mut p = CuePosition { track: 0, cluster_position: u64::MAX, relative_position: None, block_number: None, duration: None };
|
||||
for c in Children::new(e.data) {
|
||||
match c.id {
|
||||
CUE_TRACK => p.track = uint(c.data),
|
||||
CUE_CLUSTER_POSITION => p.cluster_position = uint(c.data),
|
||||
CUE_RELATIVE_POSITION => p.relative_position = Some(uint(c.data)),
|
||||
CUE_BLOCK_NUMBER => p.block_number = Some(uint(c.data)),
|
||||
CUE_DURATION => p.duration = Some(uint(c.data)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if p.track != 0 && p.cluster_position != u64::MAX {
|
||||
positions.push(p);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if let Some(time) = time
|
||||
&& !positions.is_empty()
|
||||
{
|
||||
out.push(CuePoint { time, positions });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Deepest nesting of `ChapterAtom` / `SimpleTag` we follow. Deeper children are ignored so a hostile file cannot overflow the stack.
|
||||
const MAX_NESTING_DEPTH: usize = 32;
|
||||
|
||||
fn parse_chapter(d: &[u8], depth: usize) -> Chapter {
|
||||
let mut c =
|
||||
Chapter { uid: 0, string_uid: None, start_ns: 0, end_ns: None, hidden: false, enabled: true, displays: Vec::new(), children: Vec::new() };
|
||||
for e in Children::new(d) {
|
||||
match e.id {
|
||||
CHAPTER_UID => c.uid = uint(e.data),
|
||||
CHAPTER_STRING_UID => c.string_uid = Some(string(e.data)),
|
||||
CHAPTER_TIME_START => c.start_ns = uint(e.data),
|
||||
CHAPTER_TIME_END => c.end_ns = Some(uint(e.data)),
|
||||
CHAPTER_FLAG_HIDDEN => c.hidden = uint(e.data) != 0,
|
||||
CHAPTER_FLAG_ENABLED => c.enabled = uint(e.data) != 0,
|
||||
CHAPTER_DISPLAY => {
|
||||
let mut disp = ChapterDisplay::default();
|
||||
for x in Children::new(e.data) {
|
||||
match x.id {
|
||||
CHAP_STRING => disp.string = string(x.data),
|
||||
CHAP_LANGUAGE => disp.languages.push(string(x.data)),
|
||||
CHAP_LANGUAGE_BCP47 => disp.languages_bcp47.push(string(x.data)),
|
||||
CHAP_COUNTRY => disp.countries.push(string(x.data)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if disp.languages.is_empty() {
|
||||
disp.languages.push("eng".into());
|
||||
}
|
||||
c.displays.push(disp);
|
||||
}
|
||||
CHAPTER_ATOM if depth + 1 < MAX_NESTING_DEPTH => c.children.push(parse_chapter(e.data, depth + 1)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
c
|
||||
}
|
||||
|
||||
pub(crate) fn parse_chapters(d: &[u8], out: &mut Vec<Edition>) {
|
||||
for ed in Children::new(d).filter(|e| e.id == EDITION_ENTRY) {
|
||||
let mut edition = Edition::default();
|
||||
for e in Children::new(ed.data) {
|
||||
match e.id {
|
||||
EDITION_UID => edition.uid = Some(uint(e.data)),
|
||||
EDITION_FLAG_HIDDEN => edition.hidden = uint(e.data) != 0,
|
||||
EDITION_FLAG_DEFAULT => edition.default = uint(e.data) != 0,
|
||||
EDITION_FLAG_ORDERED => edition.ordered = uint(e.data) != 0,
|
||||
CHAPTER_ATOM => edition.chapters.push(parse_chapter(e.data, 0)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
out.push(edition);
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_simple_tag(d: &[u8], depth: usize) -> SimpleTag {
|
||||
let mut t = SimpleTag { name: String::new(), language: "und".into(), default: true, string: None, binary: None, children: Vec::new() };
|
||||
for e in Children::new(d) {
|
||||
match e.id {
|
||||
TAG_NAME => t.name = string(e.data),
|
||||
TAG_LANGUAGE => t.language = string(e.data),
|
||||
TAG_DEFAULT => t.default = uint(e.data) != 0,
|
||||
TAG_STRING => t.string = Some(string(e.data)),
|
||||
TAG_BINARY => t.binary = Some(e.data.to_vec()),
|
||||
SIMPLE_TAG if depth + 1 < MAX_NESTING_DEPTH => t.children.push(parse_simple_tag(e.data, depth + 1)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
t
|
||||
}
|
||||
|
||||
pub(crate) fn parse_tags(d: &[u8], out: &mut Vec<Tag>) {
|
||||
for tg in Children::new(d).filter(|e| e.id == TAG) {
|
||||
let mut tag = Tag::default();
|
||||
for e in Children::new(tg.data) {
|
||||
match e.id {
|
||||
TARGETS => {
|
||||
for x in Children::new(e.data) {
|
||||
match x.id {
|
||||
TARGET_TYPE_VALUE => tag.targets.type_value = uint(x.data),
|
||||
TARGET_TYPE => tag.targets.type_name = Some(string(x.data)),
|
||||
TAG_TRACK_UID => tag.targets.track_uids.push(uint(x.data)),
|
||||
TAG_EDITION_UID => tag.targets.edition_uids.push(uint(x.data)),
|
||||
TAG_CHAPTER_UID => tag.targets.chapter_uids.push(uint(x.data)),
|
||||
TAG_ATTACHMENT_UID => tag.targets.attachment_uids.push(uint(x.data)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
SIMPLE_TAG => tag.simple.push(parse_simple_tag(e.data, 0)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
out.push(tag);
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_colour(d: &[u8]) -> Colour {
|
||||
let mut c = Colour::default();
|
||||
for e in Children::new(d) {
|
||||
let v = || Some(uint(e.data));
|
||||
match e.id {
|
||||
MATRIX_COEFFICIENTS => c.matrix_coefficients = v(),
|
||||
BITS_PER_CHANNEL => c.bits_per_channel = v(),
|
||||
CHROMA_SUBSAMPLING_HORZ => c.chroma_subsampling_horz = v(),
|
||||
CHROMA_SUBSAMPLING_VERT => c.chroma_subsampling_vert = v(),
|
||||
CB_SUBSAMPLING_HORZ => c.cb_subsampling_horz = v(),
|
||||
CB_SUBSAMPLING_VERT => c.cb_subsampling_vert = v(),
|
||||
CHROMA_SITING_HORZ => c.chroma_siting_horz = v(),
|
||||
CHROMA_SITING_VERT => c.chroma_siting_vert = v(),
|
||||
RANGE => c.range = v(),
|
||||
TRANSFER_CHARACTERISTICS => c.transfer_characteristics = v(),
|
||||
PRIMARIES => c.primaries = v(),
|
||||
MAX_CLL => c.max_cll = v(),
|
||||
MAX_FALL => c.max_fall = v(),
|
||||
MASTERING_METADATA => {
|
||||
let mut m = MasteringMetadata::default();
|
||||
for x in Children::new(e.data) {
|
||||
let f = float(x.data);
|
||||
match x.id {
|
||||
PRIMARY_R_X => m.primaries[0].0 = f,
|
||||
PRIMARY_R_Y => m.primaries[0].1 = f,
|
||||
PRIMARY_G_X => m.primaries[1].0 = f,
|
||||
PRIMARY_G_Y => m.primaries[1].1 = f,
|
||||
PRIMARY_B_X => m.primaries[2].0 = f,
|
||||
PRIMARY_B_Y => m.primaries[2].1 = f,
|
||||
WHITE_POINT_X => m.white_point.0 = f,
|
||||
WHITE_POINT_Y => m.white_point.1 = f,
|
||||
LUMINANCE_MAX => m.luminance_max = f,
|
||||
LUMINANCE_MIN => m.luminance_min = f,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
c.mastering = Some(m);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
c
|
||||
}
|
||||
|
||||
fn parse_video(d: &[u8]) -> VideoInfo {
|
||||
let mut v = VideoInfo::default();
|
||||
for e in Children::new(d) {
|
||||
let u = uint(e.data);
|
||||
match e.id {
|
||||
PIXEL_WIDTH => v.pixel_width = u as u32,
|
||||
PIXEL_HEIGHT => v.pixel_height = u as u32,
|
||||
DISPLAY_WIDTH => v.display_width = Some(u as u32),
|
||||
DISPLAY_HEIGHT => v.display_height = Some(u as u32),
|
||||
DISPLAY_UNIT => v.display_unit = u,
|
||||
PIXEL_CROP_TOP => v.crop.0 = u as u32,
|
||||
PIXEL_CROP_BOTTOM => v.crop.1 = u as u32,
|
||||
PIXEL_CROP_LEFT => v.crop.2 = u as u32,
|
||||
PIXEL_CROP_RIGHT => v.crop.3 = u as u32,
|
||||
FLAG_INTERLACED => v.flag_interlaced = u,
|
||||
FIELD_ORDER => v.field_order = Some(u),
|
||||
STEREO_MODE => v.stereo_mode = Some(u),
|
||||
ALPHA_MODE => v.alpha_mode = u,
|
||||
COLOUR_SPACE => v.colour_space = e.data.try_into().ok(),
|
||||
COLOUR => v.colour = Some(parse_colour(e.data)),
|
||||
PROJECTION => {
|
||||
let mut p = Projection::default();
|
||||
for x in Children::new(e.data) {
|
||||
match x.id {
|
||||
PROJECTION_TYPE => p.projection_type = uint(x.data),
|
||||
PROJECTION_POSE_YAW => p.yaw = float(x.data),
|
||||
PROJECTION_POSE_PITCH => p.pitch = float(x.data),
|
||||
PROJECTION_POSE_ROLL => p.roll = float(x.data),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
v.projection = Some(p);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
fn parse_audio(d: &[u8]) -> AudioInfo {
|
||||
let mut a = AudioInfo::default();
|
||||
for e in Children::new(d) {
|
||||
match e.id {
|
||||
SAMPLING_FREQUENCY => a.sampling_frequency = float(e.data),
|
||||
OUTPUT_SAMPLING_FREQUENCY => a.output_sampling_frequency = Some(float(e.data)),
|
||||
CHANNELS => a.channels = uint(e.data),
|
||||
BIT_DEPTH => a.bit_depth = Some(uint(e.data)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
a
|
||||
}
|
||||
|
||||
fn parse_encodings(d: &[u8]) -> Vec<ContentEncoding> {
|
||||
let mut list: Vec<(u64, ContentEncoding)> = Vec::new();
|
||||
for ce in Children::new(d).filter(|e| e.id == CONTENT_ENCODING) {
|
||||
let (mut order, mut scope, mut ty) = (0u64, 1u64, 0u64);
|
||||
let mut comp: Option<(u64, Vec<u8>)> = None;
|
||||
for e in Children::new(ce.data) {
|
||||
match e.id {
|
||||
CONTENT_ENCODING_ORDER => order = uint(e.data),
|
||||
CONTENT_ENCODING_SCOPE => scope = uint(e.data),
|
||||
CONTENT_ENCODING_TYPE => ty = uint(e.data),
|
||||
CONTENT_COMPRESSION => {
|
||||
let (mut algo, mut settings) = (0u64, Vec::new());
|
||||
for x in Children::new(e.data) {
|
||||
match x.id {
|
||||
CONTENT_COMP_ALGO => algo = uint(x.data),
|
||||
CONTENT_COMP_SETTINGS => settings = x.data.to_vec(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
comp = Some((algo, settings));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let enc = if ty == 1 {
|
||||
ContentEncoding::Encryption { scope }
|
||||
} else {
|
||||
let (algo, settings) = comp.unwrap_or((0, Vec::new()));
|
||||
if algo == 3 {
|
||||
ContentEncoding::HeaderStripping { bytes: settings, scope }
|
||||
} else {
|
||||
ContentEncoding::Compression { algo, settings, scope }
|
||||
}
|
||||
};
|
||||
list.push((order, enc));
|
||||
}
|
||||
// highest order is applied last on muxing → first to undo; keep decode order
|
||||
list.sort_by_key(|e| std::cmp::Reverse(e.0));
|
||||
list.into_iter().map(|e| e.1).collect()
|
||||
}
|
||||
|
||||
pub(crate) fn parse_tracks(d: &[u8], timestamp_scale: u64) -> Vec<Track> {
|
||||
let g = gcd(timestamp_scale, 1_000_000_000);
|
||||
let timebase = (timestamp_scale / g, 1_000_000_000 / g);
|
||||
let mut out = Vec::new();
|
||||
for te in Children::new(d).filter(|e| e.id == TRACK_ENTRY) {
|
||||
let mut t = Track {
|
||||
number: 0,
|
||||
uid: 0,
|
||||
kind: TrackKind::Other(0),
|
||||
enabled: true,
|
||||
default: true,
|
||||
forced: false,
|
||||
hearing_impaired: false,
|
||||
lacing: true,
|
||||
default_duration_ns: None,
|
||||
name: String::new(),
|
||||
language: "eng".into(),
|
||||
language_bcp47: None,
|
||||
codec_id: String::new(),
|
||||
codec_private: Vec::new(),
|
||||
codec_name: String::new(),
|
||||
codec: crate::Codec::Other(String::new()),
|
||||
codec_delay_ns: 0,
|
||||
seek_pre_roll_ns: 0,
|
||||
video: None,
|
||||
audio: None,
|
||||
encodings: Vec::new(),
|
||||
timebase,
|
||||
samples: Vec::new(),
|
||||
sync: Vec::new(),
|
||||
};
|
||||
for e in Children::new(te.data) {
|
||||
match e.id {
|
||||
TRACK_NUMBER => t.number = uint(e.data),
|
||||
TRACK_UID => t.uid = uint(e.data),
|
||||
TRACK_TYPE => t.kind = TrackKind::from_type(uint(e.data)),
|
||||
FLAG_ENABLED => t.enabled = uint(e.data) != 0,
|
||||
FLAG_DEFAULT => t.default = uint(e.data) != 0,
|
||||
FLAG_FORCED => t.forced = uint(e.data) != 0,
|
||||
FLAG_HEARING_IMPAIRED => t.hearing_impaired = uint(e.data) != 0,
|
||||
FLAG_LACING => t.lacing = uint(e.data) != 0,
|
||||
DEFAULT_DURATION => t.default_duration_ns = Some(uint(e.data)).filter(|&v| v > 0),
|
||||
NAME => t.name = string(e.data),
|
||||
LANGUAGE => t.language = string(e.data),
|
||||
LANGUAGE_BCP47 => t.language_bcp47 = Some(string(e.data)),
|
||||
CODEC_ID => t.codec_id = string(e.data),
|
||||
CODEC_PRIVATE => t.codec_private = e.data.to_vec(),
|
||||
CODEC_NAME => t.codec_name = string(e.data),
|
||||
CODEC_DELAY => t.codec_delay_ns = uint(e.data),
|
||||
SEEK_PRE_ROLL => t.seek_pre_roll_ns = uint(e.data),
|
||||
VIDEO => t.video = Some(parse_video(e.data)),
|
||||
AUDIO => t.audio = Some(parse_audio(e.data)),
|
||||
CONTENT_ENCODINGS => t.encodings = parse_encodings(e.data),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if t.number == 0 {
|
||||
continue;
|
||||
}
|
||||
if t.kind == TrackKind::Audio && t.audio.is_none() {
|
||||
t.audio = Some(AudioInfo::default());
|
||||
}
|
||||
// CodecPrivate may itself be header-stripped (scope bit 2 = private data)
|
||||
for enc in &t.encodings {
|
||||
if let ContentEncoding::HeaderStripping { bytes, scope } = enc
|
||||
&& scope & 2 != 0
|
||||
{
|
||||
let mut p = bytes.clone();
|
||||
p.extend_from_slice(&t.codec_private);
|
||||
t.codec_private = p;
|
||||
}
|
||||
}
|
||||
t.codec = map_codec(&t.codec_id, &t.codec_private, t.audio.as_ref());
|
||||
out.push(t);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ebml::{write_id, write_size};
|
||||
|
||||
/// `levels` elements with id `id`, each holding the next; the innermost is empty.
|
||||
fn nested(id: u32, levels: usize) -> Vec<u8> {
|
||||
let mut inner = Vec::new();
|
||||
for _ in 0..levels {
|
||||
let mut e = Vec::new();
|
||||
write_id(&mut e, id);
|
||||
write_size(&mut e, inner.len() as u64, 1);
|
||||
e.extend_from_slice(&inner);
|
||||
inner = e;
|
||||
}
|
||||
inner
|
||||
}
|
||||
|
||||
fn chapter_depth(c: &Chapter) -> usize {
|
||||
1 + c.children.iter().map(chapter_depth).max().unwrap_or(0)
|
||||
}
|
||||
|
||||
fn tag_depth(t: &SimpleTag) -> usize {
|
||||
1 + t.children.iter().map(tag_depth).max().unwrap_or(0)
|
||||
}
|
||||
|
||||
fn chapters_of(levels: usize) -> usize {
|
||||
let mut edition = Vec::new();
|
||||
write_id(&mut edition, EDITION_ENTRY);
|
||||
let atoms = nested(CHAPTER_ATOM, levels);
|
||||
write_size(&mut edition, atoms.len() as u64, 1);
|
||||
edition.extend_from_slice(&atoms);
|
||||
let mut out = Vec::new();
|
||||
parse_chapters(&edition, &mut out);
|
||||
chapter_depth(&out[0].chapters[0])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chapter_nesting_is_bounded() {
|
||||
assert_eq!(chapters_of(MAX_NESTING_DEPTH), MAX_NESTING_DEPTH);
|
||||
assert_eq!(chapters_of(MAX_NESTING_DEPTH + 50), MAX_NESTING_DEPTH);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_tag_nesting_is_bounded() {
|
||||
for (levels, expected) in [(MAX_NESTING_DEPTH, MAX_NESTING_DEPTH), (MAX_NESTING_DEPTH + 50, MAX_NESTING_DEPTH)] {
|
||||
let mut tag = Vec::new();
|
||||
write_id(&mut tag, TAG);
|
||||
let simple = nested(SIMPLE_TAG, levels);
|
||||
write_size(&mut tag, simple.len() as u64, 1);
|
||||
tag.extend_from_slice(&simple);
|
||||
let mut out = Vec::new();
|
||||
parse_tags(&tag, &mut out);
|
||||
assert_eq!(tag_depth(&out[0].simple[0]), expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,539 +0,0 @@
|
||||
//! Minimal Matroska / WebM muxer: SimpleBlocks (optionally laced), BlockGroups for explicit
|
||||
//! durations, Cues, SeekHead, Info with Duration. Output must be `Write + Seek` (sizes and the
|
||||
//! SeekHead are back-patched on [`MkvWriter::finish`]).
|
||||
|
||||
use std::io::{Seek, SeekFrom, Write};
|
||||
|
||||
use crate::ebml::{write_id, write_size, write_unknown_size};
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ids::*;
|
||||
use crate::track::TrackKind;
|
||||
|
||||
/// Lacing used for audio tracks.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum LacingMode {
|
||||
None,
|
||||
Xiph,
|
||||
Ebml,
|
||||
/// Fixed-size lacing when consecutive frames have equal sizes, otherwise EBML lacing.
|
||||
FixedOrEbml,
|
||||
}
|
||||
|
||||
/// Track description for [`MkvWriter`].
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TrackSpec {
|
||||
pub kind: TrackKind,
|
||||
pub codec_id: String,
|
||||
pub codec_private: Vec<u8>,
|
||||
pub default_duration_ns: Option<u64>,
|
||||
pub language: Option<String>,
|
||||
pub name: Option<String>,
|
||||
/// Pixel width/height for video tracks.
|
||||
pub video_size: Option<(u32, u32)>,
|
||||
/// (sampling frequency, channels, bit depth) for audio tracks.
|
||||
pub audio: Option<(f64, u64, Option<u64>)>,
|
||||
pub codec_delay_ns: u64,
|
||||
pub seek_pre_roll_ns: u64,
|
||||
}
|
||||
|
||||
impl TrackSpec {
|
||||
pub fn new(kind: TrackKind, codec_id: impl Into<String>) -> Self {
|
||||
Self {
|
||||
kind,
|
||||
codec_id: codec_id.into(),
|
||||
codec_private: Vec::new(),
|
||||
default_duration_ns: None,
|
||||
language: None,
|
||||
name: None,
|
||||
video_size: None,
|
||||
audio: None,
|
||||
codec_delay_ns: 0,
|
||||
seek_pre_roll_ns: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Muxer options.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MuxOptions {
|
||||
/// `webm` or `matroska`.
|
||||
pub doc_type: String,
|
||||
/// Nanoseconds per tick (default 1 000 000).
|
||||
pub timestamp_scale: u64,
|
||||
/// Start a new cluster at the next video keyframe (or any frame without video) after this long.
|
||||
pub cluster_duration_ns: u64,
|
||||
pub lacing: LacingMode,
|
||||
/// Maximum frames per laced block.
|
||||
pub max_lace_frames: usize,
|
||||
pub title: Option<String>,
|
||||
pub writing_app: String,
|
||||
}
|
||||
|
||||
impl Default for MuxOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
doc_type: "matroska".into(),
|
||||
timestamp_scale: 1_000_000,
|
||||
cluster_duration_ns: 1_000_000_000,
|
||||
lacing: LacingMode::None,
|
||||
max_lace_frames: 8,
|
||||
title: None,
|
||||
writing_app: "deckcraft".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn el(out: &mut Vec<u8>, id: u32, data: &[u8]) {
|
||||
write_id(out, id);
|
||||
write_size(out, data.len() as u64, 1);
|
||||
out.extend_from_slice(data);
|
||||
}
|
||||
|
||||
pub(crate) fn el_uint(out: &mut Vec<u8>, id: u32, v: u64) {
|
||||
let n = (8 - v.leading_zeros() as usize / 8).max(1);
|
||||
el(out, id, &v.to_be_bytes()[8 - n..]);
|
||||
}
|
||||
|
||||
pub(crate) fn el_float(out: &mut Vec<u8>, id: u32, v: f64) {
|
||||
el(out, id, &v.to_be_bytes());
|
||||
}
|
||||
|
||||
pub(crate) fn el_str(out: &mut Vec<u8>, id: u32, s: &str) {
|
||||
el(out, id, s.as_bytes());
|
||||
}
|
||||
|
||||
fn void(out: &mut Vec<u8>, total: usize) {
|
||||
debug_assert!(total >= 2);
|
||||
let sl = if total - 2 <= 126 { 1 } else { 8 };
|
||||
write_id(out, VOID);
|
||||
write_size(out, (total - 1 - sl) as u64, sl);
|
||||
out.resize(out.len() + total - 1 - sl, 0);
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Pending {
|
||||
track: usize,
|
||||
ts: i64,
|
||||
frames: Vec<Vec<u8>>,
|
||||
}
|
||||
|
||||
const SEEK_HEAD_SPACE: usize = 128;
|
||||
|
||||
/// Streaming Matroska/WebM writer.
|
||||
pub struct MkvWriter<W: Write + Seek> {
|
||||
w: W,
|
||||
opts: MuxOptions,
|
||||
tracks: Vec<TrackSpec>,
|
||||
segment_size_pos: u64,
|
||||
segment_data_start: u64,
|
||||
seek_head_pos: u64,
|
||||
duration_pos: u64,
|
||||
info_pos: u64,
|
||||
tracks_pos: u64,
|
||||
cluster: Vec<u8>,
|
||||
cluster_ts: Option<i64>,
|
||||
cluster_start_ns: i64,
|
||||
pending: Option<Pending>,
|
||||
cues: Vec<(u64, u64, u64)>, // (time ticks, track number, cluster position)
|
||||
cued_tracks_in_cluster: Vec<bool>,
|
||||
end_ns: i64,
|
||||
has_video: bool,
|
||||
}
|
||||
|
||||
impl<W: Write + Seek> MkvWriter<W> {
|
||||
pub fn new(mut w: W, tracks: Vec<TrackSpec>, opts: MuxOptions) -> Result<Self> {
|
||||
if tracks.is_empty() || opts.timestamp_scale == 0 {
|
||||
return Err(Error::Invalid("muxer needs at least one track and a non-zero timestamp scale".into()));
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
let mut h = Vec::new();
|
||||
el_uint(&mut h, EBML_VERSION, 1);
|
||||
el_uint(&mut h, EBML_READ_VERSION, 1);
|
||||
el_uint(&mut h, EBML_MAX_ID_LENGTH, 4);
|
||||
el_uint(&mut h, EBML_MAX_SIZE_LENGTH, 8);
|
||||
el_str(&mut h, DOC_TYPE, &opts.doc_type);
|
||||
el_uint(&mut h, DOC_TYPE_VERSION, 4);
|
||||
el_uint(&mut h, DOC_TYPE_READ_VERSION, 2);
|
||||
el(&mut out, EBML, &h);
|
||||
write_id(&mut out, SEGMENT);
|
||||
let base = w.stream_position()?;
|
||||
let segment_size_pos = base + out.len() as u64;
|
||||
write_unknown_size(&mut out, 8);
|
||||
let segment_data_start = base + out.len() as u64;
|
||||
let seek_head_pos = segment_data_start;
|
||||
void(&mut out, SEEK_HEAD_SPACE);
|
||||
// Info
|
||||
let info_pos = base + out.len() as u64;
|
||||
let mut info = Vec::new();
|
||||
el_uint(&mut info, TIMESTAMP_SCALE, opts.timestamp_scale);
|
||||
el_str(&mut info, MUXING_APP, "deckcraft-matroska");
|
||||
el_str(&mut info, WRITING_APP, &opts.writing_app);
|
||||
if let Some(t) = &opts.title {
|
||||
el_str(&mut info, TITLE, t);
|
||||
}
|
||||
let dur_off_in_info = info.len() + 2 + 1; // ID (2) + size (1)
|
||||
el_float(&mut info, DURATION, 0.0);
|
||||
write_id(&mut out, INFO);
|
||||
write_size(&mut out, info.len() as u64, 1);
|
||||
let duration_pos = base + out.len() as u64 + dur_off_in_info as u64;
|
||||
out.extend_from_slice(&info);
|
||||
// Tracks
|
||||
let tracks_pos = base + out.len() as u64;
|
||||
let mut tr = Vec::new();
|
||||
for (i, t) in tracks.iter().enumerate() {
|
||||
let mut e = Vec::new();
|
||||
el_uint(&mut e, TRACK_NUMBER, i as u64 + 1);
|
||||
el_uint(&mut e, TRACK_UID, 0x1000 + i as u64 + 1);
|
||||
let ty = match t.kind {
|
||||
TrackKind::Video => 1,
|
||||
TrackKind::Audio => 2,
|
||||
TrackKind::Subtitle => 0x11,
|
||||
TrackKind::Complex => 3,
|
||||
TrackKind::Logo => 0x10,
|
||||
TrackKind::Buttons => 0x12,
|
||||
TrackKind::Control => 0x20,
|
||||
TrackKind::Metadata => 0x21,
|
||||
TrackKind::Other(n) => n as u64,
|
||||
};
|
||||
el_uint(&mut e, TRACK_TYPE, ty);
|
||||
el_uint(&mut e, FLAG_LACING, (t.kind == TrackKind::Audio && opts.lacing != LacingMode::None) as u64);
|
||||
if let Some(l) = &t.language {
|
||||
el_str(&mut e, LANGUAGE, l);
|
||||
}
|
||||
if let Some(n) = &t.name {
|
||||
el_str(&mut e, NAME, n);
|
||||
}
|
||||
el_str(&mut e, CODEC_ID, &t.codec_id);
|
||||
if !t.codec_private.is_empty() {
|
||||
el(&mut e, CODEC_PRIVATE, &t.codec_private);
|
||||
}
|
||||
if let Some(d) = t.default_duration_ns {
|
||||
el_uint(&mut e, DEFAULT_DURATION, d);
|
||||
}
|
||||
if t.codec_delay_ns > 0 {
|
||||
el_uint(&mut e, CODEC_DELAY, t.codec_delay_ns);
|
||||
}
|
||||
if t.seek_pre_roll_ns > 0 {
|
||||
el_uint(&mut e, SEEK_PRE_ROLL, t.seek_pre_roll_ns);
|
||||
}
|
||||
if let Some((pw, ph)) = t.video_size {
|
||||
let mut v = Vec::new();
|
||||
el_uint(&mut v, PIXEL_WIDTH, pw as u64);
|
||||
el_uint(&mut v, PIXEL_HEIGHT, ph as u64);
|
||||
el(&mut e, VIDEO, &v);
|
||||
}
|
||||
if let Some((rate, ch, bits)) = t.audio {
|
||||
let mut a = Vec::new();
|
||||
el_float(&mut a, SAMPLING_FREQUENCY, rate);
|
||||
el_uint(&mut a, CHANNELS, ch);
|
||||
if let Some(b) = bits {
|
||||
el_uint(&mut a, BIT_DEPTH, b);
|
||||
}
|
||||
el(&mut e, AUDIO, &a);
|
||||
}
|
||||
el(&mut tr, TRACK_ENTRY, &e);
|
||||
}
|
||||
el(&mut out, TRACKS, &tr);
|
||||
w.write_all(&out)?;
|
||||
let has_video = tracks.iter().any(|t| t.kind == TrackKind::Video);
|
||||
let n = tracks.len();
|
||||
Ok(Self {
|
||||
w,
|
||||
opts,
|
||||
tracks,
|
||||
segment_size_pos,
|
||||
segment_data_start,
|
||||
seek_head_pos,
|
||||
duration_pos,
|
||||
info_pos,
|
||||
tracks_pos,
|
||||
cluster: Vec::new(),
|
||||
cluster_ts: None,
|
||||
cluster_start_ns: 0,
|
||||
pending: None,
|
||||
cues: Vec::new(),
|
||||
cued_tracks_in_cluster: vec![false; n],
|
||||
end_ns: 0,
|
||||
has_video,
|
||||
})
|
||||
}
|
||||
|
||||
/// Add one frame. `pts_ns` is the presentation time (codec delay is added back for storage);
|
||||
/// frames must be supplied in the desired file order. `duration_ns` forces a BlockGroup with a
|
||||
/// BlockDuration when it differs from the track's DefaultDuration.
|
||||
pub fn write_frame(&mut self, track: usize, pts_ns: i64, keyframe: bool, data: &[u8], duration_ns: Option<u64>) -> Result<()> {
|
||||
let spec = self.tracks.get(track).ok_or(Error::NoSuchTrack(track))?;
|
||||
// the Matroska ProRes mapping stores frames without the 8-byte `size`+`icpf` header
|
||||
let data = if spec.codec_id == "V_PRORES" && data.get(4..8) == Some(b"icpf") { &data[8..] } else { data };
|
||||
let scale = self.opts.timestamp_scale as i64;
|
||||
let stored_ns = pts_ns + spec.codec_delay_ns as i64;
|
||||
if stored_ns < 0 {
|
||||
return Err(Error::Invalid("negative timestamp".into()));
|
||||
}
|
||||
let ts = (stored_ns + scale / 2) / scale;
|
||||
let dur_end = pts_ns + duration_ns.or(spec.default_duration_ns).unwrap_or(0) as i64;
|
||||
self.end_ns = self.end_ns.max(dur_end).max(pts_ns);
|
||||
let is_video = spec.kind == TrackKind::Video;
|
||||
let lacing = spec.kind == TrackKind::Audio && self.opts.lacing != LacingMode::None;
|
||||
let explicit_dur = duration_ns.filter(|&d| Some(d) != spec.default_duration_ns);
|
||||
// new cluster?
|
||||
let start_new = match self.cluster_ts {
|
||||
None => true,
|
||||
Some(cts) => {
|
||||
let long = stored_ns - self.cluster_start_ns >= self.opts.cluster_duration_ns as i64;
|
||||
(long && (!self.has_video || (is_video && keyframe))) || ts - cts > 30_000 || ts < cts
|
||||
}
|
||||
};
|
||||
if start_new {
|
||||
self.flush_cluster()?;
|
||||
self.cluster_ts = Some(ts);
|
||||
self.cluster_start_ns = stored_ns;
|
||||
el_uint(&mut self.cluster, TIMESTAMP, ts as u64);
|
||||
}
|
||||
let cts = self.cluster_ts.unwrap_or(0);
|
||||
// cue: video keyframes, or the first frame of each track per cluster when there is no video
|
||||
if (is_video && keyframe) || (!self.has_video && !self.cued_tracks_in_cluster[track]) {
|
||||
self.cued_tracks_in_cluster[track] = true;
|
||||
let pos = self.w.stream_position()? - self.segment_data_start;
|
||||
if self.cues.last().is_none_or(|c| c.2 != pos || c.1 != track as u64 + 1) {
|
||||
self.cues.push((ts as u64, track as u64 + 1, pos));
|
||||
}
|
||||
}
|
||||
if lacing && explicit_dur.is_none() {
|
||||
let same = self.pending.as_ref().is_some_and(|p| p.track == track && p.frames.len() < self.opts.max_lace_frames);
|
||||
if !same {
|
||||
self.flush_pending();
|
||||
}
|
||||
match &mut self.pending {
|
||||
Some(p) => p.frames.push(data.to_vec()),
|
||||
None => self.pending = Some(Pending { track, ts: ts - cts, frames: vec![data.to_vec()] }),
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
self.flush_pending();
|
||||
let mut block = Vec::new();
|
||||
write_size(&mut block, track as u64 + 1, 1);
|
||||
block.extend_from_slice(&((ts - cts) as i16).to_be_bytes());
|
||||
if let Some(d) = explicit_dur {
|
||||
block.push(0);
|
||||
block.extend_from_slice(data);
|
||||
let mut g = Vec::new();
|
||||
el(&mut g, BLOCK, &block);
|
||||
el_uint(&mut g, BLOCK_DURATION, (d as i64 / scale) as u64);
|
||||
if !keyframe {
|
||||
el(&mut g, REFERENCE_BLOCK, &[0xFF]);
|
||||
}
|
||||
el(&mut self.cluster, BLOCK_GROUP, &g);
|
||||
} else {
|
||||
block.push(if keyframe { 0x80 } else { 0 });
|
||||
block.extend_from_slice(data);
|
||||
el(&mut self.cluster, SIMPLE_BLOCK, &block);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn flush_pending(&mut self) {
|
||||
let Some(p) = self.pending.take() else { return };
|
||||
let mut block = Vec::new();
|
||||
write_size(&mut block, p.track as u64 + 1, 1);
|
||||
block.extend_from_slice(&(p.ts as i16).to_be_bytes());
|
||||
let n = p.frames.len();
|
||||
let mode = if n == 1 { LacingMode::None } else { self.opts.lacing };
|
||||
let fixed = p.frames.iter().all(|f| f.len() == p.frames[0].len());
|
||||
match mode {
|
||||
LacingMode::None => block.push(0x80),
|
||||
LacingMode::Xiph => {
|
||||
block.push(0x80 | 0x02);
|
||||
block.push((n - 1) as u8);
|
||||
for f in &p.frames[..n - 1] {
|
||||
let mut s = f.len();
|
||||
while s >= 255 {
|
||||
block.push(255);
|
||||
s -= 255;
|
||||
}
|
||||
block.push(s as u8);
|
||||
}
|
||||
}
|
||||
LacingMode::FixedOrEbml if fixed => {
|
||||
block.push(0x80 | 0x04);
|
||||
block.push((n - 1) as u8);
|
||||
}
|
||||
LacingMode::Ebml | LacingMode::FixedOrEbml => {
|
||||
block.push(0x80 | 0x06);
|
||||
block.push((n - 1) as u8);
|
||||
write_size(&mut block, p.frames[0].len() as u64, 1);
|
||||
for i in 1..n - 1 {
|
||||
let d = p.frames[i].len() as i64 - p.frames[i - 1].len() as i64;
|
||||
// signed VINT: smallest length whose range holds d
|
||||
let mut len = 1;
|
||||
while len < 8 && d.unsigned_abs() >= (1u64 << (7 * len - 1)) - 1 {
|
||||
len += 1;
|
||||
}
|
||||
let v = (d + (1i64 << (7 * len - 1)) - 1) as u64 | (1u64 << (7 * len));
|
||||
block.extend_from_slice(&v.to_be_bytes()[8 - len..]);
|
||||
}
|
||||
}
|
||||
}
|
||||
for f in &p.frames {
|
||||
block.extend_from_slice(f);
|
||||
}
|
||||
el(&mut self.cluster, SIMPLE_BLOCK, &block);
|
||||
}
|
||||
|
||||
fn flush_cluster(&mut self) -> Result<()> {
|
||||
self.flush_pending();
|
||||
if self.cluster_ts.is_some() {
|
||||
let mut out = Vec::new();
|
||||
el(&mut out, CLUSTER, &self.cluster);
|
||||
self.w.write_all(&out)?;
|
||||
}
|
||||
self.cluster.clear();
|
||||
self.cluster_ts = None;
|
||||
self.cued_tracks_in_cluster.iter_mut().for_each(|c| *c = false);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write Cues, back-patch SeekHead / Segment size / Duration, and return the writer.
|
||||
pub fn finish(mut self) -> Result<W> {
|
||||
self.flush_cluster()?;
|
||||
let cues_pos = self.w.stream_position()?;
|
||||
let mut cues = Vec::new();
|
||||
for &(time, track, pos) in &self.cues {
|
||||
let mut ctp = Vec::new();
|
||||
el_uint(&mut ctp, CUE_TRACK, track);
|
||||
el_uint(&mut ctp, CUE_CLUSTER_POSITION, pos);
|
||||
let mut cp = Vec::new();
|
||||
el_uint(&mut cp, CUE_TIME, time);
|
||||
el(&mut cp, CUE_TRACK_POSITIONS, &ctp);
|
||||
el(&mut cues, CUE_POINT, &cp);
|
||||
}
|
||||
let has_cues = !cues.is_empty();
|
||||
if has_cues {
|
||||
let mut out = Vec::new();
|
||||
el(&mut out, CUES, &cues);
|
||||
self.w.write_all(&out)?;
|
||||
}
|
||||
let end = self.w.stream_position()?;
|
||||
// SeekHead
|
||||
let mut sh = Vec::new();
|
||||
let mut entries = vec![(INFO, self.info_pos), (TRACKS, self.tracks_pos)];
|
||||
if has_cues {
|
||||
entries.push((CUES, cues_pos));
|
||||
}
|
||||
for (id, pos) in entries {
|
||||
let mut s = Vec::new();
|
||||
let mut idb = Vec::new();
|
||||
write_id(&mut idb, id);
|
||||
el(&mut s, SEEK_ID, &idb);
|
||||
el_uint(&mut s, SEEK_POSITION, pos - self.segment_data_start);
|
||||
el(&mut sh, SEEK, &s);
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
el(&mut out, SEEK_HEAD, &sh);
|
||||
if out.len() + 2 > SEEK_HEAD_SPACE {
|
||||
return Err(Error::Invalid("SeekHead does not fit its reserved space".into()));
|
||||
}
|
||||
let rest = SEEK_HEAD_SPACE - out.len();
|
||||
void(&mut out, rest);
|
||||
self.w.seek(SeekFrom::Start(self.seek_head_pos))?;
|
||||
self.w.write_all(&out)?;
|
||||
// Duration
|
||||
let dur = self.end_ns as f64 / self.opts.timestamp_scale as f64;
|
||||
self.w.seek(SeekFrom::Start(self.duration_pos))?;
|
||||
self.w.write_all(&dur.to_be_bytes())?;
|
||||
// Segment size
|
||||
let mut sz = Vec::new();
|
||||
write_size(&mut sz, end - self.segment_data_start, 8);
|
||||
self.w.seek(SeekFrom::Start(self.segment_size_pos))?;
|
||||
self.w.write_all(&sz)?;
|
||||
self.w.seek(SeekFrom::Start(end))?;
|
||||
self.w.flush()?;
|
||||
Ok(self.w)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{Demuxer, TrackKind};
|
||||
use std::io::Cursor;
|
||||
|
||||
fn roundtrip(lacing: LacingMode) {
|
||||
let mut v = TrackSpec::new(TrackKind::Video, "V_MJPEG");
|
||||
v.video_size = Some((16, 16));
|
||||
v.default_duration_ns = Some(40_000_000);
|
||||
let mut a = TrackSpec::new(TrackKind::Audio, "A_PCM/INT/LIT");
|
||||
a.audio = Some((48000.0, 1, Some(16)));
|
||||
a.default_duration_ns = Some(10_000_000);
|
||||
let mut s = TrackSpec::new(TrackKind::Subtitle, "S_TEXT/UTF8");
|
||||
s.language = Some("fre".into());
|
||||
let opts = MuxOptions { lacing, ..Default::default() };
|
||||
let mut m = MkvWriter::new(Cursor::new(Vec::new()), vec![v, a, s], opts).unwrap();
|
||||
let mut expect = Vec::new();
|
||||
for i in 0..50i64 {
|
||||
let vf = vec![i as u8; 100 + i as usize];
|
||||
m.write_frame(0, i * 40_000_000, i % 10 == 0, &vf, None).unwrap();
|
||||
expect.push((0usize, i * 40, vf, i % 10 == 0));
|
||||
for k in 0..4 {
|
||||
let n = if lacing == LacingMode::FixedOrEbml { 960 } else { 900 + (i as usize * 7 + k * 13) % 300 };
|
||||
let af = vec![(i * 4 + k as i64) as u8; n];
|
||||
let t = i * 40 + k as i64 * 10;
|
||||
m.write_frame(1, t * 1_000_000, true, &af, None).unwrap();
|
||||
expect.push((1, t, af, true));
|
||||
}
|
||||
if i % 20 == 5 {
|
||||
m.write_frame(2, i * 40_000_000, true, b"hello", Some(500_000_000)).unwrap();
|
||||
expect.push((2, i * 40, b"hello".to_vec(), true));
|
||||
}
|
||||
}
|
||||
let bytes = m.finish().unwrap().into_inner();
|
||||
let mut d = Demuxer::from_slice(&bytes).unwrap();
|
||||
assert!(d.file().warnings.is_empty(), "{:?}", d.file().warnings);
|
||||
assert_eq!(d.tracks()[2].language, "fre");
|
||||
assert!(!d.file().cues.is_empty());
|
||||
assert_eq!(d.duration_ns(), Some(2_300_000_000));
|
||||
let got: Vec<_> = d.by_ref().map(|p| p.unwrap()).collect();
|
||||
assert_eq!(got.len(), expect.len());
|
||||
for (g, e) in got.iter().zip(&expect) {
|
||||
assert_eq!((g.track, g.pts, &g.data, g.keyframe), (e.0, e.1, &e.2, e.3));
|
||||
}
|
||||
let sub = got.iter().find(|p| p.track == 2).unwrap();
|
||||
assert_eq!(sub.duration, 500);
|
||||
// the index agrees with the packet stream
|
||||
let f = d.file();
|
||||
for (ti, t) in f.tracks.iter().enumerate() {
|
||||
let pk: Vec<_> = got.iter().filter(|p| p.track == ti).collect();
|
||||
assert_eq!(t.samples.len(), pk.len());
|
||||
for (i, p) in pk.iter().enumerate() {
|
||||
assert_eq!(f.read_sample(&bytes[..], ti, i).unwrap(), p.data);
|
||||
assert_eq!(t.samples[i].pts, p.pts);
|
||||
}
|
||||
}
|
||||
// seeking
|
||||
let sp = d.seek(0, 1_230_000_000).unwrap();
|
||||
assert_eq!(sp.pts, 1200);
|
||||
let p = d.next_packet().unwrap().unwrap();
|
||||
assert_eq!((p.track, p.pts, p.keyframe), (0, 1200, true));
|
||||
let sp = d.seek(1, 1_235_000_000).unwrap();
|
||||
assert_eq!(sp.pts, 1230);
|
||||
let p = d.next_packet().unwrap().unwrap();
|
||||
assert_eq!((p.track, p.pts), (1, 1230));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip_unlaced() {
|
||||
roundtrip(LacingMode::None);
|
||||
}
|
||||
#[test]
|
||||
fn roundtrip_xiph() {
|
||||
roundtrip(LacingMode::Xiph);
|
||||
}
|
||||
#[test]
|
||||
fn roundtrip_ebml() {
|
||||
roundtrip(LacingMode::Ebml);
|
||||
}
|
||||
#[test]
|
||||
fn roundtrip_fixed() {
|
||||
roundtrip(LacingMode::FixedOrEbml);
|
||||
}
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
//! Random-access byte sources for the demuxer.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::io::{self, Read, Seek, SeekFrom};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Random-access, read-only byte source (file, in-memory buffer, web Blob…).
|
||||
///
|
||||
/// Same shape as `deckcraft_isobmff::ByteSource`, so one adapter can serve both demuxers.
|
||||
pub trait ByteSource {
|
||||
/// Total length in bytes.
|
||||
fn len(&self) -> u64;
|
||||
/// Fill `buf` entirely with bytes starting at `offset`; fail with `UnexpectedEof` if past the end.
|
||||
fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()>;
|
||||
/// True if the source holds no bytes.
|
||||
fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
}
|
||||
|
||||
fn eof() -> io::Error {
|
||||
io::Error::new(io::ErrorKind::UnexpectedEof, "read past end of byte source")
|
||||
}
|
||||
|
||||
impl ByteSource for [u8] {
|
||||
fn len(&self) -> u64 {
|
||||
<[u8]>::len(self) as u64
|
||||
}
|
||||
fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()> {
|
||||
let start = usize::try_from(offset).map_err(|_| eof())?;
|
||||
let end = start.checked_add(buf.len()).ok_or_else(eof)?;
|
||||
let src = self.get(start..end).ok_or_else(eof)?;
|
||||
buf.copy_from_slice(src);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ByteSource for Vec<u8> {
|
||||
fn len(&self) -> u64 {
|
||||
self.as_slice().len() as u64
|
||||
}
|
||||
fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()> {
|
||||
self.as_slice().read_at(offset, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ByteSource + ?Sized> ByteSource for &T {
|
||||
fn len(&self) -> u64 {
|
||||
(**self).len()
|
||||
}
|
||||
fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()> {
|
||||
(**self).read_at(offset, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ByteSource + ?Sized> ByteSource for Arc<T> {
|
||||
fn len(&self) -> u64 {
|
||||
(**self).len()
|
||||
}
|
||||
fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()> {
|
||||
(**self).read_at(offset, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ByteSource + ?Sized> ByteSource for Box<T> {
|
||||
fn len(&self) -> u64 {
|
||||
(**self).len()
|
||||
}
|
||||
fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()> {
|
||||
(**self).read_at(offset, buf)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl ByteSource for std::fs::File {
|
||||
fn len(&self) -> u64 {
|
||||
self.metadata().map(|m| m.len()).unwrap_or(0)
|
||||
}
|
||||
#[cfg(unix)]
|
||||
fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()> {
|
||||
std::os::unix::fs::FileExt::read_exact_at(self, buf, offset)
|
||||
}
|
||||
#[cfg(windows)]
|
||||
fn read_at(&self, mut offset: u64, mut buf: &mut [u8]) -> io::Result<()> {
|
||||
use std::os::windows::fs::FileExt;
|
||||
while !buf.is_empty() {
|
||||
let n = self.seek_read(buf, offset)?;
|
||||
if n == 0 {
|
||||
return Err(eof());
|
||||
}
|
||||
offset += n as u64;
|
||||
buf = &mut buf[n..];
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(not(any(unix, windows)))]
|
||||
fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()> {
|
||||
let mut f = self;
|
||||
f.seek(SeekFrom::Start(offset))?;
|
||||
f.read_exact(buf)
|
||||
}
|
||||
}
|
||||
|
||||
/// Adapts any `Read + Seek` (e.g. `BufReader<File>`, `Cursor<Vec<u8>>`) into a [`ByteSource`].
|
||||
///
|
||||
/// The length is taken once at construction; the stream position is tracked so sequential reads
|
||||
/// do not issue redundant seeks.
|
||||
pub struct ReadSeekSource<R> {
|
||||
inner: RefCell<(R, u64)>,
|
||||
len: u64,
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> ReadSeekSource<R> {
|
||||
/// Wrap a reader; seeks to the end once to learn the length.
|
||||
pub fn new(mut r: R) -> io::Result<Self> {
|
||||
let len = r.seek(SeekFrom::End(0))?;
|
||||
Ok(Self { inner: RefCell::new((r, len)), len })
|
||||
}
|
||||
/// Unwrap the reader.
|
||||
pub fn into_inner(self) -> R {
|
||||
self.inner.into_inner().0
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> ByteSource for ReadSeekSource<R> {
|
||||
fn len(&self) -> u64 {
|
||||
self.len
|
||||
}
|
||||
fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()> {
|
||||
if offset.checked_add(buf.len() as u64).is_none_or(|e| e > self.len) {
|
||||
return Err(eof());
|
||||
}
|
||||
let mut g = self.inner.borrow_mut();
|
||||
let (r, pos) = &mut *g;
|
||||
if *pos != offset {
|
||||
r.seek(SeekFrom::Start(offset))?;
|
||||
*pos = offset;
|
||||
}
|
||||
let res = r.read_exact(buf);
|
||||
match res {
|
||||
Ok(()) => {
|
||||
*pos = offset + buf.len() as u64;
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
*pos = u64::MAX; // unknown; force a seek next time
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn slice_and_reader_sources() {
|
||||
let v = vec![1u8, 2, 3, 4];
|
||||
let mut b = [0u8; 2];
|
||||
v.read_at(1, &mut b).unwrap();
|
||||
assert_eq!(b, [2, 3]);
|
||||
assert!(v.read_at(3, &mut b).is_err());
|
||||
assert!(v.read_at(u64::MAX, &mut b).is_err());
|
||||
let r = ReadSeekSource::new(io::Cursor::new(v.clone())).unwrap();
|
||||
assert_eq!(r.len(), 4);
|
||||
r.read_at(2, &mut b).unwrap();
|
||||
assert_eq!(b, [3, 4]);
|
||||
r.read_at(0, &mut b).unwrap();
|
||||
assert_eq!(b, [1, 2]);
|
||||
assert!(r.read_at(3, &mut b).is_err());
|
||||
}
|
||||
}
|
||||
@@ -1,254 +0,0 @@
|
||||
//! Hand-built files exercising features FFmpeg does not write: unknown-size Segment/Clusters,
|
||||
//! header stripping, BlockGroups, Colour + mastering metadata, Chapters, Tags, Attachments, CRC-32.
|
||||
|
||||
use crate::codec::Codec;
|
||||
use crate::ebml::{self, write_id, write_unknown_size};
|
||||
use crate::ids::*;
|
||||
use crate::mux::{el, el_float, el_str, el_uint};
|
||||
use crate::track::ContentEncoding;
|
||||
use crate::{Demuxer, OpenOptions, Packet, open_with};
|
||||
|
||||
fn block(track: u8, rel: i16, flags: u8, data: &[u8]) -> Vec<u8> {
|
||||
let mut b = vec![0x80 | track];
|
||||
b.extend_from_slice(&rel.to_be_bytes());
|
||||
b.push(flags);
|
||||
b.extend_from_slice(data);
|
||||
b
|
||||
}
|
||||
|
||||
fn synthetic() -> Vec<u8> {
|
||||
let mut f = Vec::new();
|
||||
let mut h = Vec::new();
|
||||
el_str(&mut h, DOC_TYPE, "matroska");
|
||||
el_uint(&mut h, DOC_TYPE_VERSION, 4);
|
||||
el(&mut f, EBML, &h);
|
||||
write_id(&mut f, SEGMENT);
|
||||
write_unknown_size(&mut f, 8);
|
||||
// Info with a correct CRC-32 and a Void
|
||||
let mut info = Vec::new();
|
||||
el_uint(&mut info, TIMESTAMP_SCALE, 1_000_000);
|
||||
el_str(&mut info, TITLE, "Synthetic");
|
||||
el(&mut info, VOID, &[0; 3]);
|
||||
let mut crc_info = Vec::new();
|
||||
el(&mut crc_info, CRC32, &ebml::crc32(&info).to_le_bytes());
|
||||
crc_info.extend_from_slice(&info);
|
||||
el(&mut f, INFO, &crc_info);
|
||||
// Tracks
|
||||
let mut v = Vec::new();
|
||||
el_uint(&mut v, TRACK_NUMBER, 1);
|
||||
el_uint(&mut v, TRACK_UID, 11);
|
||||
el_uint(&mut v, TRACK_TYPE, 1);
|
||||
el_str(&mut v, CODEC_ID, "V_MPEG4/ISO/AVC");
|
||||
el(&mut v, CODEC_PRIVATE, &[1, 0x64, 0, 0x1F]);
|
||||
el_uint(&mut v, DEFAULT_DURATION, 40_000_000);
|
||||
let mut mm = Vec::new();
|
||||
for (id, val) in
|
||||
[(PRIMARY_R_X, 0.708), (PRIMARY_R_Y, 0.292), (PRIMARY_G_X, 0.17), (PRIMARY_G_Y, 0.797), (PRIMARY_B_X, 0.131), (PRIMARY_B_Y, 0.046)]
|
||||
{
|
||||
el_float(&mut mm, id, val);
|
||||
}
|
||||
el_float(&mut mm, WHITE_POINT_X, 0.3127);
|
||||
el_float(&mut mm, WHITE_POINT_Y, 0.329);
|
||||
el_float(&mut mm, LUMINANCE_MAX, 1000.0);
|
||||
el_float(&mut mm, LUMINANCE_MIN, 0.0001);
|
||||
let mut col = Vec::new();
|
||||
el_uint(&mut col, MATRIX_COEFFICIENTS, 9);
|
||||
el_uint(&mut col, RANGE, 2);
|
||||
el_uint(&mut col, TRANSFER_CHARACTERISTICS, 16);
|
||||
el_uint(&mut col, PRIMARIES, 9);
|
||||
el_uint(&mut col, MAX_CLL, 1000);
|
||||
el_uint(&mut col, MAX_FALL, 400);
|
||||
el(&mut col, MASTERING_METADATA, &mm);
|
||||
let mut vid = Vec::new();
|
||||
el_uint(&mut vid, PIXEL_WIDTH, 1440);
|
||||
el_uint(&mut vid, PIXEL_HEIGHT, 1080);
|
||||
el_uint(&mut vid, DISPLAY_WIDTH, 1920);
|
||||
el_uint(&mut vid, DISPLAY_HEIGHT, 1080);
|
||||
el(&mut vid, COLOUR, &col);
|
||||
// rectangular, a quarter turn clockwise (portrait phone video)
|
||||
let mut proj = Vec::new();
|
||||
el_uint(&mut proj, PROJECTION_TYPE, 0);
|
||||
el_float(&mut proj, PROJECTION_POSE_ROLL, -90.0);
|
||||
el(&mut vid, PROJECTION, &proj);
|
||||
el(&mut v, VIDEO, &vid);
|
||||
// header stripping: every frame starts with 00 00 00 01
|
||||
let mut comp = Vec::new();
|
||||
el_uint(&mut comp, CONTENT_COMP_ALGO, 3);
|
||||
el(&mut comp, CONTENT_COMP_SETTINGS, &[0, 0, 0, 1]);
|
||||
let mut enc = Vec::new();
|
||||
el(&mut enc, CONTENT_COMPRESSION, &comp);
|
||||
let mut encs = Vec::new();
|
||||
el(&mut encs, CONTENT_ENCODING, &enc);
|
||||
el(&mut v, CONTENT_ENCODINGS, &encs);
|
||||
let mut a = Vec::new();
|
||||
el_uint(&mut a, TRACK_NUMBER, 2);
|
||||
el_uint(&mut a, TRACK_TYPE, 2);
|
||||
el_str(&mut a, CODEC_ID, "A_AAC/MPEG4/LC/SBR");
|
||||
el_str(&mut a, LANGUAGE, "ger");
|
||||
let mut au = Vec::new();
|
||||
el_float(&mut au, SAMPLING_FREQUENCY, 24000.0);
|
||||
el_float(&mut au, OUTPUT_SAMPLING_FREQUENCY, 48000.0);
|
||||
el_uint(&mut au, CHANNELS, 2);
|
||||
el(&mut a, AUDIO, &au);
|
||||
let mut tr = Vec::new();
|
||||
el(&mut tr, TRACK_ENTRY, &v);
|
||||
el(&mut tr, TRACK_ENTRY, &a);
|
||||
el(&mut f, TRACKS, &tr);
|
||||
// Chapters
|
||||
let mut disp = Vec::new();
|
||||
el_str(&mut disp, CHAP_STRING, "Intro");
|
||||
let mut atom = Vec::new();
|
||||
el_uint(&mut atom, CHAPTER_UID, 5);
|
||||
el_uint(&mut atom, CHAPTER_TIME_START, 0);
|
||||
el_uint(&mut atom, CHAPTER_TIME_END, 40_000_000);
|
||||
el(&mut atom, CHAPTER_DISPLAY, &disp);
|
||||
let mut ed = Vec::new();
|
||||
el(&mut ed, CHAPTER_ATOM, &atom);
|
||||
let mut ch = Vec::new();
|
||||
el(&mut ch, EDITION_ENTRY, &ed);
|
||||
el(&mut f, CHAPTERS, &ch);
|
||||
// Attachments
|
||||
let mut af = Vec::new();
|
||||
el_str(&mut af, FILE_NAME, "font.ttf");
|
||||
el_str(&mut af, FILE_MEDIA_TYPE, "font/ttf");
|
||||
el_uint(&mut af, FILE_UID, 77);
|
||||
el(&mut af, FILE_DATA, &[9; 100]);
|
||||
let mut att = Vec::new();
|
||||
el(&mut att, ATTACHED_FILE, &af);
|
||||
el(&mut f, ATTACHMENTS, &att);
|
||||
// Cluster 1 (unknown size): keyframe SimpleBlock, BlockGroup with ReferenceBlock + duration
|
||||
write_id(&mut f, CLUSTER);
|
||||
write_unknown_size(&mut f, 1);
|
||||
el_uint(&mut f, TIMESTAMP, 1000);
|
||||
el(&mut f, SIMPLE_BLOCK, &block(1, 0, 0x80, &[0x65, 1, 2]));
|
||||
el(&mut f, SIMPLE_BLOCK, &block(2, 5, 0x80, &[0xAA; 10]));
|
||||
let mut g = Vec::new();
|
||||
el(&mut g, BLOCK, &block(1, 40, 0, &[0x41, 3]));
|
||||
el(&mut g, REFERENCE_BLOCK, &[0xD8]);
|
||||
el_uint(&mut g, BLOCK_DURATION, 80);
|
||||
el(&mut f, BLOCK_GROUP, &g);
|
||||
el(&mut f, SIMPLE_BLOCK, &block(9, 0, 0x80, &[1])); // unknown track: ignored
|
||||
// Cluster 2 (unknown size), ended by Tags
|
||||
write_id(&mut f, CLUSTER);
|
||||
write_unknown_size(&mut f, 8);
|
||||
el_uint(&mut f, TIMESTAMP, 1120);
|
||||
el(&mut f, SIMPLE_BLOCK, &block(1, 0, 0x80, &[0x65, 4]));
|
||||
let mut st = Vec::new();
|
||||
el_str(&mut st, TAG_NAME, "ARTIST");
|
||||
el_str(&mut st, TAG_STRING, "Nobody");
|
||||
let mut tg = Vec::new();
|
||||
let mut tgt = Vec::new();
|
||||
el_uint(&mut tgt, TAG_TRACK_UID, 11);
|
||||
el(&mut tg, TARGETS, &tgt);
|
||||
el(&mut tg, SIMPLE_TAG, &st);
|
||||
let mut tags = Vec::new();
|
||||
el(&mut tags, TAG, &tg);
|
||||
el(&mut f, TAGS, &tags);
|
||||
f
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn synthetic_file() {
|
||||
let bytes = synthetic();
|
||||
let f = open_with(&bytes[..], &OpenOptions { index: true, verify_crc: true }).unwrap();
|
||||
assert!(f.live);
|
||||
assert!(f.warnings.is_empty(), "{:?}", f.warnings);
|
||||
assert_eq!((f.crc_checked, f.crc_errors.len()), (1, 0));
|
||||
assert_eq!(f.info.title.as_deref(), Some("Synthetic"));
|
||||
let v = &f.tracks[0];
|
||||
assert!(matches!(&v.codec, Codec::Avc { avcc } if avcc[1] == 0x64));
|
||||
let vi = v.video.as_ref().unwrap();
|
||||
assert_eq!(vi.pixel_aspect(), (4, 3));
|
||||
assert_eq!(vi.projection.as_ref().map(|p| p.roll), Some(-90.0));
|
||||
assert_eq!(vi.display_rotation(), Some(1));
|
||||
let c = vi.colour.as_ref().unwrap();
|
||||
assert!(c.full_range());
|
||||
assert_eq!(
|
||||
(c.matrix_coefficients, c.transfer_characteristics, c.primaries, c.max_cll, c.max_fall),
|
||||
(Some(9), Some(16), Some(9), Some(1000), Some(400))
|
||||
);
|
||||
let m = c.mastering.as_ref().unwrap();
|
||||
assert_eq!(m.primaries[1], (0.17, 0.797));
|
||||
assert_eq!(m.white_point, (0.3127, 0.329));
|
||||
assert_eq!((m.luminance_max, m.luminance_min), (1000.0, 0.0001));
|
||||
assert_eq!(v.encodings, vec![ContentEncoding::HeaderStripping { bytes: vec![0, 0, 0, 1], scope: 1 }]);
|
||||
// legacy AAC ID: ASC synthesised with explicit SBR signalling (LC, 24 kHz, stereo → 48 kHz)
|
||||
assert_eq!(f.tracks[1].codec, Codec::Aac { asc: vec![0x13, 0x10, 0x56, 0xE5, 0x98] });
|
||||
assert_eq!(f.tracks[1].language, "ger");
|
||||
assert_eq!(f.chapters[0].chapters[0].title(), Some("Intro"));
|
||||
assert_eq!(f.chapters[0].chapters[0].end_ns, Some(40_000_000));
|
||||
assert_eq!(f.attachments.len(), 1);
|
||||
let at = &f.attachments[0];
|
||||
assert_eq!((at.name.as_str(), at.media_type.as_str(), at.uid), ("font.ttf", "font/ttf", 77));
|
||||
assert_eq!(&bytes[at.data_offset as usize..][..100], &[9; 100]);
|
||||
assert_eq!(f.tags[0].targets.track_uids, vec![11]);
|
||||
assert_eq!(f.tags[0].simple[0].string.as_deref(), Some("Nobody"));
|
||||
assert_eq!(f.clusters.len(), 2);
|
||||
assert_eq!(v.samples.len(), 3);
|
||||
assert_eq!(v.keyframes(), &[0, 2]);
|
||||
|
||||
let d = Demuxer::from_slice(&bytes).unwrap();
|
||||
let p: Vec<Packet> = d.map(|p| p.unwrap()).collect();
|
||||
assert_eq!(p.len(), 4);
|
||||
assert_eq!((p[0].track, p[0].pts, p[0].keyframe, p[0].duration), (0, 1000, true, 40));
|
||||
assert_eq!(p[0].data, [0, 0, 0, 1, 0x65, 1, 2]);
|
||||
assert_eq!((p[1].track, p[1].pts, p[1].data.len()), (1, 1005, 10));
|
||||
assert_eq!((p[2].pts, p[2].keyframe, p[2].duration), (1040, false, 80));
|
||||
assert_eq!(p[2].data, [0, 0, 0, 1, 0x41, 3]);
|
||||
assert_eq!((p[3].pts, p[3].pts_ns), (1120, 1_120_000_000));
|
||||
assert_eq!(f.read_sample(&bytes[..], 0, 1).unwrap(), p[2].data);
|
||||
|
||||
// CRC mismatch is reported, not fatal
|
||||
let mut bad = bytes.clone();
|
||||
let title = bad.windows(9).position(|w| w == b"Synthetic").unwrap();
|
||||
bad[title] = b's';
|
||||
let f = open_with(&bad[..], &OpenOptions { index: true, verify_crc: true }).unwrap();
|
||||
assert_eq!(f.crc_errors.len(), 1);
|
||||
|
||||
// seeking without index or cues builds the index
|
||||
let mut d = Demuxer::with_options(&bytes[..], OpenOptions { index: false, verify_crc: false }).unwrap();
|
||||
assert_eq!(d.file().tracks.len(), 2);
|
||||
assert_eq!(d.seek(0, 1_100_000_000).unwrap().pts, 1000);
|
||||
assert_eq!(d.seek(0, 1_130_000_000).unwrap().pts, 1120);
|
||||
assert_eq!(d.next_packet().unwrap().unwrap().pts, 1120);
|
||||
assert!(d.file().indexed);
|
||||
}
|
||||
|
||||
fn one_block(ts: u64, rel: i16) -> Vec<u8> {
|
||||
let mut f = Vec::new();
|
||||
let mut h = Vec::new();
|
||||
el_str(&mut h, DOC_TYPE, "matroska");
|
||||
el(&mut f, EBML, &h);
|
||||
write_id(&mut f, SEGMENT);
|
||||
write_unknown_size(&mut f, 8);
|
||||
let mut info = Vec::new();
|
||||
el_uint(&mut info, TIMESTAMP_SCALE, 1_000_000);
|
||||
el(&mut f, INFO, &info);
|
||||
let mut v = Vec::new();
|
||||
el_uint(&mut v, TRACK_NUMBER, 1);
|
||||
el_uint(&mut v, TRACK_UID, 1);
|
||||
el_uint(&mut v, TRACK_TYPE, 1);
|
||||
el_str(&mut v, CODEC_ID, "V_UNCOMPRESSED");
|
||||
let mut tr = Vec::new();
|
||||
el(&mut tr, TRACK_ENTRY, &v);
|
||||
el(&mut f, TRACKS, &tr);
|
||||
write_id(&mut f, CLUSTER);
|
||||
write_unknown_size(&mut f, 8);
|
||||
el_uint(&mut f, TIMESTAMP, ts);
|
||||
el(&mut f, SIMPLE_BLOCK, &block(1, rel, 0x80, &[0]));
|
||||
f
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_timestamp_addition_does_not_overflow() {
|
||||
let bytes = one_block(1000, 5);
|
||||
let d = Demuxer::from_slice(&bytes).unwrap();
|
||||
let p: Vec<Packet> = d.map(|p| p.unwrap()).collect();
|
||||
assert_eq!(p[0].pts, 1005);
|
||||
let edge = one_block(i64::MAX as u64, 0);
|
||||
let d = Demuxer::from_slice(&edge).unwrap();
|
||||
let p: Vec<Packet> = d.map(|p| p.unwrap()).collect();
|
||||
assert_eq!(p[0].pts, i64::MAX);
|
||||
assert!(Demuxer::from_slice(&one_block(i64::MAX as u64, 1)).is_err());
|
||||
assert!(Demuxer::from_slice(&one_block(u64::MAX, 0)).is_err());
|
||||
}
|
||||
@@ -1,348 +0,0 @@
|
||||
//! Track descriptions (RFC 9559 §5.1.4).
|
||||
|
||||
use crate::codec::Codec;
|
||||
|
||||
/// Track type (`TrackType`).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum TrackKind {
|
||||
Video,
|
||||
Audio,
|
||||
Complex,
|
||||
Logo,
|
||||
Subtitle,
|
||||
Buttons,
|
||||
Control,
|
||||
Metadata,
|
||||
Other(u8),
|
||||
}
|
||||
|
||||
impl TrackKind {
|
||||
pub(crate) fn from_type(t: u64) -> TrackKind {
|
||||
match t {
|
||||
1 => TrackKind::Video,
|
||||
2 => TrackKind::Audio,
|
||||
3 => TrackKind::Complex,
|
||||
0x10 => TrackKind::Logo,
|
||||
0x11 => TrackKind::Subtitle,
|
||||
0x12 => TrackKind::Buttons,
|
||||
0x20 => TrackKind::Control,
|
||||
0x21 => TrackKind::Metadata,
|
||||
n => TrackKind::Other(n as u8),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// SMPTE ST 2086 mastering display metadata (`MasteringMetadata`). Chromaticities are CIE 1931 xy,
|
||||
/// luminance in cd/m².
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct MasteringMetadata {
|
||||
/// Red, green, blue primaries as (x, y).
|
||||
pub primaries: [(f64, f64); 3],
|
||||
pub white_point: (f64, f64),
|
||||
pub luminance_max: f64,
|
||||
pub luminance_min: f64,
|
||||
}
|
||||
|
||||
/// `Colour` element. Code points use ITU-T H.273 values (as Matroska does).
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Colour {
|
||||
/// `MatrixCoefficients` (H.273; 2 = unspecified).
|
||||
pub matrix_coefficients: Option<u64>,
|
||||
pub bits_per_channel: Option<u64>,
|
||||
pub chroma_subsampling_horz: Option<u64>,
|
||||
pub chroma_subsampling_vert: Option<u64>,
|
||||
pub cb_subsampling_horz: Option<u64>,
|
||||
pub cb_subsampling_vert: Option<u64>,
|
||||
pub chroma_siting_horz: Option<u64>,
|
||||
pub chroma_siting_vert: Option<u64>,
|
||||
/// `Range`: 0 unspecified, 1 broadcast (limited), 2 full, 3 defined by matrix/transfer.
|
||||
pub range: Option<u64>,
|
||||
/// `TransferCharacteristics` (H.273).
|
||||
pub transfer_characteristics: Option<u64>,
|
||||
/// `Primaries` (H.273).
|
||||
pub primaries: Option<u64>,
|
||||
/// Maximum content light level (cd/m²).
|
||||
pub max_cll: Option<u64>,
|
||||
/// Maximum frame-average light level (cd/m²).
|
||||
pub max_fall: Option<u64>,
|
||||
pub mastering: Option<MasteringMetadata>,
|
||||
}
|
||||
|
||||
impl Colour {
|
||||
/// True if `Range` says full range.
|
||||
pub fn full_range(&self) -> bool {
|
||||
self.range == Some(2)
|
||||
}
|
||||
}
|
||||
|
||||
/// `Video` element.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct VideoInfo {
|
||||
pub pixel_width: u32,
|
||||
pub pixel_height: u32,
|
||||
/// `DisplayWidth`/`DisplayHeight`, in `display_unit` (0 = pixels, 3 = display aspect ratio).
|
||||
pub display_width: Option<u32>,
|
||||
pub display_height: Option<u32>,
|
||||
pub display_unit: u64,
|
||||
/// Crop (top, bottom, left, right) in pixels.
|
||||
pub crop: (u32, u32, u32, u32),
|
||||
/// `FlagInterlaced`: 0 undetermined, 1 interlaced, 2 progressive.
|
||||
pub flag_interlaced: u64,
|
||||
/// `FieldOrder` (0 progressive, 1 tff, 2 undetermined, 6 bff, 9/14 interleaved).
|
||||
pub field_order: Option<u64>,
|
||||
pub stereo_mode: Option<u64>,
|
||||
/// `AlphaMode`: 1 if BlockAdditions carry alpha.
|
||||
pub alpha_mode: u64,
|
||||
/// `ColourSpace` FourCC (uncompressed video).
|
||||
pub colour_space: Option<[u8; 4]>,
|
||||
pub colour: Option<Colour>,
|
||||
pub projection: Option<Projection>,
|
||||
}
|
||||
|
||||
/// `Projection` element (RFC 9559 §5.1.4.1.28.41): pose angles in degrees.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Projection {
|
||||
/// 0 rectangular, 1 equirectangular, 2 cubemap, 3 mesh.
|
||||
pub projection_type: u64,
|
||||
/// Clockwise around the up vector.
|
||||
pub yaw: f64,
|
||||
/// Counter-clockwise around the right vector.
|
||||
pub pitch: f64,
|
||||
/// Counter-clockwise around the forward vector.
|
||||
pub roll: f64,
|
||||
}
|
||||
|
||||
impl VideoInfo {
|
||||
/// Clockwise quarter turns (0–3) to display a rectangular video, from `ProjectionPoseRoll`
|
||||
/// (a counter-clockwise angle, so −90 is a quarter turn clockwise). None for other
|
||||
/// projections, flips (non-zero yaw / pitch) or angles that are not a multiple of 90°.
|
||||
pub fn display_rotation(&self) -> Option<u8> {
|
||||
let Some(p) = &self.projection else { return Some(0) };
|
||||
if p.projection_type != 0 || p.yaw != 0.0 || p.pitch != 0.0 || p.roll % 90.0 != 0.0 {
|
||||
return None;
|
||||
}
|
||||
Some((-(p.roll / 90.0) as i64).rem_euclid(4) as u8)
|
||||
}
|
||||
|
||||
/// Pixel aspect ratio derived from the display size (pixels or aspect-ratio units), reduced.
|
||||
pub fn pixel_aspect(&self) -> (u32, u32) {
|
||||
let (pw, ph) =
|
||||
(self.pixel_width.saturating_sub(self.crop.2 + self.crop.3) as u64, self.pixel_height.saturating_sub(self.crop.0 + self.crop.1) as u64);
|
||||
let (Some(dw), Some(dh)) = (self.display_width, self.display_height) else { return (1, 1) };
|
||||
if pw == 0 || ph == 0 || dw == 0 || dh == 0 || !matches!(self.display_unit, 0 | 3) {
|
||||
return (1, 1);
|
||||
}
|
||||
// PAR = (dw/dh) / (pw/ph)
|
||||
let (n, d) = (dw as u64 * ph, dh as u64 * pw);
|
||||
let g = gcd(n, d);
|
||||
((n / g) as u32, (d / g) as u32)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn gcd(mut a: u64, mut b: u64) -> u64 {
|
||||
while b != 0 {
|
||||
(a, b) = (b, a % b);
|
||||
}
|
||||
a.max(1)
|
||||
}
|
||||
|
||||
/// `Audio` element.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct AudioInfo {
|
||||
/// `SamplingFrequency` (Hz).
|
||||
pub sampling_frequency: f64,
|
||||
/// `OutputSamplingFrequency` (e.g. SBR output rate).
|
||||
pub output_sampling_frequency: Option<f64>,
|
||||
pub channels: u64,
|
||||
pub bit_depth: Option<u64>,
|
||||
}
|
||||
|
||||
impl Default for AudioInfo {
|
||||
fn default() -> Self {
|
||||
Self { sampling_frequency: 8000.0, output_sampling_frequency: None, channels: 1, bit_depth: None }
|
||||
}
|
||||
}
|
||||
|
||||
/// One `ContentEncoding`.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum ContentEncoding {
|
||||
/// `ContentCompAlgo` 3: the given bytes were stripped from the start of every frame.
|
||||
HeaderStripping { bytes: Vec<u8>, scope: u64 },
|
||||
/// Other compression (0 zlib, 1 bzlib, 2 lzo1x): not supported for reading frame data.
|
||||
Compression { algo: u64, settings: Vec<u8>, scope: u64 },
|
||||
/// Encryption (not supported for reading frame data).
|
||||
Encryption { scope: u64 },
|
||||
}
|
||||
|
||||
/// One entry of a track's sample (frame) table.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Sample {
|
||||
/// Byte offset of the (stored) frame data in the file.
|
||||
pub offset: u64,
|
||||
/// Stored size (excluding header-stripped bytes; see [`Track::sample_size`]).
|
||||
pub size: u32,
|
||||
/// Presentation timestamp in the track timebase (`TimestampScale` ticks).
|
||||
pub pts: i64,
|
||||
/// Duration in timebase ticks (from `BlockDuration` or `DefaultDuration`), 0 if unknown.
|
||||
pub duration: u64,
|
||||
pub keyframe: bool,
|
||||
/// Index of the owning cluster in [`crate::MkvFile::clusters`].
|
||||
pub cluster: u32,
|
||||
/// Offset of the block element (SimpleBlock / BlockGroup) header.
|
||||
pub block_offset: u64,
|
||||
/// Index of this frame within a laced block.
|
||||
pub lace: u16,
|
||||
}
|
||||
|
||||
/// A Matroska track (`TrackEntry`) plus, once indexed, its sample table.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Track {
|
||||
pub number: u64,
|
||||
pub uid: u64,
|
||||
pub kind: TrackKind,
|
||||
pub enabled: bool,
|
||||
pub default: bool,
|
||||
pub forced: bool,
|
||||
pub hearing_impaired: bool,
|
||||
pub lacing: bool,
|
||||
/// `DefaultDuration` in nanoseconds.
|
||||
pub default_duration_ns: Option<u64>,
|
||||
pub name: String,
|
||||
/// `Language` (ISO 639-2, default `eng`).
|
||||
pub language: String,
|
||||
pub language_bcp47: Option<String>,
|
||||
pub codec_id: String,
|
||||
pub codec_private: Vec<u8>,
|
||||
pub codec_name: String,
|
||||
pub codec: Codec,
|
||||
/// `CodecDelay` in nanoseconds.
|
||||
pub codec_delay_ns: u64,
|
||||
/// `SeekPreRoll` in nanoseconds.
|
||||
pub seek_pre_roll_ns: u64,
|
||||
pub video: Option<VideoInfo>,
|
||||
pub audio: Option<AudioInfo>,
|
||||
pub encodings: Vec<ContentEncoding>,
|
||||
/// Timebase numerator/denominator in seconds: `TimestampScale / 1e9`, reduced.
|
||||
pub timebase: (u64, u64),
|
||||
/// Frame table (empty unless the file was opened with indexing).
|
||||
pub samples: Vec<Sample>,
|
||||
pub(crate) sync: Vec<u32>,
|
||||
}
|
||||
|
||||
impl Track {
|
||||
/// Bytes prepended to every frame by header-stripping compression, if any.
|
||||
pub fn stripped_header(&self) -> Option<&[u8]> {
|
||||
self.encodings.iter().find_map(|e| match e {
|
||||
ContentEncoding::HeaderStripping { bytes, scope } if scope & 1 != 0 => Some(bytes.as_slice()),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// True if frames can be returned verbatim or with header stripping undone.
|
||||
pub fn frames_readable(&self) -> bool {
|
||||
self.encodings.iter().all(|e| match e {
|
||||
ContentEncoding::HeaderStripping { .. } => true,
|
||||
ContentEncoding::Compression { scope, .. } | ContentEncoding::Encryption { scope } => scope & 1 == 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Bytes to put in front of a stored frame of `stored_size` bytes to get the codec frame:
|
||||
/// header-stripped bytes, or for `V_PRORES` the 8-byte frame header (`size` + `icpf`) that the
|
||||
/// Matroska ProRes mapping omits.
|
||||
pub fn frame_prefix(&self, stored_size: u64) -> Vec<u8> {
|
||||
let mut p = self.stripped_header().map(<[u8]>::to_vec).unwrap_or_default();
|
||||
if matches!(self.codec, Codec::ProRes { .. }) {
|
||||
let total = stored_size + p.len() as u64 + 8;
|
||||
let mut h = (total as u32).to_be_bytes().to_vec();
|
||||
h.extend_from_slice(b"icpf");
|
||||
h.extend_from_slice(&p);
|
||||
p = h;
|
||||
}
|
||||
p
|
||||
}
|
||||
|
||||
/// Frame size of sample `i` as returned by `read_sample` / packets (stored size plus prefix).
|
||||
pub fn sample_size(&self, i: usize) -> Option<u64> {
|
||||
let s = self.samples.get(i)?;
|
||||
Some(s.size as u64 + self.frame_prefix(s.size as u64).len() as u64)
|
||||
}
|
||||
|
||||
/// Indices of keyframe samples, ascending by file order.
|
||||
pub fn keyframes(&self) -> &[u32] {
|
||||
&self.sync
|
||||
}
|
||||
|
||||
/// Timebase ticks → nanoseconds.
|
||||
pub fn ticks_to_ns(&self, t: i64) -> i64 {
|
||||
(t as i128 * self.timebase.0 as i128 * 1_000_000_000 / self.timebase.1 as i128) as i64
|
||||
}
|
||||
|
||||
/// Nanoseconds → timebase ticks (floor).
|
||||
pub fn ns_to_ticks(&self, ns: i64) -> i64 {
|
||||
let n = ns as i128 * self.timebase.1 as i128;
|
||||
let d = self.timebase.0 as i128 * 1_000_000_000;
|
||||
n.div_euclid(d) as i64
|
||||
}
|
||||
|
||||
/// Index of the last sample with `pts <= t` (in file order), or the first sample.
|
||||
pub fn sample_at_pts(&self, t: i64) -> Option<usize> {
|
||||
if self.samples.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut best: Option<usize> = None;
|
||||
for (i, s) in self.samples.iter().enumerate() {
|
||||
if s.pts <= t && best.is_none_or(|b| s.pts >= self.samples[b].pts) {
|
||||
best = Some(i);
|
||||
}
|
||||
}
|
||||
Some(best.unwrap_or(0))
|
||||
}
|
||||
|
||||
/// Index of the keyframe with the greatest `pts <= t` (the first keyframe if none precedes `t`).
|
||||
pub fn keyframe_before_pts(&self, t: i64) -> Option<usize> {
|
||||
let mut best: Option<usize> = None;
|
||||
for &k in &self.sync {
|
||||
let s = &self.samples[k as usize];
|
||||
if s.pts <= t && best.is_none_or(|b| s.pts >= self.samples[b].pts) {
|
||||
best = Some(k as usize);
|
||||
}
|
||||
}
|
||||
best.or_else(|| self.sync.first().map(|&k| k as usize))
|
||||
}
|
||||
|
||||
/// Index of the nearest keyframe at or before sample `index` in file order.
|
||||
pub fn sync_sample_before(&self, index: usize) -> usize {
|
||||
match self.sync.binary_search(&(index as u32)) {
|
||||
Ok(i) => self.sync[i] as usize,
|
||||
Err(0) => 0,
|
||||
Err(i) => self.sync[i - 1] as usize,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod rotation_tests {
|
||||
use super::{Projection, VideoInfo};
|
||||
|
||||
fn with_roll(projection_type: u64, yaw: f64, pitch: f64, roll: f64) -> VideoInfo {
|
||||
VideoInfo { projection: Some(Projection { projection_type, yaw, pitch, roll }), ..Default::default() }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_rotation_maps_rectangular_roll_to_clockwise_quarter_turns() {
|
||||
assert_eq!(VideoInfo::default().display_rotation(), Some(0), "no Projection: as stored");
|
||||
// ProjectionPoseRoll is counter-clockwise: -90 is a quarter turn clockwise
|
||||
for (roll, turns) in [(0.0, 0), (-90.0, 1), (180.0, 2), (-180.0, 2), (90.0, 3), (-270.0, 3), (270.0, 1), (360.0, 0)] {
|
||||
assert_eq!(with_roll(0, 0.0, 0.0, roll).display_rotation(), Some(turns), "roll {roll}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_rotation_leaves_non_rectangular_flipped_and_odd_angles_alone() {
|
||||
assert_eq!(with_roll(1, 0.0, 0.0, -90.0).display_rotation(), None, "equirectangular");
|
||||
assert_eq!(with_roll(0, 180.0, 0.0, 0.0).display_rotation(), None, "yaw flip");
|
||||
assert_eq!(with_roll(0, 0.0, 180.0, 0.0).display_rotation(), None, "pitch flip");
|
||||
assert_eq!(with_roll(0, 0.0, 0.0, 45.0).display_rotation(), None, "not a quarter turn");
|
||||
assert_eq!(with_roll(0, 0.0, 0.0, f64::NAN).display_rotation(), None, "malformed angle");
|
||||
}
|
||||
}
|
||||
@@ -330,6 +330,7 @@ pub(crate) fn codec_name(c: &CodecConfig) -> String {
|
||||
CodecConfig::Hevc(_) => "HEVC".into(),
|
||||
CodecConfig::Av1(_) => "AV1".into(),
|
||||
CodecConfig::Vp9(_) => "VP9".into(),
|
||||
CodecConfig::Apv(_) => "APV".into(),
|
||||
CodecConfig::ProRes { .. } => "Apple ProRes".into(),
|
||||
CodecConfig::Jpeg { .. } => "Motion JPEG".into(),
|
||||
CodecConfig::Dnx { .. } => "DNxHD".into(),
|
||||
|
||||
Reference in New Issue
Block a user