v0.0.3
Other
- replace never-match regex with semver_check = false
- migrate to centralized OxideAV/.github reusable workflows
- oxideav-core ^0.2 -> ^0.1 (0.2.0 was yanked)
- implement receive_arena_frame() for true zero-copy
- wire DoS framework (DecoderLimits + ArenaPool + arena Frame)
- adopt slim VideoFrame/AudioFrame shape
- investigate r5/r6 long-clip drift, prove no IDCT precision loss
- fix chained-P decoder bug at GOB end, add MQUANT-delta rate ctrl
- add FIL (loop filter) MTYPEs to P-picture mode decision
- integer-pel motion estimation + MC for P-pictures
- add P-picture (INTER, no MC) Baseline encoder
- pin release-plz to patch-only bumps
Added
receive_arena_frame()— zero-copy decode path.
Overrides the newoxideav_core::Decoder::receive_arena_frame()
method (added in oxideav-core 0.2.0) to return an arena-backed
oxideav_core::arena::sync::Framedirectly, skipping the per-plane
memcpy that the legacyreceive_frame() -> Frame::Video(VideoFrame)
path requires forSend. Internal queueing was reorganised so the
arena lease happens at drain time rather than decode time:
decodedPictures are queued raw and converted to either a
heap-backedVideoFrame(legacy path) or an arenaFrame(new
path) on demand. This keeps the pool short-lived — pool
exhaustion in the typical send-many-then-drain pattern is no
longer possible because the pool only holds slots for the
duration of frames the caller has explicitly drained via
receive_arena_frame.
Changed
-
Bumped
oxideav-coredep from0.1to0.2to pick up the
newDecoder::receive_arena_frametrait method (additive; default
impl preserves backwards compatibility for every other
oxideav-h261consumer). -
DoS-protection wiring (oxideav-core 0.1.8 framework).
H261Decodernow honours [oxideav_core::DecoderLimits] at two
layers, sub-task #85's second proof-of-concept after the h263 port.- Construction.
make_decoder(params)readsparams.limits()and
forwards to the newH261Decoder::with_limits(codec_id, limits)
constructor;H261Decoder::new(codec_id)is a thin wrapper that
usesDecoderLimits::default()(32 k × 32 k pixels, 1 GiB / arena,
8 arenas in flight).with_limitsbuilds an
Arc<oxideav_core::arena::ArenaPool>sized at
limits.max_arenas_in_flightslots ×min(limits.max_alloc_bytes_per_frame, 160 KiB)per arena (the 160 KiB cap is the H.261 worst-case CIF I420
frame plus alignment headroom — no real H.261 picture allocates more
than this regardless of the caller'smax_alloc_bytes_per_frame).
Per-arena alloc-count cap islimits.max_alloc_count_per_frame
(default 1M). - Header-parse cap.
decode_one_picturechecks
(width × height) <= limits.max_pixels_per_frameimmediately after
parse_picture_headerreturns the QCIF / CIF dimensions and surfaces
a tighter-than-format mismatch asError::InvalidData(NOT
ResourceExhausted— H.261's source format is fixed by a single
PTYPE bit, so the bitstream cannot declare an arbitrary size; a
failure here means "this codec's intrinsic frame size doesn't fit in
the caller's caps"). - Arena pool. Each picture decode leases one arena from the pool,
builds anoxideav_core::arena::Frame(refcounted handle to the
leased buffer + per-plane offset/length pairs + aFrameHeader),
materialises aVideoFramefrom it for the publicFrame::Video
enum, then drops the arena handle to return its buffer to the pool.
Pool exhaustion (every slot checked out) surfaces as
Error::ResourceExhaustedfrom the lease call, which propagates up
throughdecode_one_pictureand out ofsend_packet/flush. - Send-boundary trade-off. The
oxideav_core::Decodertrait
requiresSend, but the newoxideav_core::arena::Frameis
Rc<FrameInner>and therefore!Send. TheFrame::Video(VideoFrame)
enum returned byDecoder::receive_framestays heap-backed so
downstream consumers (oxideav-pipeline, oxideplay, etc.) keep the
same shape — the arenaFrameis a transient internal value that
backs each picture's allocation. When the workspace gains an
Arc<FrameInner>parallel-decoder variant the public API can be
migrated without disturbing this crate's pool wiring (the wiring
lives entirely insidedecode_one_picture). - Public test surface.
H261Decoderexposeslimits()and
arena_pool() -> &Arc<ArenaPool>for diagnostics and pool-aware
tests. Five new tests intests/dos_limits.rs:picture_header_too_large_returns_invalid_data— QCIF header
against a 99×99 pixel cap →InvalidDatamentioning
max_pixels_per_frame.picture_header_within_cap_decodes_normally— same QCIF header
against a 1024×1024 cap doesn't trip the dimension check.pool_exhaustion_returns_resource_exhausted— pool sized at 2,
third concurrent lease →ResourceExhausted.default_limits_admit_qcif_and_cif— sanity that the default
32 k × 32 k cap admits CIF.pool_buffer_returns_after_decode— pool sized at 1, lease/drop/
re-lease cycle works.
- Encoder is unchanged (no DoS surface — it consumes caller-owned
VideoFrames and produces compressed packets).
- Construction.
-
Encoder: P-picture (INTER) support with integer-pel motion compensation
(full-window ±15 SAD search per H.261 §3.2.2 / Annex A) and the
Inter+MC+FILMTYPEs (loop filter §3.2.3, separable 1/4-1/2-1/4 with
edge-pel passthrough). Each P-MB picks the cheapest of skip / Inter /
Inter+MC{-only,+CBP} / Inter+MC+FIL{-only,+CBP} via a bit-cost estimator
comparing MTYPE + MVD + CBP + a residual proxy. ffmpeg interop holds on
the FIL stream (ffmpeg_decodes_our_fil_p_pictures). On testsrc QCIF
the pipeline lifts ffmpeg-roundtrip PSNR from r12's 39.27 dB / 8680 B
to 39.40 dB / 8546 B (–1.5 % bytes, +0.13 dB). -
Encoder: per-GOB MQUANT-delta rate controller (§4.2.3.3). Tracks
cumulative bits within each GOB and nudges the quantiser ±1 step (within
a ±6 window around GQUANT) when an MB lands far over the linear bit
budget. Honoured only on MQUANT-bearing MTYPEs (Intra+MQUANT,
Inter+MQUANT, InterMc+CBP+MQUANT, InterMcFil+CBP+MQUANT); other modes
defer the change. Disabled byOXIDEAV_H261_NO_MQUANT=1for A/B
benchmarks. Trims 0.3 % bytes on the testsrc fixture at –0.03 dB
(8517 B / 39.37 dB vs r13's 8546 B / 39.40 dB).
Fixed
- Decoder: chained P-frame mishandling at GOB 5 MBA=33 (last MB of QCIF
GOB 5) on streams where the picture's last MB used MC-only mode. The
GOB MB-loop indecoder::decode_picture_bodyandmb::decode_mba_diff
used to break early onbits_remaining < 16, but the start-code prefix
is itself ≥16 zero bits — fewer than that cannot encode a start code,
so the remaining bits are valid MB data + padding. Now we only invoke
the start-code peek when ≥16 bits remain and otherwise let the VLC
decoder consume what's there. Self-decode of a 5-frame P-chain on
testsrc QCIF jumps from 25–28 dB PSNR (drift from misdecoded final
MBs) to a clean 36–37 dB matching ffmpeg byte-for-byte.