5.23.11 - Subtitle side-reader park + prefetch restart
Patch release. Two defects in the subtitle side readers, both found by rrgomes while reading the software path for #220.
Fixed
The side readers parked on when the next cue arrived, not on how far they had read. (#230)
The park can only be evaluated from a packet the loop receives, and with every non-subtitle stream on AVDISCARD_ALL the only packets it receives are subtitle packets. Between two cues there is no control point at all: a single av_read_frame call walks whatever lies between them. On a dense PGS track that is bounded by the cue spacing. On a sparse track, a long dialogue-free stretch, or a forced-subtitle track with a handful of cues per hour it is not bounded, and the reader runs arbitrarily far past the lead edge on a second connection to the origin.
One non-subtitle stream now stays deliverable at AVDISCARD_NONKEY. On video that yields one packet per IRAP, which is the right granularity for a 60 s window; audio is the fallback and cover art is excluded by disposition. Those packets are freed unharvested and exist only to place the read on the timeline, by DTS rather than PTS, since a video PTS runs ahead of the bytes when the stream carries B-frames.
runNativeSubtitleReaders had the same defect. It has always evaluated its park per delivered packet and was simply starved of deliveries, so it takes a pacing stream too. The whole-program reader does not park and does not take one.
Worth knowing when reasoning about side-reader cost: AVDISCARD_ALL avoids the byte read in mov, which skips the avio_seek and the read outright, but not in Matroska, where ebml_parse reads each cluster's blocks off the wire and matroska_parse_block only then checks discard. On MKV a fully discarded side demuxer still pulls every video and audio byte and throws it away.
A prefetch session that died on a read error stayed dead for the rest of playback. (#231)
The loop left on the first failed read through try? demuxer.readPacket(), which cannot tell EOF from an error. A transport failure on the side reader, a stall that exhausted the read deadline, a reconnect that gave up: all of them ended the session permanently, and the exit line reported cancelled=false for every one of them. The only thing that started a new session was a drain-tick jump, meaning a seek or a producer re-anchor, so a viewer who did not seek lost every cue beyond the pump's own forward park with no signal of it.
The loop now reports why it stopped, and only a read failure restarts. The restart is bounded twice over. Three consecutive failures with nothing harvested between them end it, backing off one, two, four seconds. A session that harvested cues before breaking is a fresh transport failure and gets a fresh budget. A total of eight restarts caps a source that fails in a loop after one packet each time. Each attempt re-anchors at the current playhead and takes its own independent reader on a custom source, and exhausting the budget logs what it costs rather than going quiet.
Notes
Both fixes are covered by tests (Issue230PrefetchReadPositionParkTests, Issue231PrefetchRestartTests). Neither closes #220 itself, which is still open on the memory side: these explain the side reader's read volume on the wire, not the heap.