External tracks can address a container's Nth subtitle stream, and share one pass over it (#266) - #267
Merged
superuser404notfound merged 1 commit intoJul 31, 2026
Conversation
…ternal track An external subtitle URL can be a container rather than a sidecar, and a host registers one ExternalSubtitleTrack per stream against that same URL. The sidecar decoder stopped at the first AVMEDIA_TYPE_SUBTITLE stream and the descriptor carried no index, so every such track decoded that same stream: three selectable tracks rendering identical cues, three full container reads. ExternalSubtitleTrack.sourceStreamIndex names the stream as an ABSOLUTE AVStream index inside the container at `url`, matching the convention that embedded track ids are stream indices. nil keeps the pre-#266 first-subtitle-stream behaviour. An index that is out of range or names a non-subtitle stream throws streamIndexNotSubtitle rather than falling back, since a silent fallback is indistinguishable from the behaviour the index exists to escape. The field indexes the container at the track's URL, NOT the played media, so it is deliberately kept out of NativeSubtitleTrackEntry.sourceStreamIndex (which indexes the main demuxer and stays nil for external entries). Decoding several streams now runs off ONE pass over the container: decodeFile takes [Int32?] and returns positionally, one decoder state per requested stream (timing anchor, ASS PlayRes and open-image bookkeeping are all per-stream). Load-declared tracks sharing a URL and headers collapse into one fill job, so N tracks on one MKV cost one read instead of N, which AVDISCARD_ALL cannot shorten on Matroska anyway. A shared pass fails as a whole, so a failure retries the targets individually: one host-side index mistake must not blank the container's other tracks. A store that still could not be filled stays unfinished rather than serving a complete but blank rendition. Tested against an embedded 1.7 KB Matroska fixture (video at stream 0, English at 1, Spanish at 2) so the absolute-index contract is covered in CI without a fixture fetch. Reported by edde746. (#266) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HX5zV3Fcf7Nzq4DYGdXvQE
superuser404notfound
deleted the
feat/issue-266-external-subtitle-stream-index
branch
July 31, 2026 08:35
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the
ExternalSubtitleTrack.sourceStreamIndexrequest in #266, plus the refetch that the index would otherwise make worse.The defect
SubtitleDecoder.decodeFileSyncstopped at the firstAVMEDIA_TYPE_SUBTITLEstream andExternalSubtitleTrackcarried no index, so registering one track per stream of a container URL produced N selectable tracks all rendering the first stream, each refetching the whole container. Confirmed at the reported lines.What changed
ExternalSubtitleTrack.sourceStreamIndex: Int32?names an ABSOLUTEAVStreamindex inside the container aturl, matching the convention that embedded track ids are stream indices.nilkeeps the pre-#266 first-subtitle-stream behaviour, so nothing existing moves. An index that is out of range or names a non-subtitle stream throwsSubtitleDecoderError.streamIndexNotSubtitlerather than falling back.The field is deliberately kept out of
NativeSubtitleTrackEntry.sourceStreamIndex, which carries the same name for a different axis (the MAIN demuxer's stream, arming the pump tap, andnilfor external entries by design). The external index reaches the decoder through the registry instead, so the two never mix.One pass per container.
decodeFilenow also takes[Int32?]and returns positionally, with one decoder state per requested stream (the timing anchor, the ASS PlayRes and the open-image bookkeeping are all per-stream and were locals before). Load-declared tracks sharing a URL and headers collapse into a single fill job, so three tracks on one MKV cost one read instead of three.AVDISCARD_ALLwould not have helped: a Matroska demuxer reads every discarded byte anyway.A shared pass fails as a whole, so a failure retries the targets individually. One host-side index mistake must not blank the container's other tracks. A store that still could not be filled stays unfinished, since a finished empty store would serve a complete but blank rendition.
Two notes for the request itself
EngineLog; the host sees an empty overlay. The error is thrown and named, but the visibility the wording implies does not exist yet. Worth its own issue if it matters.TrackInfo.codecstill comes from the URL extension, so a container URL reportssubripandpreserveASSMarkupwill not engage for ASS streams. SetformatHint: "ass"on those tracks. The real codec is only known after the network probe, well after registration publishes the track.Tests
16 new tests over a 1.7 KB Matroska fixture embedded as base64 (video at stream 0, English at 1, Spanish at 2), so the absolute-index contract runs in CI with no fixture fetch. The layout is what makes the test sharp: an ordinal reading of index 1 would land on Spanish.
Covered: default first-stream behaviour, explicit index, absolute-not-ordinal, non-subtitle index, out-of-range index, one-pass separation with per-stream timing,
nilmixed with explicit indices, a repeated index, job grouping (shared container, distinct containers, differing headers, duplicate registrations), and the individual-retry fallback.Full suite green (1361 tests),
-strict-concurrency=completeclean, tvOS Simulator build clean.Reported by @edde746.