mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-13 22:12:12 +00:00
Merge pull request #1154 from storytold/codex/778-h264-picnum
video: harden H.264 and configure frame limits
This commit is contained in:
8 files changed
+256
-31
No files matched your search
@@ -99,7 +99,8 @@ A picture is refused when the pictures the decoder would hold do not fit
|
||||
1 GiB on 32-bit targets): the DPB (the larger of its slots at the new size and what it holds
|
||||
now), the picture being decoded, and what is in flight or waiting in the output queue. Pictures
|
||||
already held count at their own size, and a buffer shared by the DPB and the queue counts once.
|
||||
The per-picture size cap applies as well.
|
||||
The default per-picture luma cap is 2^25 samples; applications with a different bounded frame
|
||||
policy can raise it with `Decoder::set_max_picture_luma_samples`.
|
||||
|
||||
`configure_avcc` checks every entry of the record before it handles any: the first list holds
|
||||
SPS NAL units and the second PPS NAL units. A record that carries a picture slice is an error and
|
||||
|
||||
@@ -115,6 +115,8 @@ pub struct Decoder {
|
||||
/// Bytes the picture store may take ([`crate::budget`]).
|
||||
memory_budget: u64,
|
||||
threads: usize,
|
||||
/// Maximum reconstructed luma samples in one picture.
|
||||
max_luma_samples: u64,
|
||||
/// Draft mode: skip deblocking of non-reference pictures ([`Decoder::set_draft`]).
|
||||
draft: bool,
|
||||
}
|
||||
@@ -179,6 +181,7 @@ impl Decoder {
|
||||
max_in_flight: threads.max(1) + 2,
|
||||
memory_budget: crate::budget::DECODE_MEMORY_BUDGET,
|
||||
threads: workers,
|
||||
max_luma_samples: super::params::MAX_LUMA_SAMPLES,
|
||||
draft: false,
|
||||
}
|
||||
}
|
||||
@@ -391,7 +394,7 @@ impl Decoder {
|
||||
// Before the header fields that depend on the SPS: they are validated for the streams
|
||||
// we decode (a 10-bit slice may have a QP below 0), so a stream we cannot decode must
|
||||
// fail as unsupported here, not as invalid there (#626).
|
||||
sps.check_supported()?;
|
||||
sps.check_supported_with_limit(self.max_luma_samples)?;
|
||||
found = Some((pps.clone(), sps.clone()));
|
||||
Ok((&**pps, &**sps))
|
||||
})?;
|
||||
@@ -468,6 +471,14 @@ impl Decoder {
|
||||
self.memory_budget = bytes;
|
||||
}
|
||||
|
||||
/// Set the maximum luma sample count allowed in a single decoded picture.
|
||||
///
|
||||
/// The default is 2^25 samples. This is a per-picture geometry guard; the separate
|
||||
/// memory budget controls aggregate decoded picture storage.
|
||||
pub fn set_max_picture_luma_samples(&mut self, samples: u64) {
|
||||
self.max_luma_samples = samples;
|
||||
}
|
||||
|
||||
/// Refuse an SPS whose pictures would not fit the memory budget, before anything is allocated.
|
||||
fn check_budget(&self, sps: &Sps) -> Result<()> {
|
||||
let chroma = sps.chroma_array_type();
|
||||
|
||||
@@ -176,6 +176,7 @@ impl<F: Clone> Dpb<F> {
|
||||
return Ok(lists);
|
||||
}
|
||||
let cur_fn = sh.frame_num;
|
||||
ensure!(max_frame_num > 0 && max_frame_num <= (1 << 16) && cur_fn < max_frame_num, "frame number out of range");
|
||||
let pic_num = |e: &DpbEntry<F>| Self::frame_num_wrap(e, cur_fn, max_frame_num);
|
||||
let short: Vec<&DpbEntry<F>> = self.entries.iter().filter(|e| e.mark == RefMark::Short).collect();
|
||||
let mut long: Vec<&DpbEntry<F>> = self.entries.iter().filter(|e| e.mark == RefMark::Long).collect();
|
||||
@@ -212,6 +213,7 @@ impl<F: Clone> Dpb<F> {
|
||||
let mut ref_idx = 0usize;
|
||||
for m in &sh.ref_pic_list_mod[l] {
|
||||
ensure!(ref_idx < n, "too many ref_pic_list_modification entries");
|
||||
ensure!(m.idc >= 2 || m.value < max_frame_num, "abs_diff_pic_num_minus1 out of range");
|
||||
list.push(None); // temporarily one longer
|
||||
let (target, is_long): (Option<&DpbEntry<F>>, bool) = match m.idc {
|
||||
0 | 1 => {
|
||||
@@ -272,13 +274,13 @@ impl<F: Clone> Dpb<F> {
|
||||
/// Adaptive memory control (8.2.5.4). Returns true if MMCO 5 was executed.
|
||||
fn apply_mmcos(&mut self, mmcos: &[Mmco], cur_frame_num: u32, max_frame_num: u32, cur_long: &mut Option<u32>) -> Result<bool> {
|
||||
let mut had5 = false;
|
||||
let curr_pic_num = cur_frame_num as i32;
|
||||
let curr_pic_num = i64::from(cur_frame_num);
|
||||
for m in mmcos {
|
||||
match m.op {
|
||||
1 => {
|
||||
let pn = curr_pic_num - (m.difference_of_pic_nums_minus1 as i32 + 1);
|
||||
let pn = curr_pic_num - (i64::from(m.difference_of_pic_nums_minus1) + 1);
|
||||
for e in &mut self.entries {
|
||||
if e.mark == RefMark::Short && Self::frame_num_wrap(e, cur_frame_num, max_frame_num) == pn {
|
||||
if e.mark == RefMark::Short && i64::from(Self::frame_num_wrap(e, cur_frame_num, max_frame_num)) == pn {
|
||||
e.mark = RefMark::Unused;
|
||||
}
|
||||
}
|
||||
@@ -295,14 +297,14 @@ impl<F: Clone> Dpb<F> {
|
||||
self.max_long_term_frame_idx.is_some_and(|max| m.long_term_frame_idx <= max),
|
||||
"MMCO long-term index exceeds the configured limit"
|
||||
);
|
||||
let pn = curr_pic_num - (m.difference_of_pic_nums_minus1 as i32 + 1);
|
||||
let pn = curr_pic_num - (i64::from(m.difference_of_pic_nums_minus1) + 1);
|
||||
for e in &mut self.entries {
|
||||
if e.mark == RefMark::Long && e.long_term_frame_idx == m.long_term_frame_idx {
|
||||
e.mark = RefMark::Unused;
|
||||
}
|
||||
}
|
||||
for e in &mut self.entries {
|
||||
if e.mark == RefMark::Short && Self::frame_num_wrap(e, cur_frame_num, max_frame_num) == pn {
|
||||
if e.mark == RefMark::Short && i64::from(Self::frame_num_wrap(e, cur_frame_num, max_frame_num)) == pn {
|
||||
e.mark = RefMark::Long;
|
||||
e.long_term_frame_idx = m.long_term_frame_idx;
|
||||
}
|
||||
@@ -461,6 +463,83 @@ mod tests {
|
||||
})
|
||||
}
|
||||
|
||||
fn reference_slice() -> SliceHeader {
|
||||
SliceHeader {
|
||||
nal_ref_idc: 1,
|
||||
nal_unit_type: 1,
|
||||
idr: false,
|
||||
first_mb_in_slice: 0,
|
||||
slice_type_raw: 0,
|
||||
slice_type: SliceType::P,
|
||||
pps_id: 0,
|
||||
colour_plane_id: 0,
|
||||
frame_num: 0,
|
||||
field_pic: false,
|
||||
bottom_field: false,
|
||||
idr_pic_id: 0,
|
||||
pic_order_cnt_lsb: 0,
|
||||
delta_pic_order_cnt_bottom: 0,
|
||||
delta_pic_order_cnt: [0; 2],
|
||||
redundant_pic_cnt: 0,
|
||||
direct_spatial_mv_pred: false,
|
||||
num_ref_idx_active: [1, 0],
|
||||
ref_pic_list_mod: [Vec::new(), Vec::new()],
|
||||
pred_weight_table: None,
|
||||
no_output_of_prior_pics: false,
|
||||
long_term_reference: false,
|
||||
adaptive_ref_pic_marking: false,
|
||||
mmcos: Vec::new(),
|
||||
cabac_init_idc: 0,
|
||||
slice_qp_delta: 0,
|
||||
sp_for_switch: false,
|
||||
slice_qs_delta: 0,
|
||||
disable_deblocking_filter_idc: 0,
|
||||
slice_alpha_c0_offset_div2: 0,
|
||||
slice_beta_offset_div2: 0,
|
||||
slice_group_change_cycle: 0,
|
||||
header_bits: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_picture_number_differences_do_not_overflow() {
|
||||
for op in [1, 3] {
|
||||
for difference in [i32::MAX as u32, 1 << 31, u32::MAX] {
|
||||
let mut dpb: Dpb<u32> = Dpb::new();
|
||||
dpb.max_long_term_frame_idx = Some(0);
|
||||
let op = Mmco {
|
||||
op,
|
||||
difference_of_pic_nums_minus1: difference,
|
||||
long_term_pic_num: 0,
|
||||
long_term_frame_idx: 0,
|
||||
max_long_term_frame_idx_plus1: 0,
|
||||
};
|
||||
assert!(dpb.apply_mmcos(&[op], 5, 16, &mut None).is_ok());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_reference_list_differences_are_rejected() {
|
||||
use crate::h264::decode::slice::RefPicListMod;
|
||||
for idc in [0, 1] {
|
||||
for value in [0, 15] {
|
||||
let mut sh = reference_slice();
|
||||
sh.frame_num = 5;
|
||||
sh.ref_pic_list_mod[0].push(RefPicListMod { idc, value });
|
||||
let dpb: Dpb<u32> = Dpb::new();
|
||||
assert!(dpb.build_ref_lists(&sh, 0, 16, |entry| entry.frame).is_ok());
|
||||
}
|
||||
for value in [16, i32::MAX as u32, 1 << 31, u32::MAX] {
|
||||
let mut sh = reference_slice();
|
||||
sh.frame_num = 5;
|
||||
sh.ref_pic_list_mod[0].push(RefPicListMod { idc, value });
|
||||
let dpb: Dpb<u32> = Dpb::new();
|
||||
assert!(dpb.build_ref_lists(&sh, 0, 16, |entry| entry.frame).is_err());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A huge frame_num jump must only materialise the frames the sliding window keeps (#2779).
|
||||
#[test]
|
||||
fn frame_num_gap_materialises_at_most_max_num_ref_frames() {
|
||||
|
||||
@@ -6,7 +6,7 @@ use craft_media::bitstream::BitReader;
|
||||
|
||||
/// Maximum supported reconstructed luma area. 8K UHD fits comfortably while
|
||||
/// hostile SPS dimensions cannot allocate full pictures and decoder work state.
|
||||
const MAX_LUMA_SAMPLES: u64 = 1 << 25;
|
||||
pub(super) const MAX_LUMA_SAMPLES: u64 = 1 << 25;
|
||||
|
||||
/// Scaling matrices after applying fall-back rules, stored in raster order (weightScale).
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@@ -527,7 +527,11 @@ impl Sps {
|
||||
}
|
||||
|
||||
pub fn check_supported(&self) -> Result<()> {
|
||||
if u64::from(self.width()).saturating_mul(u64::from(self.height())) > MAX_LUMA_SAMPLES {
|
||||
self.check_supported_with_limit(MAX_LUMA_SAMPLES)
|
||||
}
|
||||
|
||||
pub(super) fn check_supported_with_limit(&self, max_luma_samples: u64) -> Result<()> {
|
||||
if u64::from(self.width()).saturating_mul(u64::from(self.height())) > max_luma_samples {
|
||||
return unsupported("picture dimensions exceed the decoder memory limit");
|
||||
}
|
||||
if !self.frame_mbs_only {
|
||||
@@ -744,6 +748,31 @@ mod tests {
|
||||
assert_eq!(sps.pic_order_cnt_type, 2);
|
||||
assert_eq!(sps.crop_rect(), (0, 0, 176, 144));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn caller_can_raise_single_picture_sample_limit() {
|
||||
let mut w = BitWriter::new();
|
||||
w.write_bits(66, 8);
|
||||
w.write_bits(0xC0, 8);
|
||||
w.write_bits(30, 8);
|
||||
w.write_ue(0);
|
||||
w.write_ue(0);
|
||||
w.write_ue(2);
|
||||
w.write_ue(1);
|
||||
w.write_bit(false);
|
||||
w.write_ue(511); // 8192 luma samples wide
|
||||
w.write_ue(511); // 8192 luma samples high
|
||||
w.write_bit(true);
|
||||
w.write_bit(true);
|
||||
w.write_bit(false);
|
||||
w.write_bit(false);
|
||||
w.rbsp_trailing();
|
||||
|
||||
let sps = Sps::parse(&w.finish()).unwrap();
|
||||
assert_eq!((sps.width(), sps.height()), (8192, 8192));
|
||||
assert!(sps.check_supported().is_err());
|
||||
assert!(sps.check_supported_with_limit(1 << 26).is_ok());
|
||||
}
|
||||
/// Baseline SPS with the given `ue(v)` overrides for the fields hostile streams can max out.
|
||||
fn hostile_sps(profile: u32, bit_depth: u32, log2_frame_num: u32, w: u32, h: u32, crop: u32) -> Vec<u8> {
|
||||
let mut bw = BitWriter::new();
|
||||
|
||||
@@ -462,39 +462,42 @@ impl PocState {
|
||||
let (prev_msb, prev_lsb) = if sh.idr { (0, 0) } else { (self.prev_ref_msb, self.prev_ref_lsb) };
|
||||
let max_lsb = 1i32 << sps.log2_max_poc_lsb;
|
||||
let lsb = sh.pic_order_cnt_lsb as i32;
|
||||
let msb = if lsb < prev_lsb && prev_lsb.wrapping_sub(lsb) >= max_lsb / 2 {
|
||||
prev_msb.wrapping_add(max_lsb)
|
||||
} else if lsb > prev_lsb && lsb.wrapping_sub(prev_lsb) > max_lsb / 2 {
|
||||
prev_msb.wrapping_sub(max_lsb)
|
||||
let msb = if lsb < prev_lsb && prev_lsb.saturating_sub(lsb) >= max_lsb / 2 {
|
||||
prev_msb.saturating_add(max_lsb)
|
||||
} else if lsb > prev_lsb && lsb.saturating_sub(prev_lsb) > max_lsb / 2 {
|
||||
prev_msb.saturating_sub(max_lsb)
|
||||
} else {
|
||||
prev_msb
|
||||
};
|
||||
let top = msb.wrapping_add(lsb);
|
||||
let bottom = top.wrapping_add(sh.delta_pic_order_cnt_bottom);
|
||||
let top = msb.saturating_add(lsb);
|
||||
let bottom = top.saturating_add(sh.delta_pic_order_cnt_bottom);
|
||||
Poc { top, bottom, msb, frame_num_offset: 0 }
|
||||
}
|
||||
1 => {
|
||||
let frame_num_offset = self.frame_num_offset(sh.idr, sh.frame_num, sps);
|
||||
let n = sps.offset_for_ref_frame.len() as i32;
|
||||
let mut abs_frame_num = if n != 0 { frame_num_offset.wrapping_add(sh.frame_num as i32) } else { 0 };
|
||||
let n = sps.offset_for_ref_frame.len() as i64;
|
||||
let mut abs_frame_num = if n != 0 { i64::from(frame_num_offset) + i64::from(sh.frame_num) } else { 0 };
|
||||
if sh.nal_ref_idc == 0 && abs_frame_num > 0 {
|
||||
abs_frame_num -= 1;
|
||||
}
|
||||
let mut expected = 0i32;
|
||||
let mut expected = 0i64;
|
||||
if abs_frame_num > 0 {
|
||||
let cycle_cnt = (abs_frame_num - 1) / n;
|
||||
let in_cycle = (abs_frame_num - 1) % n;
|
||||
let delta_cycle = sps.offset_for_ref_frame.iter().fold(0i32, |a, &o| a.wrapping_add(o));
|
||||
expected = cycle_cnt.wrapping_mul(delta_cycle);
|
||||
let delta_cycle = sps.offset_for_ref_frame.iter().map(|&o| i64::from(o)).sum::<i64>();
|
||||
expected = cycle_cnt.saturating_mul(delta_cycle);
|
||||
for &o in &sps.offset_for_ref_frame[..=in_cycle as usize] {
|
||||
expected = expected.wrapping_add(o);
|
||||
expected = expected.saturating_add(i64::from(o));
|
||||
}
|
||||
}
|
||||
if sh.nal_ref_idc == 0 {
|
||||
expected = expected.wrapping_add(sps.offset_for_non_ref_pic);
|
||||
expected = expected.saturating_add(i64::from(sps.offset_for_non_ref_pic));
|
||||
}
|
||||
let top = expected.wrapping_add(sh.delta_pic_order_cnt[0]);
|
||||
let bottom = top.wrapping_add(sps.offset_for_top_to_bottom_field).wrapping_add(sh.delta_pic_order_cnt[1]);
|
||||
let top = expected.saturating_add(i64::from(sh.delta_pic_order_cnt[0])).clamp(i64::from(i32::MIN), i64::from(i32::MAX)) as i32;
|
||||
let bottom = i64::from(top)
|
||||
.saturating_add(i64::from(sps.offset_for_top_to_bottom_field))
|
||||
.saturating_add(i64::from(sh.delta_pic_order_cnt[1]))
|
||||
.clamp(i64::from(i32::MIN), i64::from(i32::MAX)) as i32;
|
||||
Poc { top, bottom, msb: 0, frame_num_offset }
|
||||
}
|
||||
_ => {
|
||||
@@ -502,9 +505,9 @@ impl PocState {
|
||||
let temp = if sh.idr {
|
||||
0
|
||||
} else if sh.nal_ref_idc == 0 {
|
||||
2i32.wrapping_mul(frame_num_offset.wrapping_add(sh.frame_num as i32)).wrapping_sub(1)
|
||||
2i32.saturating_mul(frame_num_offset.saturating_add(sh.frame_num as i32)).saturating_sub(1)
|
||||
} else {
|
||||
2i32.wrapping_mul(frame_num_offset.wrapping_add(sh.frame_num as i32))
|
||||
2i32.saturating_mul(frame_num_offset.saturating_add(sh.frame_num as i32))
|
||||
};
|
||||
Poc { top: temp, bottom: temp, msb: 0, frame_num_offset }
|
||||
}
|
||||
@@ -515,7 +518,7 @@ impl PocState {
|
||||
if idr {
|
||||
0
|
||||
} else if self.prev_frame_num > frame_num {
|
||||
self.prev_frame_num_offset.wrapping_add(sps.max_frame_num() as i32)
|
||||
self.prev_frame_num_offset.saturating_add(sps.max_frame_num() as i32)
|
||||
} else {
|
||||
self.prev_frame_num_offset
|
||||
}
|
||||
@@ -528,7 +531,7 @@ impl PocState {
|
||||
if sh.nal_ref_idc != 0 {
|
||||
if mmco5 {
|
||||
self.prev_ref_msb = 0;
|
||||
self.prev_ref_lsb = poc.top.wrapping_sub(poc.frame());
|
||||
self.prev_ref_lsb = poc.top.saturating_sub(poc.frame());
|
||||
} else {
|
||||
self.prev_ref_msb = poc.msb;
|
||||
self.prev_ref_lsb = sh.pic_order_cnt_lsb as i32;
|
||||
@@ -624,6 +627,14 @@ mod poc_tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extreme_bottom_delta_saturates_instead_of_reversing_picture_order() {
|
||||
let sps = sps(0);
|
||||
let sh = header(0, 1, i32::MAX, [0, 0], 1);
|
||||
let poc = PocState::default().compute(&sh, &sps);
|
||||
assert_eq!((poc.top, poc.bottom), (1, i32::MAX));
|
||||
}
|
||||
|
||||
/// Offsets and deltas are se(v) values that a stream may set anywhere in the i32 range.
|
||||
#[test]
|
||||
fn poc_does_not_overflow_on_extreme_deltas() {
|
||||
@@ -654,4 +665,23 @@ mod poc_tests {
|
||||
let _ = poc.frame();
|
||||
state.update(&sh, &poc);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn poc_type1_cancellation_happens_before_clamping() {
|
||||
let sps = sps(1);
|
||||
let sh = header(0, 0, 0, [-i32::MAX, 0], 1);
|
||||
let state = PocState { prev_frame_num_offset: 2, ..PocState::default() };
|
||||
let poc = state.compute(&sh, &sps);
|
||||
assert_eq!(poc.top, i32::MAX);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn poc_type1_saturates_only_after_wide_cycle_arithmetic_exceeds_i64() {
|
||||
let mut sps = sps(1);
|
||||
sps.offset_for_ref_frame = vec![i32::MAX; 255];
|
||||
let sh = header(u32::MAX, 0, 0, [0, 0], 1);
|
||||
let state = PocState { prev_frame_num_offset: i32::MAX, ..PocState::default() };
|
||||
let poc = state.compute(&sh, &sps);
|
||||
assert_eq!(poc.top, i32::MAX);
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,8 @@ use craft_video::prores::*;
|
||||
let frame: Frame = decode_frame(&sample)?; // 10-bit 4:2:2 / 12-bit 4:4:4
|
||||
let f10 = decode_frame_with(&sample, &DecodeOptions { bit_depth: Some(10), threads: true })?;
|
||||
decode_frame_into(&sample, &DecodeOptions::default(), &mut frame_buf)?; // reuses planes
|
||||
let limits = DecodeLimits { max_pixels: 8192 * 8192, max_frame_bytes: 512 * 1024 * 1024 };
|
||||
let bounded = decode_frame_with_limits(&sample, &DecodeOptions::default(), &limits)?;
|
||||
let hdr: FrameHeader = probe(&sample)?; // header only
|
||||
|
||||
// Encode (progressive).
|
||||
@@ -28,6 +30,11 @@ alpha: Option<Vec<u16>>, interlace: Interlace, color: ColorInfo { primaries, tra
|
||||
aspect_ratio, frame_rate_code }` holds planar samples with no row padding (`chroma_width()` is
|
||||
`ceil(width/2)` for 4:2:2). The colour codes are H.273 code points straight from the frame header.
|
||||
|
||||
`DecodeLimits` bounds both frame area and aggregate decoded plane storage before allocation.
|
||||
`decode_frame_with` uses an aggregate limit of 300 MiB and does not impose an additional area
|
||||
limit; use `decode_frame_with_limits` when the application needs a stricter pixel policy or a
|
||||
different finite memory ceiling.
|
||||
|
||||
`Profile` = `Proxy` (`apco`), `Lt` (`apcs`), `Standard` (`apcn`), `Hq` (`apch`), `P4444` (`ap4h`),
|
||||
`P4444Xq` (`ap4x`). The frame itself doesn't say which profile it is (only the container fourcc
|
||||
does), so the decoder doesn't need to know.
|
||||
|
||||
@@ -11,6 +11,21 @@ use crate::prores::{AlphaType, ChromaFormat, Error, Frame, Interlace, Result};
|
||||
/// Maximum aggregate decoded plane storage for one ProRes frame (300 MiB; 8K ProRes 4444 with alpha is ~265 MiB).
|
||||
const MAX_FRAME_BYTES: usize = 300 * 1024 * 1024;
|
||||
|
||||
/// Resource limits for decoding one ProRes frame.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct DecodeLimits {
|
||||
/// Maximum frame width × height. Checked before any plane allocation.
|
||||
pub max_pixels: u64,
|
||||
/// Maximum aggregate output plane storage in bytes.
|
||||
pub max_frame_bytes: usize,
|
||||
}
|
||||
|
||||
impl Default for DecodeLimits {
|
||||
fn default() -> Self {
|
||||
Self { max_pixels: u64::MAX, max_frame_bytes: MAX_FRAME_BYTES }
|
||||
}
|
||||
}
|
||||
|
||||
/// Decoder options.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct DecodeOptions {
|
||||
@@ -105,15 +120,28 @@ impl Scratch {
|
||||
|
||||
/// Decode one ProRes frame.
|
||||
pub fn decode_frame_with(data: &[u8], opts: &DecodeOptions) -> Result<Frame> {
|
||||
decode_frame_with_limits(data, opts, &DecodeLimits::default())
|
||||
}
|
||||
|
||||
/// Decode one ProRes frame with explicit geometry and aggregate-memory limits.
|
||||
pub fn decode_frame_with_limits(data: &[u8], opts: &DecodeOptions, limits: &DecodeLimits) -> Result<Frame> {
|
||||
let mut f = Frame::new(0, 0, ChromaFormat::Yuv422, 10, false)?;
|
||||
decode_frame_into(data, opts, &mut f)?;
|
||||
decode_frame_into_with_limits(data, opts, limits, &mut f)?;
|
||||
Ok(f)
|
||||
}
|
||||
|
||||
/// Decode one ProRes frame into `out`, reusing its plane allocations (the fastest way to decode
|
||||
/// a sequence). On error the contents of `out` are unspecified.
|
||||
pub fn decode_frame_into(data: &[u8], opts: &DecodeOptions, out: &mut Frame) -> Result<()> {
|
||||
decode_frame_into_with_limits(data, opts, &DecodeLimits::default(), out)
|
||||
}
|
||||
|
||||
/// Decode one ProRes frame into `out` under explicit resource limits.
|
||||
pub fn decode_frame_into_with_limits(data: &[u8], opts: &DecodeOptions, limits: &DecodeLimits, out: &mut Frame) -> Result<()> {
|
||||
let (hdr, mut off, frame_size) = FrameHeader::parse(data)?;
|
||||
if u64::from(hdr.width) * u64::from(hdr.height) > limits.max_pixels {
|
||||
return Err(Error::Unsupported("frame size"));
|
||||
}
|
||||
let data = &data[..frame_size];
|
||||
let depth = opts.bit_depth.unwrap_or(match hdr.chroma {
|
||||
ChromaFormat::Yuv422 => 10,
|
||||
@@ -131,7 +159,7 @@ pub fn decode_frame_into(data: &[u8], opts: &DecodeOptions, out: &mut Frame) ->
|
||||
.checked_add(chroma.checked_mul(2).ok_or(Error::Invalid("chroma plane count overflows"))?)
|
||||
.and_then(|n| if hdr.alpha == AlphaType::None { Some(n) } else { n.checked_add(luma) })
|
||||
.ok_or(Error::Invalid("frame plane count overflows"))?;
|
||||
if samples.checked_mul(std::mem::size_of::<u16>()).is_none_or(|n| n > MAX_FRAME_BYTES) {
|
||||
if samples.checked_mul(std::mem::size_of::<u16>()).is_none_or(|n| n > limits.max_frame_bytes) {
|
||||
return Err(Error::Invalid("frame dimensions exceed the decoder memory limit"));
|
||||
}
|
||||
let interlaced = hdr.interlace != Interlace::Progressive;
|
||||
@@ -564,6 +592,44 @@ mod alpha_decode_tests {
|
||||
assert_eq!(slice_quantiser(224).unwrap(), qscale(224));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_limits_preserve_large_4444_geometry_before_allocating() {
|
||||
let hdr = FrameHeader {
|
||||
header_size: 20,
|
||||
bitstream_version: 1,
|
||||
encoder_id: *b"test",
|
||||
width: 8192,
|
||||
height: 8192,
|
||||
chroma: ChromaFormat::Yuv444,
|
||||
interlace: Interlace::Progressive,
|
||||
aspect_ratio: 0,
|
||||
frame_rate_code: 0,
|
||||
color: crate::prores::ColorInfo::default(),
|
||||
alpha: AlphaType::Bits16,
|
||||
qmat_luma: [4; 64],
|
||||
qmat_chroma: [4; 64],
|
||||
luma_matrix_present: false,
|
||||
chroma_matrix_present: false,
|
||||
};
|
||||
let header = hdr.write();
|
||||
let frame_size = 8 + header.len();
|
||||
let mut data = Vec::with_capacity(frame_size);
|
||||
data.extend_from_slice(&(frame_size as u32).to_be_bytes());
|
||||
data.extend_from_slice(&crate::prores::header::FRAME_ID);
|
||||
data.extend_from_slice(&header);
|
||||
|
||||
let defaults = DecodeLimits { max_pixels: 8192 * 8192, ..DecodeLimits::default() };
|
||||
assert!(matches!(
|
||||
decode_frame_with_limits(&data, &DecodeOptions::default(), &defaults),
|
||||
Err(Error::Invalid("frame dimensions exceed the decoder memory limit"))
|
||||
));
|
||||
|
||||
let soundcraft = DecodeLimits { max_pixels: 8192 * 8192, max_frame_bytes: 512 * 1024 * 1024 };
|
||||
assert!(matches!(decode_frame_with_limits(&data, &DecodeOptions::default(), &soundcraft), Err(Error::Truncated)));
|
||||
let too_few_pixels = DecodeLimits { max_pixels: 8192 * 8192 - 1, ..soundcraft };
|
||||
assert!(matches!(decode_frame_with_limits(&data, &DecodeOptions::default(), &too_few_pixels), Err(Error::Unsupported("frame size"))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alpha_decoder_requires_complete_bounded_runs() {
|
||||
let mut out = [0u16; 1];
|
||||
|
||||
@@ -18,7 +18,9 @@ mod encode;
|
||||
pub mod header;
|
||||
mod tables;
|
||||
|
||||
pub use decode::{DecodeOptions, decode_frame, decode_frame_into, decode_frame_with, probe};
|
||||
pub use decode::{
|
||||
DecodeLimits, DecodeOptions, decode_frame, decode_frame_into, decode_frame_into_with_limits, decode_frame_with, decode_frame_with_limits, probe,
|
||||
};
|
||||
pub use encode::{Encoder, EncoderConfig};
|
||||
pub use header::{FrameHeader, PictureHeader};
|
||||
|
||||
|
||||
Reference in new issue
Block a user