Check the no-B-frame constraint in doctor and the encoder - #373
Conversation
hflow doctor now reports video-b-picture when a canonical video message's slice headers classify a picture as B. The count comes from scan_picture_coding_types(...).picture_count so the hot path walks the NALs once; on a scan refusal it delegates to count_h264_pictures, whose caught ValueError keeps both pinned finding texts byte-identical and reproduces the pre-classification behavior when first_mb_in_slice parses and slice_type does not. _enforce_encode_guarantees now refuses any B picture from the encoder, naming the offending access unit via a per-unit rescan on the error path. docs/FORMAT.md gains the video-b-picture row and closes both open gap notes. Refs Hebbian-Robotics#368
Prose only. The inserted sentences left lines between 30 and 96 columns in a paragraph otherwise wrapped near 78.
kstonekuan
left a comment
There was a problem hiding this comment.
LGTM, merging. The constraint I was most worried about is the one you handled best.
I diffed every message and every doctor finding for the same payloads, main against this branch. Exactly one line changes:
all-zero payload -> ['video-invalid-slice-header']
terminating bit, truncated suffix -> ['video-invalid-slice-header']
valid first_mb, truncated slice_type -> ['video-not-aud-delimited']
well-formed I slice (type 2) -> ['video-not-aud-delimited']
-well-formed B slice (type 1) -> ['video-not-aud-delimited']
+well-formed B slice (type 1) -> ['video-b-picture', 'video-not-aud-delimited']
empty payload -> ['video-invalid-slice-header']
Both count_h264_pictures messages are byte-identical, and so is ensure_access_unit_delimiter's behaviour through them. The third row is the subtle one: a header whose slice_type is truncated but whose first_mb_in_slice parses still counts and still reports no slice-header error, which is the per-field divergence #358 preserved. Delegating to count_h264_pictures on the error path rather than translating the scan's failure is what keeps that true, and it costs nothing on the hot path because it only runs when the scan already refused.
You also pinned that delegation, which I was about to check and did not have to. Removing it:
error-path delegation removed -> 4 failed
test_doctor_keeps_both_pinned_count_messages_when_the_scan_refuses
test_doctor_reports_invalid_slice_header_over_b_picture_when_a_header_is_malformed
test_nonconforming_ros2_video_is_reported
test_nonconforming_video_is_reported
The first two are yours, written for exactly that property. The other two are pre-existing tests that would have caught it, which is the better kind of evidence.
Both new checks bite:
doctor B finding removed -> test_doctor_reports_a_b_picture_from_slice_headers
encoder B guarantee removed -> test_encode_guarantees_raise_on_a_b_picture_naming_the_unit,
test_encode_guarantees_find_a_b_picture_in_a_later_unit
Scanning the joined stream first and only rescanning per unit on the error path is the right shape: the encoder pays one scan on success and the unit index costs nothing unless something is actually wrong.
On the timing, and this part is my fault rather than yours. Measured independently over real per-message access units from a conforming bframes=0 stream:
main branch
300 messages 3.84 ms 4.25 ms (+10.7%)
3000 messages 38.16 ms 41.97 ms (+10.0%)
findings on conforming video: [] on both sides
Your numbers and mine agree on the magnitude. DoD 6 said "does not regress measurably", which was me writing the wrong sentence: classifying a second Exp-Golomb field per NAL cannot be free, and what I actually meant was the clause right after it about not walking the NALs twice. You did that, and reported the delta instead of burying it, which is the correct response to a badly worded requirement. In absolute terms a 900-message episode pays about a millisecond against a doctor run that reads the whole MCAP.
Pushed one fixup, dc638f7: prose reflow only. The inserted sentences left that paragraph with lines between 30 and 96 columns where the rest wraps near 78. Also worth noting you closed only the gap this PR closes and left the non-VCL-NAL-before-first-AUD note standing, which a careless edit would have taken with it.
Gate clean: ruff check, ruff format --check, ty check, lychee on FORMAT.md, 1428 passed / 6 skipped.
Closes #368.
Closes #368.