Skip to content

6.1.4 - A/53 caption timing

Choose a tag to compare

@superuser404notfound superuser404notfound released this 30 Jul 17:44

A timing fix on the in-picture closed-caption path. Anyone rendering A/53 captions from a broadcast source wants this one.

Fixed

  • In-picture A/53 closed captions are timed against the source again, not against the playlist. Every A/53 caption was displaced by the current playlist shift, and because that shift is recomputed on each producer restart, the displacement changed after every seek. Reported by edde746, who traced the value flow from source and measured the shift magnitude without a caption-bearing clip on hand (#259).

    The axis, not the time base. A/53 extraction rides the segment producer's per-packet finalize step, alongside the HDR10+ scan, which is the right place for it: decode order, repaired DTS, and the packet is still in the source time base because the rescale to the muxer's happens on the next line. What that spot no longer has is the source axis. The pump rebases every packet onto the output timeline well above it (packet.pointee.pts -= activeShift), so the timestamps handed to the caption tap were on the item axis while everything downstream reads them as source PTS: the decoded cues feed subtitleCues, and a host renders those against currentTime, which folds that same shift back in.

    Both sibling feeds were already correct, which is why nothing pointed at this. A demuxable eia_608 / c608 caption track is handed to the tap at the top of the read loop, before the rebase, and the software path reads its triplets out of AV_FRAME_DATA_A53_CC side data, where no shift exists at all. NativeSubtitleCueStore states the same contract from the other side: it holds cues on the source axis and subtracts the shift when a window is queried.

    The magnitude is the shift, firstActualVideoDts - desiredFirstVideoTfdt. On a clip with B-frames it is two frames at head of stream (83 ms at 24 fps); a restart that lands off its planned keyframe has been measured in seconds; and a broadcast MPEG-TS source whose first DTS sits far from zero, the case this path exists for, carries it from the first packet.

    foldingShiftBack folds it back at the observation site, mirroring segmentIndex(forSourcePts:), which already does exactly that before comparing against the source-axis segment boundaries. NOPTS and an unresolved shift pass through untouched.

    The regression witness needed a caption-bearing source, and no encoder emits A/53 from a synthetic one, so Scripts/fetch-fixtures.sh now builds it: user_data_registered_itu_t_t35 SEI injected into a synthetic H.264 clip, a cc_data on every access unit, and a complete pop-on sequence every 2 s held inside a single access unit so the caption is atomic there and survives B-frame reordering. FFmpeg's own CEA-608 decoder reads the captions back out of it.

Upgrading

.package(url: "https://github.com/superuser404notfound/AetherEngine", from: "6.1.4")

No API change and no source change for consumers. Hosts that render subtitleCues against sourceTime, which is the documented contract, get the corrected timing with no work.