Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c5ba7a726 |
No files matched your search
+180
-1
@@ -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
|
||||
|
||||
@@ -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>> {
|
||||
|
||||
@@ -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).
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]]);
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user