Compare commits

...
Author SHA1 Message Date
Brandon ThomasandClaude Opus 5.5 2c5ba7a726 webm: decode VP9 alpha from BlockAdditions
The Matroska demuxer indexes the BlockAdditional with BlockAddID 1 of
unlaced BlockGroups (Sample::addition, MkvFile::read_block_additional,
Packet::block_additional). The Matroska source decodes it for VP9 tracks
with AlphaMode 1 through a second VP9 decoder instance and attaches its
luma plane as the frame's straight alpha (also to RGB frames). Seeks
start only where both streams are random-access points.

Tests: synthetic BlockAdditions demux test; WebM VP9 yuva420p fixture
(ffmpeg/libvpx as external oracle) colour and alpha bit-exact after seeks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 23:48:19 -04:00
7 changed files with 388 additions and 4 deletions

No files matched your search

+180 -1
View File
@@ -4,6 +4,10 @@
//! containers (H.264, HEVC, VP9, AV1, ProRes, MJPEG). Audio: AAC and Opus
//! via our decoders, PCM directly, MP3/FLAC/Vorbis via the bootstrap decoders.
//!
//! VP9 alpha (WebM `AlphaMode` 1): each block's `BlockAdditional` (`BlockAddID` 1) is a second VP9
//! bitstream whose luma plane is the straight alpha channel. A second decoder instance decodes it
//! alongside the colour stream ([`AlphaDecoder`]) and its luma becomes the frame's alpha plane.
//!
//! Opus (`A_OPUS`): `CodecPrivate` is the `OpusHead`; output is always 48 kHz. The demuxer
//! subtracts `CodecDelay` from timestamps, so the pre-skip samples land before zero and are never
//! read; random access decodes `SeekPreRoll` (at least [`crate::audio::OPUS_PRE_ROLL`]) of preceding
@@ -43,6 +47,8 @@ pub struct MkvSource {
atrack: Option<usize>,
/// Video codec as an ISO-BMFF sample entry (for the decoder factories).
ventry: Option<SampleEntry>,
/// VP9 with `AlphaMode` 1: decode the BlockAdditions alpha stream too.
valpha: bool,
video: GopCache,
audio: Mutex<AudioState>,
/// Audio packet start positions in source sample frames.
@@ -220,6 +226,7 @@ impl MkvSource {
}
let mut explicit_color = None;
let mut ventry = None;
let mut valpha = false;
let video = vtrack.map(|i| {
let t = &file.tracks[i];
let v = t.video.clone().unwrap_or_default();
@@ -239,6 +246,7 @@ impl MkvSource {
explicit_color = Some(color);
}
ventry = sample_entry(&t.codec, &t.codec_private, t.video.as_ref(), w as u16, h as u16);
valpha = v.alpha_mode == 1 && matches!(t.codec, Codec::Vp9 { .. });
let secs = file.duration_ns().unwrap_or(0) as f64 / 1e9;
let bitrate = (secs > 0.0).then(|| (t.samples.iter().map(|s| s.size as u64).sum::<u64>() as f64 * 8.0 / secs) as u64);
VideoStreamInfo {
@@ -317,6 +325,7 @@ impl MkvSource {
vtrack,
atrack,
ventry,
valpha,
video: GopCache::new(explicit_color),
audio: Mutex::new(AudioState { decoder: None, packets: HashMap::new(), order: Vec::new(), last_decoded: None }),
audio_starts,
@@ -423,16 +432,186 @@ impl VideoSamples for MkvVideo<'_> {
self.src.file.tracks[self.track].sample_at_pts(t)
}
fn read(&self, i: usize) -> crate::Result<Vec<u8>> {
self.src.read(self.track, i)
let main = self.src.read(self.track, i)?;
if !self.src.valpha {
return Ok(main);
}
let alpha = self.src.file.read_block_additional(&self.src.bytes, self.track, i).map_err(|e| CodecError::Container(e.to_string()))?;
Ok(pack_alpha(&main, alpha.as_deref()))
}
fn make_decoder(&self) -> crate::Result<Box<dyn VideoDecoder>> {
match &self.src.ventry {
Some(e) if self.src.valpha => Ok(Box::new(AlphaDecoder::new(make_video_decoder(e)?, make_video_decoder(e)?))),
Some(e) => make_video_decoder(e),
None => Err(CodecError::Unsupported(format!("no decoder for {} video", codec_label(&self.src.file.tracks[self.track].codec)))),
}
}
}
/// A colour sample and its alpha sample as one [`AlphaDecoder`] sample: the colour sample's length
/// (u32 LE), the colour sample, then the alpha sample (empty: the block has no alpha).
fn pack_alpha(main: &[u8], alpha: Option<&[u8]>) -> Vec<u8> {
let alpha = alpha.unwrap_or_default();
let mut v = Vec::with_capacity(4 + main.len() + alpha.len());
v.extend_from_slice(&(main.len() as u32).to_le_bytes());
v.extend_from_slice(main);
v.extend_from_slice(alpha);
v
}
/// The (colour, alpha) halves of a [`pack_alpha`] sample (alpha `None` when absent).
fn unpack_alpha(sample: &[u8]) -> (&[u8], Option<&[u8]>) {
let Some(len) = sample.get(..4).map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]) as usize) else { return (sample, None) };
let body = &sample[4..];
if len > body.len() {
return (body, None);
}
let (main, alpha) = body.split_at(len);
(main, (!alpha.is_empty()).then_some(alpha))
}
/// A decoded alpha stream's luma plane, at its own bit depth.
enum AlphaPlane {
U8(Arc<Vec<u8>>),
U16(Arc<Vec<u16>>, u32),
}
impl AlphaPlane {
fn of(f: &VideoFrame) -> Option<(u32, u32, AlphaPlane)> {
use filmcraft_frame::PixelData;
let p = match &f.data {
PixelData::Yuv8 { planes, .. } => AlphaPlane::U8(planes[0].clone()),
PixelData::Yuv16 { planes, bits, .. } => AlphaPlane::U16(planes[0].clone(), *bits),
_ => return None,
};
Some((f.width, f.height, p))
}
/// As 8-bit values.
fn to_u8(&self) -> Arc<Vec<u8>> {
match self {
AlphaPlane::U8(a) => a.clone(),
AlphaPlane::U16(a, bits) => {
let max = ((1u32 << bits) - 1).max(1);
Arc::new(a.iter().map(|&v| ((v as u32 * 255 + max / 2) / max).min(255) as u8).collect())
}
}
}
/// As `bits`-bit values.
fn to_u16(&self, bits: u32) -> Arc<Vec<u16>> {
let to = (1u32 << bits) - 1;
match self {
AlphaPlane::U16(a, b) if *b == bits => a.clone(),
AlphaPlane::U16(a, b) => {
let from = ((1u32 << b) - 1).max(1);
Arc::new(a.iter().map(|&v| ((v as u32 * to + from / 2) / from).min(to) as u16).collect())
}
AlphaPlane::U8(a) => Arc::new(a.iter().map(|&v| ((v as u32 * to + 127) / 255) as u16).collect()),
}
}
}
/// VP9 with a BlockAdditions alpha stream: one decoder for the colour samples, a second for the
/// alpha samples (same codec), fed [`pack_alpha`] samples. Each output picture gets the alpha
/// picture of the same pts as its (straight) alpha plane; a picture without one stays opaque.
struct AlphaDecoder {
main: Box<dyn VideoDecoder>,
alpha: Box<dyn VideoDecoder>,
/// Decoded alpha planes by pts, waiting for their colour picture.
planes: std::collections::BTreeMap<i64, (u32, u32, AlphaPlane)>,
}
impl AlphaDecoder {
fn new(main: Box<dyn VideoDecoder>, alpha: Box<dyn VideoDecoder>) -> Self {
Self { main, alpha, planes: Default::default() }
}
fn take_alpha(&mut self, out: Vec<crate::video::DecodedFrame>) {
for d in out {
if let Some(p) = AlphaPlane::of(&d.frame) {
self.planes.insert(d.pts, p);
}
}
}
/// Attach the alpha plane decoded for each picture's pts (older unmatched planes are dropped).
fn attach(&mut self, out: Vec<crate::video::DecodedFrame>) -> Vec<crate::video::DecodedFrame> {
use filmcraft_frame::PixelData;
out.into_iter()
.map(|mut d| {
let plane = self.planes.remove(&d.pts);
self.planes = self.planes.split_off(&d.pts);
let Some((w, h, a)) = plane.filter(|(w, h, _)| (*w, *h) == (d.frame.width, d.frame.height)) else { return d };
let n = w as usize * h as usize;
match &mut d.frame.data {
PixelData::Yuv8 { alpha, .. } => *alpha = Some(a.to_u8()),
PixelData::Yuv16 { alpha, bits, .. } => *alpha = Some(a.to_u16(*bits)),
PixelData::Rgba8(px) => {
let a = a.to_u8();
let px = Arc::make_mut(px);
for (i, v) in a.iter().take(n).enumerate() {
px[i * 4 + 3] = *v;
}
}
_ => {}
}
d
})
.collect()
}
}
impl VideoDecoder for AlphaDecoder {
fn decode(&mut self, sample: &[u8], pts: i64) -> crate::Result<Vec<crate::video::DecodedFrame>> {
let (main, alpha) = unpack_alpha(sample);
if let Some(a) = alpha {
// a damaged alpha stream leaves the pictures opaque rather than failing them
if let Ok(out) = self.alpha.decode(a, pts) {
self.take_alpha(out);
}
}
let out = self.main.decode(main, pts)?;
Ok(self.attach(out))
}
fn flush(&mut self) -> Vec<crate::video::DecodedFrame> {
let a = self.alpha.flush();
self.take_alpha(a);
let out = self.main.flush();
let out = self.attach(out);
self.planes.clear();
out
}
fn reset(&mut self) {
self.main.reset();
self.alpha.reset();
self.planes.clear();
}
fn name(&self) -> &str {
"FilmCraft VP9 + alpha"
}
fn intra_only(&self) -> bool {
self.main.intra_only() && self.alpha.intra_only()
}
/// Decoding can start only where both streams can.
fn is_random_access(&self, sample: &[u8]) -> Option<bool> {
let (main, alpha) = unpack_alpha(sample);
let m = self.main.is_random_access(main);
match alpha.map(|a| self.alpha.is_random_access(a)) {
Some(Some(false)) => Some(false),
_ => m,
}
}
fn is_disposable(&self, sample: &[u8]) -> bool {
let (main, alpha) = unpack_alpha(sample);
self.main.is_disposable(main) && alpha.is_none_or(|a| self.alpha.is_disposable(a))
}
fn set_draft(&mut self, on: bool) {
self.main.set_draft(on);
self.alpha.set_draft(on);
}
}
impl MediaSource for MkvSource {
fn info(&self) -> &MediaInfo {
&self.info
+63
View File
@@ -429,6 +429,69 @@ fn mkv_vp9_rgb() {
}
}
/// WebM VP9 with alpha (`AlphaMode` 1, alpha stream in BlockAdditions): colour and alpha planes
/// are sample-exact against libvpx (which decodes the alpha stream; ffmpeg's native VP9 decoder
/// ignores it), also after seeks; frames convert to straight-alpha RGBA.
#[test]
fn webm_vp9_alpha_matches_libvpx() {
let name = "alpha_vp9.webm";
let frames = 40usize;
let n = frames.to_string();
let args = [
"-f",
"lavfi",
"-i",
"testsrc2=s=176x144:r=25,format=yuva420p,geq=lum='p(X,Y)':cb='cb(X,Y)':cr='cr(X,Y)':a='mod(X*3+Y+N*7,256)'",
"-frames:v",
&n,
"-c:v",
"libvpx-vp9",
"-pix_fmt",
"yuva420p",
"-g",
"16",
"-b:v",
"400k",
"-deadline",
"realtime",
"-speed",
"8",
];
let Some(path) = fixture_path(name, &args) else { return };
let p = path.to_str().unwrap();
let Some(reference) =
fixture("alpha_vp9.webm.libvpx.yuva420p", &["-c:v", "libvpx-vp9", "-i", p, "-fps_mode", "passthrough", "-f", "rawvideo", "-pix_fmt", "yuva420p"])
else {
return;
};
let bytes: Arc<[u8]> = std::fs::read(&path).unwrap().into();
let src = crate::open_bytes(name, bytes).unwrap();
let info = src.info().clone();
assert_eq!(info.container, "WebM");
let v = info.video.as_ref().unwrap();
assert!(v.has_alpha);
let (w, h) = (176usize, 144usize);
let frame_len = w * h * 5 / 2;
assert_eq!(reference.len(), frame_len * frames);
let rate = info.frame_rate();
let order: Vec<usize> = [frames - 1, 3, 20, 17, 16, 0, 8].into_iter().chain(25..32).collect();
let mut translucent = 0;
for k in order {
let f = src.video_frame(FrameRequest::full(rate.tick_of(k as i64))).unwrap();
let filmcraft_frame::PixelData::Yuv8 { planes, alpha, .. } = &f.data else { panic!("not 8-bit YUV: {}", f.format_label()) };
let r = &reference[k * frame_len..(k + 1) * frame_len];
let colour: Vec<u8> = planes.iter().flat_map(|p| p.iter().copied()).collect();
assert!(colour == r[..w * h * 3 / 2], "frame {k}: colour differs from libvpx");
let a = alpha.as_ref().unwrap_or_else(|| panic!("frame {k}: no alpha plane"));
assert!(a[..] == r[w * h * 3 / 2..], "frame {k}: alpha differs from libvpx");
// straight alpha survives conversion to RGBA
let px = f.to_rgba8();
assert!(px.chunks_exact(4).zip(a.iter()).all(|(p, a)| p[3] == *a), "frame {k}: RGBA alpha");
translucent += a.iter().filter(|&&v| v > 8 && v < 247).count();
}
assert!(translucent > 0, "the fixture's alpha should not be binary");
}
/// ffmpeg + libopus decode of a container fixture (pre-skip / codec delay / edit list applied) as
/// interleaved f32 at 48 kHz: the reference for our Opus path.
fn opus_reference(name: &str) -> Option<Vec<f32>> {
+6 -1
View File
@@ -39,6 +39,10 @@ while let Some(p) = d.next_packet()? { // file order, all tracks
`block ts + i × (BlockDuration or DefaultDuration)/n`; if neither is known, later laces repeat the
block timestamp and have `duration == 0` (`Packet::lace` tells which).
- **Keyframes:** SimpleBlock keyframe flag; BlockGroup = keyframe iff it has no ReferenceBlock.
- **BlockAdditions:** the `BlockAdditional` with `BlockAddID` 1 (the default) of an unlaced
BlockGroup is indexed as `Sample::addition` and read with `MkvFile::read_block_additional`
(`Packet::block_additional` when streaming). With `AlphaMode` 1 (WebM VP9/VP8) it is a second
bitstream whose luma plane is the straight alpha channel; `filmcraft-codecs` decodes it.
- **Lacing:** Xiph, fixed-size, EBML. **Unknown sizes:** Segment (live/streamed output) and
Clusters end at the next top-level element. **CRC-32:** optional verification of level-1
elements and clusters; mismatches go to `MkvFile::crc_errors` (not fatal). **Void** skipped.
@@ -109,7 +113,8 @@ CRC-32, Chapters, Tags, Attachments, or content encodings.
- Only the first Segment is read (no linked/chained segments, no ordered-chapter editions playback).
- zlib/bzlib/lzo compressed and encrypted tracks are detected but their frames are not decoded.
- BlockAdditions (e.g. VP9 alpha, WebVTT settings in Matroska) are skipped; `EncryptedBlock` ignored.
- Only `BlockAddID` 1 BlockAdditions are exposed (others, e.g. ITU-T T.35 HDR metadata, are
skipped), and none for laced blocks; `EncryptedBlock` ignored. The muxer does not write them.
- `TrackTimestampScale` (deprecated) is ignored.
- Lace timestamps without any duration information repeat the block timestamp (FFmpeg derives them
from codec parsers).
+65 -1
View File
@@ -98,6 +98,10 @@ pub struct Packet {
/// Frame bytes with header stripping undone. Frames of tracks with other content encodings
/// (zlib, encryption) are returned as stored; see [`Track::frames_readable`].
pub data: Vec<u8>,
/// `BlockAdditional` payload with `BlockAddID` 1 from the enclosing BlockGroup's
/// `BlockAdditions` (unlaced blocks only): the alpha bitstream of WebM VP9 / VP8 tracks with
/// [`crate::VideoInfo::alpha_mode`] 1. `None` for SimpleBlocks and groups without one.
pub block_additional: Option<Vec<u8>>,
}
/// Result of [`Demuxer::seek`].
@@ -301,6 +305,8 @@ struct BlockRef {
discardable: bool,
invisible: bool,
discard_padding: Option<i64>,
/// `BlockAdditional` with `BlockAddID` 1: absolute (offset, size).
addition: Option<(u64, u32)>,
}
#[derive(Clone, Copy, Debug)]
@@ -318,6 +324,7 @@ struct FrameRef {
discard_padding: Option<i64>,
block_offset: u64,
lace: u16,
addition: Option<(u64, u32)>,
}
fn track_lookup(tracks: &[Track]) -> impl Fn(u64) -> Option<usize> + '_ {
@@ -376,12 +383,13 @@ fn parse_block_elem<S: ByteSource + ?Sized>(
discardable: flags & 0x01 != 0,
invisible: flags & 0x08 != 0,
discard_padding: None,
addition: None,
}))
}
BLOCK_GROUP => {
let mut pos = ds;
let mut block = None;
let (mut dur, mut has_ref, mut pad) = (None, false, None);
let (mut dur, mut has_ref, mut pad, mut addition) = (None, false, None, None);
while pos < end {
let Some(c) = io.header(pos, end)? else { break };
let ce = c.end(end);
@@ -390,6 +398,7 @@ fn parse_block_elem<S: ByteSource + ?Sized>(
BLOCK_DURATION => dur = Some(ebml::uint(io.peek(c.data_start(), (ce - c.data_start()) as usize)?)),
REFERENCE_BLOCK => has_ref = true,
DISCARD_PADDING => pad = Some(ebml::int(io.peek(c.data_start(), (ce - c.data_start()) as usize)?)),
BLOCK_ADDITIONS => addition = block_addition(io, c.data_start(), ce)?.or(addition),
_ => {}
}
pos = ce;
@@ -405,12 +414,44 @@ fn parse_block_elem<S: ByteSource + ?Sized>(
discardable: false,
invisible: flags & 0x08 != 0,
discard_padding: pad,
addition,
}))
}
_ => Ok(None),
}
}
/// The `BlockAdditional` with `BlockAddID` 1 (the default) among the `BlockMore` children of a
/// `BlockAdditions` element spanning `start..end`, as an absolute (offset, size).
fn block_addition<S: ByteSource + ?Sized>(io: &mut Io<'_, S>, start: u64, end: u64) -> Result<Option<(u64, u32)>> {
let mut pos = start;
while pos < end {
let Some(m) = io.header(pos, end)? else { break };
let me = m.end(end);
if m.id == BLOCK_MORE {
let (mut id, mut data) = (1u64, None);
let mut cp = m.data_start();
while cp < me {
let Some(c) = io.header(cp, me)? else { break };
let ce = c.end(me);
match c.id {
BLOCK_ADD_ID => id = ebml::uint(io.peek(c.data_start(), (ce - c.data_start()) as usize)?),
BLOCK_ADDITIONAL => data = Some((c.data_start(), (ce - c.data_start()) as u32)),
_ => {}
}
cp = ce;
}
if id == 1
&& let Some(d) = data
{
return Ok(Some(d));
}
}
pos = me;
}
Ok(None)
}
/// Expand a block into frames with per-lace timestamps.
fn expand(b: &BlockRef, t: &Track, scale: u64, skip: u16, out: &mut impl Extend<FrameRef>) {
let n = b.frames.len() as u64;
@@ -450,6 +491,8 @@ fn expand(b: &BlockRef, t: &Track, scale: u64, skip: u16, out: &mut impl Extend<
discard_padding: b.discard_padding,
block_offset: b.block_offset,
lace: i as u16,
// additions belong to the whole block: only an unlaced frame owns them unambiguously
addition: if n == 1 { b.addition } else { None },
}
}));
}
@@ -782,6 +825,7 @@ fn add_blocks(file: &mut MkvFile, cluster: u32, blocks: &[BlockRef]) {
cluster,
block_offset: f.block_offset,
lace: f.lace,
addition: f.addition,
});
}
}
@@ -840,6 +884,17 @@ impl MkvFile {
Ok(data)
}
/// The `BlockAdditional` (`BlockAddID` 1) of sample `index` of `track`, if its block has one:
/// for WebM VP9 / VP8 with [`crate::VideoInfo::alpha_mode`] 1, the alpha bitstream.
pub fn read_block_additional<S: ByteSource + ?Sized>(&self, src: &S, track: usize, index: usize) -> Result<Option<Vec<u8>>> {
let t = self.tracks.get(track).ok_or(Error::NoSuchTrack(track))?;
let s = t.samples.get(index).ok_or(Error::NoSuchSample(index))?;
let Some((offset, size)) = s.addition else { return Ok(None) };
let mut data = vec![0; size as usize];
src.read_at(offset, &mut data)?;
Ok(Some(data))
}
/// Keyframe sample of `track` with the greatest pts ≤ `time_ns` (indexed files only).
pub fn keyframe_before(&self, track: usize, time_ns: i64) -> Option<usize> {
let t = self.tracks.get(track)?;
@@ -1045,6 +1100,14 @@ impl<S: ByteSource> Demuxer<S> {
} else {
self.src.read_at(f.offset, &mut data[at..])?;
}
let block_additional = match f.addition {
Some((offset, size)) => {
let mut a = vec![0; size as usize];
self.src.read_at(offset, &mut a)?;
Some(a)
}
None => None,
};
Ok(Packet {
track: f.track,
track_number: t.number,
@@ -1059,6 +1122,7 @@ impl<S: ByteSource> Demuxer<S> {
offset: f.offset,
lace: f.lace,
data,
block_additional,
})
}
+3
View File
@@ -55,6 +55,9 @@ 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_MORE: u32 = 0xA6;
pub const BLOCK_ADD_ID: u32 = 0xEE;
pub const BLOCK_ADDITIONAL: u32 = 0xA5;
pub const BLOCK_DURATION: u32 = 0x9B;
pub const REFERENCE_PRIORITY: u32 = 0xFA;
pub const REFERENCE_BLOCK: u32 = 0xFB;
+65
View File
@@ -201,3 +201,68 @@ fn synthetic_file() {
assert_eq!(d.next_packet().unwrap().unwrap().pts, 1120);
assert!(d.file().indexed);
}
/// WebM VP9 with `AlphaMode` 1: the alpha bitstream rides in `BlockAdditions` (`BlockMore` with
/// `BlockAddID` 1, the default when absent); SimpleBlocks and other IDs carry none.
#[test]
fn block_additions_alpha() {
let mut f = Vec::new();
let mut h = Vec::new();
el_str(&mut h, DOC_TYPE, "webm");
el(&mut f, EBML, &h);
let mut seg = Vec::new();
let mut info = Vec::new();
el_uint(&mut info, TIMESTAMP_SCALE, 1_000_000);
el(&mut seg, INFO, &info);
let mut v = Vec::new();
el_uint(&mut v, TRACK_NUMBER, 1);
el_uint(&mut v, TRACK_TYPE, 1);
el_str(&mut v, CODEC_ID, "V_VP9");
let mut vid = Vec::new();
el_uint(&mut vid, PIXEL_WIDTH, 16);
el_uint(&mut vid, PIXEL_HEIGHT, 16);
el_uint(&mut vid, ALPHA_MODE, 1);
el(&mut v, VIDEO, &vid);
let mut tr = Vec::new();
el(&mut tr, TRACK_ENTRY, &v);
el(&mut seg, TRACKS, &tr);
let group = |rel: i16, key: bool, data: &[u8], more: &[(Option<u64>, &[u8])]| {
let mut g = Vec::new();
el(&mut g, BLOCK, &block(1, rel, 0, data));
if !key {
el(&mut g, REFERENCE_BLOCK, &[0xD8]);
}
let mut adds = Vec::new();
for (id, payload) in more {
let mut m = Vec::new();
if let Some(id) = id {
el_uint(&mut m, BLOCK_ADD_ID, *id);
}
el(&mut m, BLOCK_ADDITIONAL, payload);
el(&mut adds, BLOCK_MORE, &m);
}
if !more.is_empty() {
el(&mut g, BLOCK_ADDITIONS, &adds);
}
g
};
let mut cl = Vec::new();
el_uint(&mut cl, TIMESTAMP, 0);
el(&mut cl, BLOCK_GROUP, &group(0, true, &[1, 1], &[(Some(1), &[0xA1, 0xA1, 0xA1])]));
el(&mut cl, BLOCK_GROUP, &group(40, false, &[2], &[(Some(4), &[9]), (None, &[0xA2])]));
el(&mut cl, SIMPLE_BLOCK, &block(1, 80, 0, &[3]));
el(&mut cl, BLOCK_GROUP, &group(120, false, &[4], &[(Some(2), &[9])]));
el(&mut seg, CLUSTER, &cl);
el(&mut f, SEGMENT, &seg);
let file = open_with(&f[..], &OpenOptions::default()).unwrap();
let t = &file.tracks[0];
assert_eq!(t.video.as_ref().unwrap().alpha_mode, 1);
assert_eq!(t.samples.len(), 4);
let adds: Vec<Option<Vec<u8>>> = (0..4).map(|i| file.read_block_additional(&f[..], 0, i).unwrap()).collect();
assert_eq!(adds, vec![Some(vec![0xA1; 3]), Some(vec![0xA2]), None, None]);
assert_eq!(file.read_sample(&f[..], 0, 0).unwrap(), [1, 1]);
let p: Vec<Packet> = Demuxer::from_slice(&f).unwrap().map(|p| p.unwrap()).collect();
assert_eq!(p.iter().map(|p| p.block_additional.clone()).collect::<Vec<_>>(), adds);
assert_eq!(p.iter().map(|p| p.data.clone()).collect::<Vec<_>>(), vec![vec![1, 1], vec![2], vec![3], vec![4]]);
}
+6 -1
View File
@@ -91,7 +91,8 @@ pub struct VideoInfo {
/// `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.
/// `AlphaMode`: 1 if BlockAdditions (`BlockAddID` 1) carry alpha: for VP9/VP8, a second
/// bitstream of the same codec whose luma plane is the (straight) alpha channel.
pub alpha_mode: u64,
/// `ColourSpace` FourCC (uncompressed video).
pub colour_space: Option<[u8; 4]>,
@@ -166,6 +167,10 @@ pub struct Sample {
pub block_offset: u64,
/// Index of this frame within a laced block.
pub lace: u16,
/// Absolute (offset, size) of the `BlockAdditional` with `BlockAddID` 1 of the enclosing
/// BlockGroup (RFC 9559 §5.1.3.5.2; for WebM VP9 with `AlphaMode` 1, the alpha bitstream).
/// Only set for unlaced blocks. See [`crate::MkvFile::read_block_additional`].
pub addition: Option<(u64, u32)>,
}
/// A Matroska track (`TrackEntry`) plus, once indexed, its sample table.