Merge pull request #1154 from storytold/codex/778-h264-picnum

video: harden H.264 and configure frame limits
This commit is contained in:
Dylan M. Taylor authored and GitHub committed 2026-11-13 03:49:15 -05:00
commit ea24422ae9
8 files changed
+256 -31

No files matched your search

+2 -1
View File
@@ -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
+12 -1
View File
@@ -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();
+84 -5
View File
@@ -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() {
+31 -2
View File
@@ -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();
+49 -19
View File
@@ -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);
}
}
+7
View File
@@ -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.
+68 -2
View File
@@ -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];
+3 -1
View File
@@ -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};