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

9.1 KiB

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.