mirror of
https://github.com/storytold/soundcraft.git
synced 2026-10-24 02:26:46 -06:00
Build: use craft-media for bitstream and isobmff (#776)
* Build: use craft-media for bitstream and isobmff
soundcraft-video re-exports craft_media::{bitstream, isobmff} from
craft-libs instead of carrying a copy of FilmCraft's crates. The module
paths stay, so crate::bitstream and crate::isobmff resolve as before.
The shared copy is newer: it adds APV, .mp2 as MPEG audio, AAC channel
configurations 11-14, SampleEntry::av1/flac, Mp4Writer::set_sample_entry
and the se(v) fix for i32::MIN.
* Build: pin craft-media to craft-libs f0ad6c1e
This commit is contained in:
Generated
+9
@@ -906,6 +906,14 @@ name = "craft-docpath"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/storytold/craft-libs?rev=82a24b006eefa036b43c09a67fb9a3fbd17ffe63#82a24b006eefa036b43c09a67fb9a3fbd17ffe63"
|
source = "git+https://github.com/storytold/craft-libs?rev=82a24b006eefa036b43c09a67fb9a3fbd17ffe63#82a24b006eefa036b43c09a67fb9a3fbd17ffe63"
|
||||||
|
|
||||||
|
[[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]]
|
[[package]]
|
||||||
name = "craft-ui"
|
name = "craft-ui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -3798,6 +3806,7 @@ dependencies = [
|
|||||||
name = "soundcraft-video"
|
name = "soundcraft-video"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"craft-media",
|
||||||
"image",
|
"image",
|
||||||
"proptest",
|
"proptest",
|
||||||
"rayon",
|
"rayon",
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ cpal = "0.18"
|
|||||||
# Which file paths stored in a session may be opened.
|
# Which file paths stored in a session may be opened.
|
||||||
craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "82a24b006eefa036b43c09a67fb9a3fbd17ffe63" }
|
craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "82a24b006eefa036b43c09a67fb9a3fbd17ffe63" }
|
||||||
|
|
||||||
|
# Video containers and bitstreams, shared with the other craft apps.
|
||||||
|
craft-media = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "f0ad6c1e1d297908915f4be315b0fcfc9be7b5d4" }
|
||||||
|
|
||||||
# ui
|
# ui
|
||||||
craft-ui = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "5c874d51eeb3de70e735c8ce87a59a7dedd47675", features = ["serde"] }
|
craft-ui = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "5c874d51eeb3de70e735c8ce87a59a7dedd47675", features = ["serde"] }
|
||||||
# egui 0.37 needs craft-libs#199 (craft-ui) first; pin 0.36 until it lands.
|
# egui 0.37 needs craft-libs#199 (craft-ui) first; pin 0.36 until it lands.
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ default = []
|
|||||||
threads = ["dep:rayon"]
|
threads = ["dep:rayon"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
craft-media.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
image = { workspace = true, features = ["jpeg"] }
|
image = { workspace = true, features = ["jpeg"] }
|
||||||
|
|||||||
@@ -1,475 +0,0 @@
|
|||||||
//! Bit-level reading and writing shared by FilmCraft's codec crates.
|
|
||||||
//!
|
|
||||||
//! - [`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.
|
|
||||||
//! - [`annexb_nals`] / [`length_prefixed_nals`]: split Annex-B byte streams and AVCC/HVCC samples into NAL units.
|
|
||||||
|
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
#[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 the AVCC/HVCC range of 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("invalid NAL length 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Signed Exp-Golomb `se(v)`.
|
|
||||||
pub fn read_se(&mut self) -> Result<i32> {
|
|
||||||
let k = self.read_ue()? as i64;
|
|
||||||
Ok(if k & 1 == 1 { ((k + 1) / 2) as i32 } else { -((k / 2) as i32) })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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) {
|
|
||||||
let x = v as u64 + 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn write_se(&mut self, v: i32) {
|
|
||||||
let k = if v > 0 { (v as u32) * 2 - 1 } else { (-(v as i64) as u32) * 2 };
|
|
||||||
self.write_ue(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) into NAL units.
|
|
||||||
///
|
|
||||||
/// The width comes from the stream's configuration record (`(byte & 3) + 1`), so anything
|
|
||||||
/// outside 1..=4 is a programming or parsing error and is rejected up front: a width of 0
|
|
||||||
/// would never advance the cursor, looping forever and appending empty NALs until the
|
|
||||||
/// process runs out of memory, and a wider prefix cannot occur in a real sample.
|
|
||||||
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() {
|
|
||||||
let Some(start) = i.checked_add(length_size) else {
|
|
||||||
return Err(BitError::Eof);
|
|
||||||
};
|
|
||||||
if start > data.len() {
|
|
||||||
return Err(BitError::Eof);
|
|
||||||
}
|
|
||||||
let mut len = 0usize;
|
|
||||||
for &b in &data[i..start] {
|
|
||||||
len = (len << 8) | b as usize;
|
|
||||||
}
|
|
||||||
let Some(end) = start.checked_add(len) else {
|
|
||||||
return Err(BitError::Eof);
|
|
||||||
};
|
|
||||||
if end > data.len() {
|
|
||||||
return Err(BitError::Eof);
|
|
||||||
}
|
|
||||||
out.push(&data[start..end]);
|
|
||||||
i = end;
|
|
||||||
}
|
|
||||||
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 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 length_prefixed_rejects_bad_length_size() {
|
|
||||||
// Regression test for #469: with a width of 0 neither the prefix nor a
|
|
||||||
// zero-length payload advanced the cursor, so any non-empty sample
|
|
||||||
// looped forever appending empty NALs. Widths above 4 cannot come
|
|
||||||
// from an AVCC/HVCC record either.
|
|
||||||
let d = [0, 0, 0, 2, 0x67, 1];
|
|
||||||
assert_eq!(length_prefixed_nals(&d, 0), Err(BitError::InvalidLengthSize));
|
|
||||||
assert_eq!(length_prefixed_nals(&d, 5), Err(BitError::InvalidLengthSize));
|
|
||||||
assert_eq!(length_prefixed_nals(&d, usize::MAX), Err(BitError::InvalidLengthSize));
|
|
||||||
assert_eq!(length_prefixed_nals(&[], 0), Err(BitError::InvalidLengthSize));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn length_prefixed_widths() {
|
|
||||||
// 1-byte prefixes.
|
|
||||||
assert_eq!(length_prefixed_nals(&[2, 0x67, 1, 1, 0x68], 1).unwrap(), vec![&[0x67, 1][..], &[0x68][..]]);
|
|
||||||
// 2-byte prefixes.
|
|
||||||
assert_eq!(length_prefixed_nals(&[0, 2, 0x67, 1, 0, 1, 0x68], 2).unwrap(), vec![&[0x67, 1][..], &[0x68][..]]);
|
|
||||||
// 3-byte prefixes (lengthSizeMinusOne == 2 in the configuration record).
|
|
||||||
assert_eq!(length_prefixed_nals(&[0, 0, 2, 0x67, 1], 3).unwrap(), vec![&[0x67, 1][..]]);
|
|
||||||
// A truncated prefix or payload is Eof, not a panic or a short read.
|
|
||||||
assert_eq!(length_prefixed_nals(&[0, 0], 4), Err(BitError::Eof));
|
|
||||||
assert_eq!(length_prefixed_nals(&[0, 0, 0, 9, 0x67], 4), Err(BitError::Eof));
|
|
||||||
// Empty input splits to no NALs.
|
|
||||||
assert!(length_prefixed_nals(&[], 4).unwrap().is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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)); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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::isobmff::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,26 +0,0 @@
|
|||||||
//! 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.
|
|
||||||
|
|
||||||
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;
|
|
||||||
@@ -1,966 +0,0 @@
|
|||||||
//! Muxers: progressive MP4/MOV ([`Mp4Writer`], optional faststart) and fragmented MP4 ([`FragmentedWriter`]).
|
|
||||||
|
|
||||||
use crate::isobmff::bytes::BoxBuf;
|
|
||||||
use crate::isobmff::codec::{CodecConfig, SampleEntry, TimecodeConfig};
|
|
||||||
use crate::isobmff::demux::Edit;
|
|
||||||
use crate::isobmff::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()));
|
|
||||||
}
|
|
||||||
return Ok(Some(p.bytes_per_frame()));
|
|
||||||
}
|
|
||||||
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::isobmff::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::isobmff::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::isobmff::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::isobmff::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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -14,25 +14,25 @@
|
|||||||
//!
|
//!
|
||||||
//! ## Origin
|
//! ## Origin
|
||||||
//!
|
//!
|
||||||
//! The container, H.264 and ProRes code is a copy of the clean-room crates of our sibling app
|
//! [`bitstream`] and [`isobmff`] are re-exports of `craft_media::bitstream` and
|
||||||
//! FilmCraft (`storytold/filmcraft`, same organisation, MIT OR Apache-2.0): `filmcraft-bitstream`
|
//! `craft_media::isobmff` from craft-libs (`storytold/craft-libs`, the shared crate FilmCraft uses
|
||||||
//! → [`bitstream`], `filmcraft-isobmff` → [`isobmff`], `filmcraft-h264` → [`h264`] (without its
|
//! too). The H.264 and ProRes code is a copy of the clean-room crates of our sibling app FilmCraft
|
||||||
//! test-only synthetic stream module), `filmcraft-prores` → [`prores`]. The crafting apps share no
|
//! (`storytold/filmcraft`, same organisation, MIT OR Apache-2.0): `filmcraft-h264` → [`h264`]
|
||||||
//! code as dependencies; each keeps its own copy. Only the module paths were changed; fixes found
|
//! (without its test-only synthetic stream module), `filmcraft-prores` → [`prores`]. Only the
|
||||||
//! here should be offered back upstream. [`Movie`], the pixel conversion and the frame caches are
|
//! module paths were changed; fixes found here should be offered back upstream. [`Movie`], the
|
||||||
//! SoundCraft's own.
|
//! pixel conversion and the frame caches are SoundCraft's own.
|
||||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||||
// Codec code indexes fixed-size blocks; index loops read more clearly than iterator chains there.
|
// Codec code indexes fixed-size blocks; index loops read more clearly than iterator chains there.
|
||||||
#![allow(clippy::needless_range_loop, clippy::too_many_arguments)]
|
#![allow(clippy::needless_range_loop, clippy::too_many_arguments)]
|
||||||
|
|
||||||
pub mod bitstream;
|
|
||||||
pub mod convert;
|
pub mod convert;
|
||||||
pub mod h264;
|
pub mod h264;
|
||||||
pub mod isobmff;
|
|
||||||
mod movie;
|
mod movie;
|
||||||
pub mod prores;
|
pub mod prores;
|
||||||
pub mod synth;
|
pub mod synth;
|
||||||
|
|
||||||
|
pub use craft_media::{bitstream, isobmff};
|
||||||
|
|
||||||
pub use isobmff::ByteSource;
|
pub use isobmff::ByteSource;
|
||||||
pub use movie::{Frame, Movie, MovieInfo, SharedSource, fit_size};
|
pub use movie::{Frame, Movie, MovieInfo, SharedSource, fit_size};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user