6.23.1 - A set ends where its author ended it
Drop-in from 6.23.0, no API change. Two fixes, both bitmap subtitles after a seek, both reported on 6.21.0 by @rrgomes in #362.
A set ends where its author ended it
A PGS display set has no end of its own. Whatever packet follows it on the stream closes it, its own zero-object clear or the next composition, and the decoder meanwhile stamps FFmpeg's end_display_time = UINT32_MAX placeholder on the cue.
The drain decodes a window bounded at playhead + 60 s, and that forward edge falls wherever it falls. Where it landed between a set and the clear a few seconds later, the set published with the placeholder, the drain cursor moved on, and nothing went back for the clear: the cursor only moves forward and the next reset starts at the next landing. The set was then closed by whatever composition turned up next, tens or hundreds of seconds away. The report measured 3.55 s authored delivered as 76.7 s, with 366 s, 417 s and 817 s elsewhere in the same session, and the progression in its log (2879.5, then 2806.1, then 2769.1) is each pass closing the same set at the nearest thing it happened to decode.
The answer was in the packet store the whole time. The pump harvests far ahead of the drain window, so the clear is retained at the moment the set publishes and reading its PTS needs no decode: it is exactly the pgsTrimAt the successor would apply once the window reached it. Every tick now closes each still-open cue at SubtitlePacketStore.firstPTS(streamIndex:after:), over the whole retained window rather than over what that tick decoded, so a set left open by an earlier tick is closed by the first tick that finds the answer stored.
For the store to be able to answer, the harvest has to lead the decode. The forward prefetch parked at playhead + 60 s as well, which made the set at the window's edge systematically the one whose clear was stored nowhere, so it now parks a 15 s margin beyond the drain window. That costs no extra bytes over a session; the reader is sequential either way and the park only decides when. Where the store genuinely has nothing after a set, its harvest frontier or a stream cut short, the cue stays open and 6.22.1's reconstruction-window close still owns it: inventing an end there is the laundering this replaces.
A hole in the harvest is not a silence in the source
The same report's second face: stretches where sets were missing from the published window altogether, eleven authored sets between 2966.6 s and 2988.5 s arriving as two.
A seek restarts the segment pump behind the landing and it fills forward, while the store still holds an island the previous run harvested further ahead. The drain window then reads as "packets, hole, packets", and decoding straight across it carried the cursor to the far side. The hole's packets land a second later and are never read, so that stretch of the title has no subtitles at all until some later seek happens to reset the window behind it, and the set before the hole is closed at the island rather than at its own clear.
The size of the gap cannot tell a hole from a silence the author left, and a threshold on it does real damage: a set is separated from its own clear by its display duration, so a 3 s rule stopped the tick at every authored line and delivery fell to a sixth of the sets in a fixture run. Harvest order can tell them apart. A run reads a stream forwards, so within one run PTS and sequence rise together, and a PTS-ascending pair whose sequence descends is two runs meeting over a span neither of them has read. The store stamps that order on every entry, a re-harvest takes a fresh stamp, which is what releases the wait, and the drain cursor carries its own, without which a hole opening exactly at the cursor leaves no pair in the window to compare.
Three bounds keep the wait from becoming a defect of its own: it is honoured only at or after the playhead, so a landing line is never delayed; the playhead catching up ends it, because from there the island is what the viewer needs next; and a tick budget ends it regardless, so a boundary the harvest never closes costs 10 s of a window that runs a 60 s lead.
Verifying it
A tick that waited now says so, rather than reading as an empty window:
[AetherEngine] #357 subtitle-delivery ... packets=0 events=0 cues=0 admitted=0 published=0 outcome=harvestHole gapAt=280.02
aetherctl play gained --seek-count, which stops seeking after N seeks so a run can be a burst and then play. Both halves are needed for anything about what a seek sequence leaves behind, and every seek harness here previously ran its bursts to the end of the session.
Measured against a fixture of a real PGS stream repeated every 26 s into a 400 s H.264 file (80 authored sets, clears 3.5 s to 4 s after their set), with the published window compared per cue against ffprobe's packets:
- 25 seeks in 50 s, three runs each. On 6.23.0: 10, 12 and 10 sets carried an end that was not the authored one, the worst 110 s late, and 5 more per run never closed at all. On 6.23.1: 0, 0 and 1, none open.
- Burst of two (populate an island at 300 s, seek back to 231 s), then play through the region. On 6.23.0: all three runs lost an authored set entirely and closed its predecessor 11.5 s late. On 6.23.1: all three delivered all 11 sets on their authored bounds.
Hosts need no change.