Skip to content

5.25.0 - Verified interlace routing

Choose a tag to compare

@superuser404notfound superuser404notfound released this 28 Jul 08:59

Minor release. A routing rule that decided on a declaration now checks that declaration against what the decoder actually emits.

Changed

A declared interlaced field order is verified against decoded frames before it routes a stream to software. (#232)

H.264 that declares interlaced carriage takes the software path so DeinterlaceFilter can run, because tvOS AVPlayer does not deinterlace and 1080i broadcast would otherwise comb. The rule is right. What it read as evidence was not.

Progressive-in-interlaced-carriage (PsF) trips it for nothing. Blu-ray has no 1080p25, so European 25 fps masters ship as 1080i25: interlaced carriage, progressive pictures. Those titles took the software detour and then never deinterlaced, because SoftwareVideoDecoder engages the filter on AV_FRAME_FLAG_INTERLACED and on nothing else, and on PsF that flag never appears. What played was already the un-deinterlaced picture, produced without hardware decode.

The two signals disagree by construction, in FFmpeg itself:

h264_parser.c sets AV_FIELD_TT for a FRAME-coded picture on SEI pic_struct = TOP_BOTTOM alone. It weighs neither ct_type nor how the slices were actually coded. That is the value that lands in codecpar.field_order, which is what the routing rule read.

h264_slice.c flags that same picture only when it is field- or MBAFF-coded, and a clock_timestamp carrying ct_type overrides even that. That is the value that lands on the frame, and it is what the deinterlacer acts on.

Routing consumed the structurally less informed of the two.

InterlaceProbe now decodes a short sample before routing. It does not try to answer "is this content progressive", which would be a judgement about pictures. It answers the narrower question the rule actually rests on: will the deinterlacer ever engage. The predicate it applies is the engagement predicate, so a sample in which the flag never appears proves the software detour is a no-op for that stream, and the native path renders the same frames with hardware decode.

Because the probe's predicate is the runtime's predicate, its false positives are harmless by construction. A stream whose first frame is flagged only through libavcodec's prev_interlaced_frame = 1 decoder-init bias is exactly the stream on which the real software path would engage the filter at that same frame and latch it. Reporting "interlaced" there is agreement with the runtime, not a miss.

Bounds on the check:

Only a clean sample overrules the declaration. An inconclusive one (decoder open failed, read error, fewer than 12 frames, budget expired) keeps the previous routing.

A flagged frame ends the sample at once, so genuinely interlaced material pays a frame or two of decode rather than a full sample.

Seekable VOD only. The sample moves the read position of the demuxer the session reuses, and the reader's deadline state is demux-thread-only while the session demuxer runs with prefetch, so no read deadline is armed from the load path. Live 1080i broadcast, the case the rule exists for, is neither seekable nor mis-declared.

Only the declared-interlace rule can be overruled. A codec that is software-bound anyway (MPEG-2, VC-1, VP9, AV1 without hardware) never triggers a probe, since the answer could not change its route.

Verification

Synthesized 1920x1080 fixtures, both declaring field_order=tt, run end to end through aetherctl play:

Fixture Dispatch Deinterlacer
progressive frames under a TT declaration → native, plays clean never built at frame size
genuinely interlaced frames → software engaged [hardware]: 1920x1080

The second row is the regression check: genuinely interlaced material routes and deinterlaces exactly as before.

Probe cost on those fixtures, measured on Apple silicon: 29 ms for genuinely interlaced material (early exit on frame 1), 104 ms for a full 45-frame sample. skip_idct was measured and rejected, it changed nothing (108 ms against 104 ms).

Eight tests cover the probe and the routing gate, including a fixture carrying a container fiel atom so field_order reads TT while no decoded frame is flagged, which is the reporter's signature.

Acknowledgements

Reported by @rrgomes, with the layer analysis (field_order=tt against interlaced_frame=0 across every sampled frame, and the absence of a frame-size [Deinterlace] engaged line in the session logs) that located the divergence and its cost.