Skip to content

External tracks can address a container's Nth subtitle stream, and share one pass over it (#266) - #267

Merged
superuser404notfound merged 1 commit into
mainfrom
feat/issue-266-external-subtitle-stream-index
Jul 31, 2026
Merged

External tracks can address a container's Nth subtitle stream, and share one pass over it (#266)#267
superuser404notfound merged 1 commit into
mainfrom
feat/issue-266-external-subtitle-stream-index

Conversation

@superuser404notfound

Copy link
Copy Markdown
Owner

Implements the ExternalSubtitleTrack.sourceStreamIndex request in #266, plus the refetch that the index would otherwise make worse.

The defect

SubtitleDecoder.decodeFileSync stopped at the first AVMEDIA_TYPE_SUBTITLE stream and ExternalSubtitleTrack carried 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 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, so nothing existing moves. An index that is out of range or names a non-subtitle stream throws SubtitleDecoderError.streamIndexNotSubtitle rather 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, and nil for external entries by design). The external index reaches the decoder through the registry instead, so the two never mix.

One pass per container. decodeFile now 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_ALL would 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

  • "Report an invalid index" has no host-facing channel today. A sidecar decode failure only reaches 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.codec still comes from the URL extension, so a container URL reports subrip and preserveASSMarkup will not engage for ASS streams. Set formatHint: "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, nil mixed 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=complete clean, tvOS Simulator build clean.

Reported by @edde746.

…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
superuser404notfound merged commit 0672c51 into main Jul 31, 2026
3 checks passed
@superuser404notfound
superuser404notfound deleted the feat/issue-266-external-subtitle-stream-index branch July 31, 2026 08:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant