v0.1.7
Other
- 4-component CMYK / YCCK progressive (SOF2) decode
- 12-bit precision progressive (SOF2 P=12) decode
- encoder docstring: refresh stale "P=8 only" note for lossless RGB
- lossless (SOF3) three-component decode at every P in 2..=16
- rustfmt — split InBand q if-else across lines
- fuzz packetizer + close five wire-length panic surfaces
- add rtp_depacketize cargo-fuzz target for RFC 2435 surface
- gate fixture corpus on Tier::Exact + Tier::PsnrFloor
- fix i32 overflow in dequantise when Pq=1 (16-bit quantiser)
- 12-bit 4:2:2 (Yuv422P12Le) + 4:4:4 (Yuv444P12Le) YUV
- add decode robustness target + fix seven panic surfaces
- cache static-Q in-band quantization tables across frames (RFC 2435 §4.2)
- add RFC 2435 RTP/JPEG packetizer (encode side)
- add RFC 2435 RTP/JPEG depacketizer
- lossless encoder: restart markers + non-zero point transform
- rewrite library-citation comments to remove external-library references
- lossless (SOF3) three-component encoder + decoder
- lossless (SOF3) grayscale encoder: P=2..=16 + all 7 predictors
- add multi-frame demuxer with seek_to + marker-aware scanner
- compare libjpeg cross-decode in YUV space, not RGB
Added
-
4-component (CMYK / Adobe YCCK) progressive (SOF2 with
P = 8)
decode. T.81 §G.1.1 permits the progressive coding process at every
component-count the spec admits (Nf ∈ 1..=4), but the decoder was
previously rejectingSOF2withNf = 4even though every
downstream stage already supported the geometry:
decode_progressive_scanis component-count agnostic (interleaved
DC walks every SOS component, AC scans are always non-interleaved
per the spec),init_coef_buffersalready sizes for up to 4
components, andrender_from_coefsalready produces a packed
PixelFormat::Cmykplane forNf = 4honouring the Adobe APP14
colour-transform flag (plain CMYK, Adobe-inverted CMYK at
transform=0, YCCK at transform=2). The SOF2 4-component path
therefore lights up by removing theNf > 3rejection; the
Nf = 4 & P = 12combination is still rejected with
Error::Unsupportedbecause the workspacePixelFormatenum
carries no 12-bit CMYK variant.A new test-only encoder helper
encode_jpeg_progressive_cmyk_1111
emits a 4-component SOF2 progressive JPEG atP = 8with the same
spectral-selection-only scan decomposition the existing
three-component progressive YUV helpers use: one interleaved DC
scan (Ss = Se = 0, Ah = Al = 0) followed by per-component AC
bands[1..=5]then[6..=63]for each of the four components
(1 + 4 + 4 = 9 SOS segments total). Each component is declared
H_i = V_i = 1so the MCU equals one data unit per component;
component 1 binds quant table 0 (luma_q), components 2/3/4 share
quant table 1 (chroma_q), mirroring the baseline
encode_jpeg_cmyk_1111policy. The helper accepts the same
adobe_transform: Option<u8>parameter as the baseline path
(None→ no APP14,Some(0)→ Adobe CMYK inverted-on-wire,
Some(2)→ Adobe YCCK with K-inversion).Three new tests under
decoder::cmyk_tests
(cmyk_progressive_plain_roundtrip,
cmyk_progressive_adobe_inverted_roundtrip,
ycck_progressive_k_plane_matches) drive each transform variant
through the helper and decode it back, asserting per-component
PSNR ≥ 30 dB at Q = 90 (same tolerance as the baseline tests they
mirror). A fourth test (cmyk_progressive_p12_rejected) hand-
crafts a minimal SOF2 segment withP = 12, Nf = 4and confirms
the parser still rejects the unsupported combo with
Error::Unsupportedbefore any scan is read. -
12-bit precision progressive (SOF2 with
P = 12) decode. T.81 §G.1.1
permits the progressive coding process at precision 8 or 12, but the
decoder previously rejectedSOF2atP = 12withError::Unsupported
even thoughinit_coef_buffersalready allocated 12-bit-shaped
coefficient accumulator planes andrender_from_coefsalready routed
P = 12to the dedicatedrender_from_coefs_12bitpath (level shift
2048, clamp[0, 4095], 16-bit-LE output planes). The progressive
scan path itself (decode_progressive_scan+prog_decode_dc/
prog_decode_ac_first/prog_decode_ac_refine) operates entirely on
i32coefficient planes, so the increased DC/AC residual magnitude
range atP = 12fits without numeric changes. TheBitReader::get_bits
24-bit ceiling accommodates the wider DC categories (up to 15) and AC
magnitudes (up to 14) the spec admits atP = 12.Output shape matches the sequential
P = 12path:- Grayscale →
Gray12Le(one 16-bit-LE plane, low 12 bits carry the
sample). - Three-component YUV at 4:4:4 →
Yuv444P12Le. - Three-component YUV at 4:2:2 →
Yuv422P12Le. - Three-component YUV at 4:2:0 →
Yuv420P12Le.
Non-2x luma sampling factors atP = 12continue to be rejected with
Error::Unsupported(noPixelFormatenum entry for, e.g., 4:1:1 at
12-bit).
A new test-only encoder helper
encode_yuv_jpeg_progressive_12bit
emits a three-component SOF2 progressive JPEG atP = 12with the
spectral-selection-only scan decomposition (interleaved DC pass +
Y/Cb/Cr AC bands[1..=5]then[6..=63],Ah = Al = 0), reusing
the same Annex K Huffman tables andDEFAULT_LUMA_Q50/
DEFAULT_CHROMA_Q50quant tables as the existing 12-bit baseline
helperencode_yuv_jpeg_12bit. Three new tests under
decoder::precision_12_tests(yuv444_12bit_progressive_roundtrip,
yuv422_12bit_progressive_roundtrip,yuv420_12bit_progressive_roundtrip)
drive a smooth gradient through the helper and decode it back,
asserting per-sample closeness against the originals (diff < 24,
same tolerance as the existing baseline 12-bit YUV roundtrip tests). - Grayscale →
-
High-bit-depth lossless (SOF3) three-component decode. Previously the
decoder accepted SOF3 RGB-class scans only atP = 8(output:
packedRgb24); decoding at higher precisions raised
Error::Unsupported. The decoder now covers every precision in
2..=16for three-component lossless, with output shape selected by
precision:P = 8→ packedRgb24(one plane, 3 bytes/pixel) —
unchanged.P = 10→ planarGbrp10Le(3 planes, 16-bit LE per sample,
low 10 bits carry the sample).P = 12→ planarGbrp12Le.P = 14→ planarGbrp14Le.- any other
Pin2..=16(i.e. 2..=7, 9, 11, 13, 15, 16)
→ packedRgb48Le(one plane, 6 bytes/pixel; samples
narrower than 16 bits sit in the low bits of each 16-bit word).
Per-component buffer ordering is preserved end-to-end: planes pass
through encoder → decoder in the same SOS scan order (mirroring the
existing colour-agnosticRgb24behaviour), so a caller that wants
canonicalGbrp*LeG-B-R layout passes its G, B, R planes to the
encoder in that order. Roundtrip bit-exactness verified by five new
integration tests intests/lossless_roundtrip.rs:
lossless_rgb_10bit_every_predictor_planar_gbrp10(every Annex H
predictor 1..=7),lossless_rgb_12bit_predictor_4_planar_gbrp12,
lossless_rgb_14bit_predictor_7_planar_gbrp14,
lossless_rgb_16bit_predictor_1_packed_rgb48, and
lossless_rgb_odd_precision_9_widens_to_rgb48. The previous
lossless_rgb_rejects_higher_precision_decodetest (which asserted
theError::Unsupportedfor P > 8) is removed in the same commit
per the workspace "rewrite, don't#[ignore]" guardrail.
-
MjpegPixelFormat(standalone API) gainsRgb48Le,Gbrp10Le,
Gbrp12Le, andGbrp14Levariants, with bidirectional
From<MjpegPixelFormat> for oxideav_core::PixelFormat/ reverse
mapping inregistry.rsso the registry-sideCodecParameters
surface accepts and produces them. -
fuzz/fuzz_targets/rtp_packetize.rs: cargo-fuzz harness covering
the RFC 2435 RTP/JPEG packetizer (oxideav_mjpeg::rtp::packetize).
Drives arbitrary bytes (≤ 16 KiB) through the encode-side JPEG
segment walker (fn parse_jpeg), which indexes into the input by
the big-endian length field of each SOI / SOF / DQT / DRI / SOS /
catch-all segment. The harness samples bothQMode::Quality(1..=99)
andQMode::InBand(128..=255)(and the rejection paths outside
those ranges) and a range ofmax_payloadMTUs so the
header-room rejection and the fragment-split loop both run on
every iteration. Contract: no panic, slice OOB, debug-build
integer overflow, or infinite loop from a zero-length segment.
Successful returns are shape-checked: first fragment offset 0,
marker bit set on the final packet, no payload exceeds the
caller'smax_payload. This is now the seventh fuzz harness in
fuzz/alongsidedecode,jpeg_self_roundtrip,
jpeg_progressive_self_roundtrip,libjpeg_encode_oxideav_decode,
oxideav_encode_libjpeg_decode, andrtp_depacketize. Last
local 15 s baseline (debug binary, no sanitizer instrumentation):
21 819 067 runs, 0 crashes; the daily reusable fuzz workflow runs
the release-instrumented build with proper coverage and pulls the
new harness in automatically viafuzz/Cargo.tomlauto-discovery. -
fuzz/fuzz_targets/rtp_depacketize.rs: cargo-fuzz harness covering
the RFC 2435 RTP/JPEG depacketizer (oxideav_mjpeg::rtp). Feeds
arbitrary bytes throughparse_main_header,parse_restart_header,
andJpegDepacketizer::push— the latter as a sequence of
synthetic "packets" so the §3.1.2 fragment-offset reassembly
buffer, the §3.1.7 Restart Marker header parser, the §3.1.8
Quantization Table header parser, the §4.2 static-Q table cache,
the marker-bit close path, and thereset()cache-retention
invariant are all exercised on every iteration. Contract: no
panic, slice OOB, debug-build integer overflow, or buffer
allocation the input couldn't plausibly back. Assembled frames
(when the marker bit closes a frame) are asserted to begin with
SOI and end with EOI; their interior bytes are not validated
(round-trip correctness is owned by the unit tests in
src/rtp.rs). This is now the sixth fuzz harness infuzz/
alongsidedecode,jpeg_self_roundtrip,
jpeg_progressive_self_roundtrip,
libjpeg_encode_oxideav_decode, and
oxideav_encode_libjpeg_decode. -
tests/docs_corpus.rs: the fixture-corpus harness now gates on
numerical floors instead of merely reporting. Two newTiervariants:Tier::Exactasserts every sample matches the reference. Five
fixtures land here today —tiny-baseline-1x1,
baseline-grayscale-32x32,lossless-1986-mode,
arithmetic-coded(SOF9 Q-coder Decode path), and
baseline-yuv411-32x32— all already at 100 % exact.Tier::PsnrFloor { db, exact_pct }asserts bothtotal.psnr >= db
andtotal.match_pct() >= exact_pct. Eleven lossy fixtures land here
with floors set ~0.5–2 dB below the observed PSNR and ~1–2 pp
below the observed exact-sample percentage; tight enough to catch
a real regression, loose enough to absorb normal floating-point
jitter.
Both new tiers fail CI on a regression rather than silently
degrading the corpus baseline.ReportOnlyandIgnoredremain for
forward-flexibility but are unused at present.
-
12-bit precision decoder: 4:2:2 (
Yuv422P12Le) and 4:4:4 (Yuv444P12Le)
chroma sampling, alongside the previously-supported 4:2:0
(Yuv420P12Le). All three formats run through the shared
render_from_coefs_12bitpath with the spec'sP=12level shift of
2^(P-1) = 2048and a[0, 4095]output clamp (T.81 §A.3.1). The
decoder accepts any sequential 12-bit JPEG (SOF0/SOF1) declaring
three components withCb/CratH=V=1and luma at(1,1),
(2,1), or(2,2); non-2x luma sampling at 12-bit (e.g. 4:1:1)
still returnsError::Unsupported.MjpegPixelFormat::Yuv422P12Le
andMjpegPixelFormat::Yuv444P12Lejoin the standalone enum with
Fromconversions againstoxideav_core::PixelFormatin both
directions. New roundtrip tests indecoder::precision_12_tests
cover all three sampling factors plus the 4:1:1-rejection contract. -
encoder::encode_yuv_jpeg_12bit(test-only crate helper): emits a
standalone three-component SOF1 JPEG atP=12for any sampling
factor in{(1,1), (2,1), (2,2), (4,1)}. Reuses the Annex K luma /
chroma Huffman tables (callers must keep per-block DC/AC categories
≤ 11). Drives the decoder-side roundtrip tests; not exposed in the
public API. -
fuzz/fuzz_targets/decode.rs: cargo-fuzz robustness target that drives
arbitrary bytes (capped at 64 KiB) through the publicDecodertrait
(make_decoder→send_packet→receive_frame). The contract is
"no panic": any malformed input must yieldErr(_)rather than an
unwrap, slice-OOB, integer overflow, or unboundedVec::with_capacity
/vec![0; n]allocation. 60 000 runs reach coverage 1 830 / 6 667
without a crash. The harness is registered as a fifth bin in
fuzz/Cargo.tomlalongside the existing round-trip / cross-decode
targets, so the daily reusable fuzz workflow's auto-discovery picks
it up without further wiring.
Fixed
-
RTP/JPEG packetize parser panic surfaces in
fn parse_jpeg
(oxideav_mjpeg::rtp):- SOF0/SOF1
len < 2previously underflowedlen - 2in the
payload bounds calculation (body + len - 2 > jpeg.len()); the
check now refuseslen < 8(the SOF fixed-header minimum, T.81
§B.2.2) up-front before any subtraction. - SOF0/SOF1 with
Nf = 3but a declared length too short to
carry the three 3-byte component records (8 + 3 * Nf = 17)
previously indexedjpeg[body + 13]past the segment end. A
newlen < 8 + 3 * nccheck rejects the truncated header before
the component-records read. - DQT
len < 2previously underflowedlen - 2when computing
the table-body slice end; explicitlen < 2guard added. - SOS
len < 2previously yielded ascan_start = pos + len
underflow (and the subsequent&jpeg[scan_start..scan_end]slice
panic insidepacketize). Bounds-checked. - Catch-all length-prefixed segment with
len == 0(any
unsupported marker — APPn, COM, DHT, …) previously caused
pos += 0, an infinite loop. The arm now requireslen >= 2
(the segment must at least carry its own length field) and
refuses anypos + len > jpeg.len(). Five regression tests
cover the SOF length/component, DQT, SOS, and APP0 cases.
- SOF0/SOF1
-
Decoder dequantise i32 overflow (
render_from_coefs×3 sites): when a
DQT carriesPq = 1(16-bit precision, T.81 §B.2.4.1) the quant value can
be up to 65535, and a coefficient at the high end of the DCT range
(progressiveAl-shifted, or accumulated DPCM DC) can multiply past
i32::MAX. The 8-bit, 12-bit, and progressive render paths now perform the
dequantise multiplication inf32directly — the IDCT input isf32
either way, andf32carries the product well past 24 bits of mantissa
without overflow. Fuzz-found regression (crash-ee0cdd45); replaying the
artifact through the patcheddecodetarget now completes in 0 ms. -
Decoder panic surfaces uncovered during fuzz harness bring-up:
- SOS
Tdj/Tajselectors outside0..=3no longer panic
indexing the 4-widedc_huff/ac_huff/arith_dc/arith_ac
arrays; a newvalidate_sosrejects them asError::Invalid
before scan dispatch. - SOF
Tqselectors outside0..=3no longer panic indexing
state.quant; a newvalidate_sofrejects them up-front. - SOS
Ns = 0/Ns > 4rejected byvalidate_sos(an empty
component list otherwise produced an emptyprev_dcwhose first
index panicked). - SOF
Nf = 0/Nf > 4andHi/Vioutside1..=4
rejected byvalidate_sof(zero sampling factors previously hit
unwrap_or(1)fallbacks before downstream MCU arithmetic divided
by them). - Repeated SOF segments in a single JPEG now return
Error::Invalid("JPEG: multiple SOF segments")rather than
overwritingstate.sofwhile a stalecoef_bufallocated against
the prior SOF stayed live — the geometry mismatch would later
OOB the per-block accumulator. - SOF pixel-budget DoS:
Wt × Ht × Nf > 64 Mpxrejected as
Error::Unsupported("SOF: pixel budget exceeded"). A 16-byte SOF
segment could previously request~17 GiBof per-component output
buffers. BitReader::get_bits(n)underflow: a Huffman-decoded SSSS of
0was a>> 32UB onu32(debug-panic, release-wrap);
n > 24underflowed the24 - self.nbitsshift in the refill
loop. Both bounds are now checked:n == 0short-circuits to
Ok(0),n > 24returnsError::Invalid.- Lossless SSSS > 16: Annex H Table H.2 limits SSSS to
0..=16,
but a crafted DHT can deliver any byte; the lossless scan decoder
now rejects out-of-range SSSS rather than callingget_bits(s)
with a value theextend/ shift machinery has no defined
behaviour for.
- SOS
-
rtp::JpegDepacketizer: cross-frame in-band quantization-table caching
(RFC 2435 §4.2). For a static Q value (128..=254) the sender may carry the
Quantization Table header once and then omit the tables (Length = 0) on
subsequent frames; the depacketizer now caches the tables per Q value and
reuses them when a later frame's header is empty, so a multi-frame static-Q
stream decodes past the first frame. ALength = 0frame with no cached
tables for that Q (e.g. a receiver that joined mid-stream) still returns
Unsupported, matching the §4.2 startup caveat. Q = 255 is dynamic and never
populates the cache (the spec forbids depending on a previous frame's tables
for Q = 255).reset()clears the in-progress reassembly buffer but retains
the table cache;new()starts fully fresh. Five tests cover cache reuse,
the no-prior-cache error, per-Q keying, the Q = 255 non-caching rule, and
cache survival acrossreset(). -
rtpmodule: RFC 2435 RTP/JPEG packetization (the encode-side inverse
of the depacketizer).rtp::packetize(jpeg, max_payload, qmode)parses a
complete baseline (SOF0/SOF1) three-component YUV JPEG, strips the frame and
scan headers, and fragments the entropy-coded scan intortp::JpegPacket
RTP/JPEG payloads. Luma sampling2x1maps to the well-known type 0 (4:2:2)
and2x2to type 1 (4:2:0); a DRI segment promotes the type to 64/65 and
emits the §3.1.7 Restart Marker header (whole-frame reassembly, F=L=1,
count=0x3FFF).rtp::QModeselects table carriage:Quality(1..=99)puts an
IJG-quality Q value in the Q field (receiver regenerates Annex K tables), or
InBand(128..=255)carries the JPEG's own two DQT tables in a §3.1.8
Quantization Table header on the first fragment (offset 0). Fragments are
byte-contiguous; the first has fragment offset 0, the last has
JpegPacket::marker == true(caller sets the RTP marker bit). The caller
still owns RTP transport (12-byte fixed header, sequence numbers, 90 kHz
timestamp). Progressive/lossless/grayscale/CMYK, non-2:x luma sampling, and
16-bit DQT returnUnsupported— RTP/JPEG has no well-known type for them.
Tests cover header layout, type 0/1 selection, restart-bit promotion, scan
fragmentation, the unsupported-input rejections, a structural
packetize→depacketize scan round trip, and end-to-end
encode→packetize→depacketize→decode for both the in-band and Q-field paths. -
rtpmodule: RFC 2435 RTP/JPEG depacketization.rtp::JpegDepacketizer
reassembles fragmented RTP/JPEG payloads (keyed on the §3.1.2 Fragment
Offset, so misordered intra-frame delivery is tolerated) and
reconstructs the absent SOI / DQT / SOF0 / DHT / [DRI] / SOS / EOI
marker segments into a complete JPEG interchange stream the existing
decoder consumes directly. Covers the well-known fixed type mappings
0/64 (4:2:2,H=2 V=1luma) and 1/65 (4:2:0,H=2 V=2luma) with the
three-component YUV interleaved scan (§4.1). Quantization tables are
recovered from the Q field via the Independent JPEG Group scale formula
over Annex K.1 / K.2 forQ ∈ 1..=99(§4.2), or read in-band from the
§3.1.8 Quantization Table header forQ ∈ 128..=255(8- and 16-bit
precision, the latter saturated to the emitted 8-bit DQT). Types
64..=127 consume the §3.1.7 Restart Marker header and emit a DRI
segment with the carried interval. Annex K typical Huffman tables are
written for the abbreviated-format scan. The standalone helpers
rtp::parse_main_header/rtp::parse_restart_headerexpose the wire
header layout (MainHeader/RestartHeader). End-to-end tests encode
a frame, strip it to an RTP/JPEG payload (both Q-field and in-band-table
paths), depacketize, and decode the result back to a valid frame.
Out-of-band table negotiation (Q ≥ 128 with no in-band tables) and the
non-well-known dynamic types 128..=255 returnUnsupported. RTP
transport framing (the 12-byte RTP fixed header, sequence ordering)
stays the caller's responsibility. -
encoder::encode_lossless_jpeg_grayscale_with_opts(width, height, samples, stride, precision, predictor, restart_interval, point_transform)andencoder::encode_lossless_jpeg_rgb_with_opts(...):
public lossless (SOF3) encoder variants that emit DRI +RST0..=RST7
everyrestart_intervalMCUs (cycling modulo 8 per T.81 §F.1.1.5.2)
and honour a non-zeropoint_transform(Pt, the SOSAlnibble).
On every restart boundary the encoder byte-aligns the stream and
re-seeds every component's predictor history to the per-component
origin2^(P − Pt − 1)per T.81 §H.1.2.1; withPt > 0every input
sample is right-shifted byPtbefore prediction, and the decoder
side reconstructs(sample >> Pt) << Pt. Bit-exact roundtrips at
every supported predictor / restart interval / Pt combination across
the grayscale 2..=16-bit precisions and the 8-bit three-component
path. The default-options entry points
encode_lossless_jpeg_grayscale/encode_lossless_jpeg_rgbare
unchanged (now thin wrappers passingrestart_interval = 0,
point_transform = 0). -
encoder::encode_lossless_jpeg_rgb(width, height, [r, g, b], strides, precision, predictor): public three-component (RGB-class) lossless
JPEG (SOF3) encoder. Emits a standalone SOF3 stream with one
interleaved SOS scan, each component declaredH_i = V_i = 1per
T.81 §H.1.2 (lossless data unit is one sample, so the natural MCU is
one residual per component at each pixel position). Each component
is modeled with its own independent predictor buffer (per H.1.2:
"each component in the scan is modeled independently, using
predictions derived from neighbouring samples of that component").
Supports every precisionP ∈ 2..=16and every Annex H Table H.1
predictor1..=7. Bit-exact roundtrip atP = 8against the
packedRgb24decoder output. -
SOF3 lossless decoder: accept three-component interleaved scans at
P = 8and emit packedPixelFormat::Rgb24. Per-component sample
buffers track independent predictor history; restart markers reset
every component's predictor together. Four-component CMYK-class
lossless and non-unit sampling factors stay rejected with
Unsupported. -
MjpegPixelFormat::Rgb24(8-bit packed R-G-B) added to the
standalone-build pixel-format enum, withFromconversions in both
directions againstoxideav_core::PixelFormat::Rgb24. -
encoder::encode_lossless_jpeg_grayscale(width, height, samples, stride, precision, predictor): public single-component grayscale
lossless JPEG (SOF3) encoder. Supports every precisionP ∈ 2..=16
and every Annex H Table H.1 predictor1..=7. Output is bit-exact —
the existing SOF3 grayscale decoder recovers every sample verbatim,
including the special SSSS=16 / Di=32768 half-modulus case (T.81
§H.1.2.2). Uses a single canonical wide-symbol DC Huffman table
(STD_DC_LOSSLESS_*) that is Kraft-complete over symbols 0..=16, so
the same table is valid at every supported precision without
per-image tuning. -
MjpegEncoder::set_lossless(bool)/
MjpegEncoder::set_lossless_predictor(u8)on the registry-side
encoder. Withset_lossless(true)the trait-API encoder accepts
Gray8/Gray10Le/Gray12Le/Gray16LeVideoFrameinput
and dispatches to the lossless path; without the flag, grayscale
input is rejected with a clear error so the historical YUV-only
contract stays explicit. -
Raw Motion-JPEG container demuxer (
mjpeg-raw, owns the.mjpeg/
.mjpgextensions). One packet per JPEG frame in the stream, marker-
aware boundary scanner (T.81 §B.1.1.2 / §B.1.1.4) that honours
length-prefixed segment bodies — APP1 thumbnails, COM segments, etc.
cannot false-trigger an SOI / EOI match. Default time base is1/25
(frameicarriespts = i); callers that know the real rate can
post-process the emittedStreamInfo::time_base. -
Demuxer::seek_to(stream_index, pts)on the raw MJPEG demuxer.
Lazy(pts, byte_offset)index pushed every 5 frames (anchor at
frame 0 seeded at open time); binary-search-then-linear-scan to the
exact target frame. Past-end targets clamp to the last frame and
surfaceError::Eoffrom the followingnext_packet. Integration
tests intests/seek.rscover zero-reset, mid-stream seek, past-end
clamp, byte-stuffedFF D8false-positives, and byte-for-byte
parity with a baseline drain.