Files
libfreemkv/docs/h264.md
T
Matthew Jackson 6e14fc0c0c libfreemkv: 1.7.0 groundwork — sector-recycle soundness + CI hardening
Security
- Remove the unsafe Vec::set_len over recycled sector buffers — UB reachable
  from safe code (GHSA-j8ww-f5fg-9pmh, low). Uses Vec::resize; pooling unchanged.

CI / tooling
- clippy on stable with the MSRV single-sourced from Cargo.toml; cargo-deny
  dependency-audit gate with first-party git sources allowed (#15); audience-based
  comment-guard (#17).

Docs
- README (keydb ownership, BD-J parser count), drive-access transport/reset
  notes, mux timeline module-doc links.
2026-09-02 12:40:55 -07:00

190 lines
9.1 KiB
Markdown

# H.264 codec parser notes
## Slice-type mapping
`h264_slice_coding_type` maps an H.264 `slice_type` (Rec. ITU-T H.264 §7.4.3,
Table 7-6) to a coding type. Values 5..=9 repeat 0..=4 (the "all slices of
this type" forms), so `slice_type % 5`: 0 = P, 1 = B, 2 = I, 3 = SP
(predicted → P), 4 = SI (intra → I). Returns `None` for values outside 0..=9
(malformed header).
## `unescape_ebsp_prefix`
Copies the leading bytes of an EBSP with emulation-prevention bytes removed
(ISO/IEC 14496-10 §7.3.1: a 0x03 following 0x00 0x00 is not part of the
RBSP). Only a short prefix is produced, because the sole caller decodes two
ue(v) fields from the very start of a slice header. `first_mb_in_slice` and
`slice_type` are each at most 32 bits, so 16 octets is ample and keeps this
off the per-frame allocation path for anything larger.
## `unescape_ebsp`
Copies `ebsp` with emulation-prevention bytes removed, stopping after at most
`max_octets` output bytes (pass `ebsp.len()` for "no cap").
The zero run-length counter is cumulative across the whole scan and is reset
only when an escape byte is actually dropped — matching the reference
decoding process in ITU-T H.264 §7.3.1, which discards a 0x03 following ANY
run of two-or-more 0x00 bytes, not just an exact `00 00 03` sliding window. A
window-based scanner that advances by 3 on a match and by 1 otherwise
disagrees with this on a run of 3+ real zero bytes ahead of an 0x03:
`00 00 00 03` decodes here as `00 00 00` (the run is escaped, matching every
other H.264 decoder), where a window scan starting fresh after each
non-match would keep the 0x03 as real payload. Only one implementation of
this rule may exist in this module — see `parse_sps_high_profile_ext`, which
used to keep its own window-scan copy that disagreed with this one on
exactly that input.
## `handle_param_set`
Handles an SPS/PPS NAL (mirrors the HEVC fix). The strip/emit decision is
made against the currently-ACTIVE set `cur`, NOT the codecPrivate copy
`first`: a streaming MKV decoder applies avcC once at init and thereafter
updates a parameter set only from an in-band NAL, so a switch BACK to the
first-seen body (== codecPrivate) is still a change the decoder must be told
about. Stripping on `== first` silently dropped that revert.
- First of its type → seeds codecPrivate; stripped (decoder gets it from avcC).
- Equal to the active set `cur` → redundant; stripped.
- Different from `cur` (a change in EITHER direction) → emitted in-band and
`cur` updated.
Returns `true` when the NAL was emitted in-band into `frame_data`.
## `reassert_active`
Appends the active parameter set `cur` to `prefix` (length-prefixed) so
every keyframe is SELF-CONTAINED: it carries the active SPS/PPS in-band
ahead of its slices. Skipped only when this access unit ALREADY carried the
NAL in-band (`emitted` — avoids a duplicate) or no active set exists yet.
Unconditional (not only when the active set differs from codecPrivate): a
streaming decoder applies the avcC param sets once at init, then relies on
in-band repetition. Some sources stop repeating a param set at later IDRs
even though its body is unchanged; if the decoder then drops it (a reset
event), nothing re-sends it and every subsequent slice fails (param-set id
out of range) until the next genuine change. Re-asserting at EVERY keyframe
— what compliant muxers do at every IDR — makes streaming decode
self-healing. Re-sending an identical param set is benign; cost is a few
bytes per keyframe. This strictly supersets the change-only re-assert, so
the param-set-revert fix is unaffected.
## `parse_sps_high_profile_ext`
Parses `(chroma_format_idc, bit_depth_luma_minus8, bit_depth_chroma_minus8)`
from a High-Profile SPS NAL (profile_idc ∈ `HIGH_PROFILES` — the 14
chroma/bit-depth extended profiles `codec_private` invokes this for).
SPS RBSP layout (ITU-T H.264 §7.3.2.1.1) up to the fields we need:
```
byte 0 NAL header (already known to be type 7)
byte 1 profile_idc
byte 2 constraint_set_flags / reserved
byte 3 level_idc
ue(v) seq_parameter_set_id
— High-profile branch —
ue(v) chroma_format_idc
if chroma_format_idc == 3: u(1) separate_colour_plane_flag
ue(v) bit_depth_luma_minus8
ue(v) bit_depth_chroma_minus8
```
RBSP emulation-prevention bytes (0x00 0x00 0x03 → 0x00 0x00) are removed
before bit-parsing so the bit reader sees clean RBSP data.
Returns `None` if the SPS is too short or malformed (Exp-Golomb code
overflows 32 bits, leading-zero count > 31, etc.). The caller silently omits
the extension in that case.
## Test notes
### `open_gop_intra_access_unit_is_a_keyframe`
Open-GOP resync fix: a non-IDR intra-coded access unit MUST be flagged a
keyframe so the B1 resync gate (`mux/resync.rs`) can disarm on a BD
open-GOP tail instead of dropping every frame to EOF and truncating the
video. BD H.264 titles reach random access at a non-IDR I-frame (the
recovery point); HEVC gets this free from the CRA NAL type, H.264 does not,
so the measured intra coding type is the signal. Red before the intra
promotion in `parse` — a non-IDR I-slice used to yield `keyframe == false`.
### `inter_coded_access_unit_is_not_a_keyframe`
Guards the promotion's precision: a non-intra (P) access unit must NOT be
promoted to a keyframe, or the resync gate would resume on a frame that
carries inter-references to dropped data and the MKV cue index would gain
bogus random-access points.
### `slice_header_parse_removes_emulation_prevention_bytes`
The slice-header parse must remove emulation-prevention bytes first
(ISO/IEC 14496-10 §7.3.1/§7.4.1): the payload after the NAL header is EBSP,
and a 0x03 following 0x00 0x00 is not part of the RBSP.
Reachable when `first_mb_in_slice` is 65535 or more — a ue(v) that needs
sixteen leading zero bits, so the payload opens 0x00 0x00 and an encoder
must escape it. A UHD frame is ~32,400 macroblocks so conforming Blu-ray
never gets there, but 8K does, and the disc is untrusted input. Reading the
raw NAL decoded `slice_type` against the inserted byte and produced the
wrong picture type.
### `unescape_ebsp_drops_escape_after_a_run_of_three_zeros`
Regression: `parse_sps_high_profile_ext` used to re-derive the
emulation-prevention rule with its own window scanner (match `00 00 03` at
position i, advance by 3; else advance by 1) instead of calling the shared
`unescape_ebsp`. On a run of 3+ real 0x00 bytes ahead of an 0x03 —
non-conformant, but this is untrusted disc input, not a spec-clean encoder —
the two disagreed: `unescape_ebsp`'s cumulative zero counter (the rule the
H.264 reference decoding process in §7.3.1 specifies) drops the 0x03 as an
escape, while a fresh 3-byte window starting right after the non-matching
first byte kept it as real payload. Pin the shared function's behaviour
here so a second hand-rolled copy doesn't quietly reappear.
### `keyframe_param_reassert_emits_exact_bytes`
The keyframe SPS/PPS re-assert must produce EXACTLY these bytes: the active
SPS, then the active PPS, then the access unit's own NALs, each as a 4-byte
big-endian length prefix followed by the NAL body (ISO/IEC 14496-15
length-prefixed form, lengthSizeMinusOne = 3).
This pins the full byte string, not just its length, so the in-place
`splice` that replaced the build-a-new-buffer-and-copy approach is proven
byte-for-byte equivalent — including the ORDER (parameter sets ahead of the
slices, which is what makes the keyframe self-contained). The literals were
captured from the pre-splice implementation's output.
### `keyframe_param_reassert_does_not_reallocate_the_frame`
MEASURED: the keyframe parameter-set re-assert must be spliced into the
front of the already-assembled access unit IN PLACE, not built as a fresh
full-size buffer.
It used to `prefix.extend_from_slice(&frame_data)` and then replace
`frame_data` with `prefix`, which grew a few-hundred-byte `prefix` to the
FULL access-unit size — a fresh multi-hundred-KB allocation — memcpy'd the
whole frame into it, and dropped the presized buffer. One extra whole-frame
allocation plus one extra whole-frame copy per keyframe: a UHD title is
~200,000 frames of 150-400 KB with a keyframe every 1-2 s, i.e. thousands of
avoidable multi-hundred-KB copies per title, each large enough to go through
mmap. `PARAM_REASSERT_HEADROOM` exists so the splice never reallocates; this
counts the reallocations that happen, which must be zero. Mirrors the HEVC
parser's test of the same name.
### `avcc_profile_244_appends_extension_bytes`
ISO 14496-15 §5.3.3.1.2 regression: profile_idc=244 (High 4:4:4 Predictive)
ALSO mandates the chroma/bit-depth extension. It was missing from
HIGH_PROFILES, so a 244 stream took the Baseline/Main path and emitted an
avcC with NO extension bytes — non-conforming, and strict parsers then
assume 8-bit 4:2:0. The extension must be appended.
### `sps_reader_read_ue_thirty_one_leading_zeros_is_still_valid`
`SpsReader::read_ue`'s truncation guard is `leading_zeros > 31`: 31 leading
zero bits is the longest legal code and must decode, not abort. Mirrors the
equivalent guard in the shared `BitReader` in `startcode.rs`, kept here
because `SpsReader` is a separate, unshared implementation used only for SPS
parsing.