Add embedded audio/subtitle track selection for multi-track files

Files with multiple embedded tracks (e.g. MKV from media sources or
downloads) previously exposed none of them: mpv's track-list was never
read, aid was never set, and sid was only ever no/auto. The quality
selector showed no Audio or Subtitles tab for such files.

MPVClient now observes track-list/aid/sid and delivers a coalesced,
leniently decoded [MPVTrack] snapshot; selection state is derived from
the reported "selected" flags. Embedded tracks are switched live via
aid/sid (no reload), surfaced in the existing Audio/Subtitles sections
alongside external captions on iOS, tvOS and macOS. External sub-add/
audio-add tracks are filtered out to avoid double-listing. User picks
are sticky across same-video reloads (quality switch, audio mode,
retries) and preferred audio/subtitle languages auto-select a matching
embedded track once per load, with external captions taking precedence.
This commit is contained in:
Arkadiusz Fal
2026-07-29 22:14:46 +02:00
parent 150ebf42a4
commit 67ed8c322b
17 changed files with 841 additions and 11 deletions

View File

@@ -99,6 +99,9 @@ final class MPVBackend: PlayerBackend {
private var currentCaption: Caption?
private var pendingAutoplay: Bool = false
/// Tracks reported by mpv for the loaded file (embedded and external).
private(set) var currentTracks: [MPVTrack] = []
// Retry mechanism: 4 attempts with increasing timeouts and delays
// Attempt 1: 3s timeout, 1s delay | Attempt 2: 3s timeout, 3s delay
// Attempt 3: 10s timeout, 5s delay | Attempt 4: 10s timeout, fail
@@ -481,6 +484,10 @@ final class MPVBackend: PlayerBackend {
currentCaption = nil
mpvClient?.removeAllSubtitlesAsync()
// Clear stale track info; the observer repopulates it once the new file loads
currentTracks = []
delegate?.backend(self, didUpdateTracks: [])
// Reset first-frame tracking for new content
renderView?.resetFirstFrameTracking()
applyStreamFrameRateHint(stream)
@@ -910,6 +917,22 @@ final class MPVBackend: PlayerBackend {
currentCaption
}
// MARK: - Embedded Tracks
/// Select an embedded audio track by mpv track id (live, no reload).
func selectEmbeddedAudioTrack(_ trackID: Int) {
mpvClient?.setAudioTrack(trackID)
}
/// Select an embedded subtitle track by mpv track id (nil = off).
/// Deliberately no `sub-remove`: external captions stay loaded so the user
/// can switch back without a re-download. Selection is no longer
/// represented by a Caption, so `currentCaption` is cleared.
func selectEmbeddedSubtitleTrack(_ trackID: Int?) {
currentCaption = nil
mpvClient?.setSubtitleTrack(trackID)
}
/// Update subtitle appearance settings on the active MPV instance.
/// Call this after changing subtitle settings to apply them immediately without restarting playback.
func updateSubtitleSettings() {
@@ -1573,6 +1596,14 @@ extension MPVBackend: MPVClientDelegate {
// Cache state is used for buffer display on seek bar - no action needed here
}
nonisolated func mpvClient(_ client: MPVClient, didUpdateTrackList tracks: [MPVTrack]) {
Task { @MainActor [weak self] in
guard let self else { return }
self.currentTracks = tracks
self.delegate?.backend(self, didUpdateTracks: tracks)
}
}
nonisolated func mpvClientDidEndFile(_ client: MPVClient, reason: MPVEndFileReason, errorCode: Int32, errorString: String?) {
Task { @MainActor [weak self] in
self?.handleEndFile(reason: reason, errorCode: errorCode, errorString: errorString)