Skip to content

feat(session): resolve the audio track from an ordered language preference - #32

Merged
bilipp merged 1 commit into
mainfrom
feature/200-preferred-audio-language
Sep 1, 2026
Merged

feat(session): resolve the audio track from an ordered language preference#32
bilipp merged 1 commit into
mainfrom
feature/200-preferred-audio-language

Conversation

@bilipp

@bilipp bilipp commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Summary

Engine half of a paired change for bilipp/Lume#200 — a preferred-audio-language setting in the Lume app. This adds the API the app drives and resolves the audio track against it at open time.

Companion app PR: bilipp/Lume#211.

Implementation

PlayerConfiguration gains two fields, both defaulting to today's behaviour:

  • preferredAudioLanguages: [String] — ordered, already-normalised bare tags supplied by the caller. The engine does not normalise; the app does, and passes ["de", "en"].
  • autoEnableForcedSubtitlesForForeignAudio: Bool — off by default.

buildPipeline(info:) resolves the audio track against that preference before demuxer.resume(), falling back to the existing first(where: \.isDefault) ?? first when the list is empty or nothing matches. With an empty list the resolution is byte-for-byte what it was.

Why at open, not after

Selecting after open() routes through selectAudioTrackseek(to: position), and position is 0 until the first frame — which silently wipes a configuration.startPosition VOD resume. Worse for live: MediaInfo.isSeekable is true whenever pb->seekable != 0, which many live IPTV HTTP endpoints report, and providers drop the connection on the seek. Resolving during buildPipeline costs no lane teardown and no seek.

Matching

TrackLanguageMatcher carries the ISO 639-2/B alias table Foundation does not provide — Locale.LanguageCode("ger").identifier(.alpha2) is nil while "deu" resolves, and ger/fre/chi/cze/dut/gre are exactly what IPTV containers ship. Candidates are scored by preference rank, a commentary / audio-description penalty, and exact-over-regional-variant; track titles are matched per whitespace/punctuation token, never by substring, so GER 5.1 matches but Hungarian does not match a hu-seeking pass via gar. und / mul / mis / zxx / vo / vost / multi / original resolve to nothing, which means "leave the container's choice alone".

Testing

  • swift build clean
  • swift test85/85 tests in 14 suites pass
  • Tests added — TrackLanguageMatcherTests plus four PlayerSessionTests cases asserting selectedAudioTrackIndex after open() with a preference set
  • Empty-preference path asserted to keep the previous default selection

Tests/…/Fixtures/generate-fixtures.sh now generates the multi-audio-language MKV PLAN.md's testing section already anticipated (two audio streams tagged eng / ger, eng dispositioned default). Generated media stays uncommitted, as with the other fixtures.

Reviewer notes

  • Additive public API only — two public vars with defaults on an existing public struct that has a public init(), safe under the package's -enable-library-evolution.
  • Must land with feat(player): auto-select the viewer's preferred audio language Lume#211. The app resolves ../LumeEngine as a local SPM package, so that branch does not build against an engine checkout without this commit. Sideload CI needs an ENGINE_REF bump once this is tagged — deliberately not done here, since pointing the workflow at a tag that does not exist yet breaks the build harder than the current skew.
  • A near-twin of TrackLanguageMatcher lives app-side. That duplication is deliberate for now: sharing it would mean a new SPM target plus a package-product dependency on the app and test targets. Worth revisiting if the two drift.

…rence

PlayerConfiguration gains `preferredAudioLanguages` (ordered, already-normalised
bare tags supplied by the caller) and `autoEnableForcedSubtitlesForForeignAudio`.
buildPipeline resolves the audio track against that preference before
demuxer.resume(), so the choice costs no lane teardown and no seek — selecting
after open() would seek to a position that is 0 until the first frame, wiping a
startPosition VOD resume, and MediaInfo.isSeekable is true for many live IPTV
endpoints that providers then drop.

Both fields default to the previous behaviour, so an empty preference resolves
`first(where: \.isDefault) ?? first` exactly as before.

TrackLanguageMatcher carries the ISO 639-2/B alias table Foundation does not
provide — Locale.LanguageCode("ger").identifier(.alpha2) is nil while "deu"
resolves — and scores candidates by preference rank, commentary/audio-description
penalty, and exact-over-regional-variant, matching track titles per token rather
than by substring.

Paired with the Lume app change for bilipp/Lume#200; the app resolves
../LumeEngine as a local SPM package, so the two commits must land together.
@bilipp
bilipp merged commit 2e3a9e9 into main Sep 1, 2026
4 checks passed
@bilipp
bilipp deleted the feature/200-preferred-audio-language branch September 1, 2026 22:05
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