mirror of
https://github.com/yattee/yattee.git
synced 2026-08-05 23:01:28 +00:00
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:
@@ -62,6 +62,37 @@ final class PlayerService {
|
||||
/// Currently loaded caption.
|
||||
private(set) var currentCaption: Caption?
|
||||
|
||||
/// Tracks reported by mpv for the currently loaded file (embedded and external).
|
||||
private(set) var embeddedTracks: [MPVTrack] = []
|
||||
|
||||
/// Embedded (in-container) audio tracks of the current file.
|
||||
var embeddedAudioTracks: [MPVTrack] {
|
||||
embeddedTracks.filter { $0.type == .audio && !$0.isExternal }
|
||||
}
|
||||
|
||||
/// Embedded (in-container) subtitle tracks of the current file.
|
||||
var embeddedSubtitleTracks: [MPVTrack] {
|
||||
embeddedTracks.filter { $0.type == .sub && !$0.isExternal }
|
||||
}
|
||||
|
||||
/// The embedded audio track mpv currently plays, if any.
|
||||
var selectedEmbeddedAudioTrackID: Int? {
|
||||
embeddedAudioTracks.first(where: \.isSelected)?.trackID
|
||||
}
|
||||
|
||||
/// The embedded subtitle track mpv currently shows, if any.
|
||||
var selectedEmbeddedSubtitleTrackID: Int? {
|
||||
embeddedSubtitleTracks.first(where: \.isSelected)?.trackID
|
||||
}
|
||||
|
||||
/// Embedded-track picks that should survive same-video reloads
|
||||
/// (quality switch, audio-mode toggle, buffer-stall retry).
|
||||
private var desiredEmbeddedAudioTrackID: Int?
|
||||
private var desiredEmbeddedSubtitleTrackID: Int?
|
||||
|
||||
/// Whether preferred-language auto-selection already ran for this load.
|
||||
private var didAutoSelectEmbeddedTracks = false
|
||||
|
||||
/// The current download being played, if any.
|
||||
private(set) var currentDownload: Download?
|
||||
|
||||
@@ -234,6 +265,15 @@ final class PlayerService {
|
||||
availableStreams = []
|
||||
}
|
||||
|
||||
// Embedded-track state: the mpv-reported list always refreshes on load,
|
||||
// but sticky user picks only reset when the video actually changes.
|
||||
embeddedTracks = []
|
||||
didAutoSelectEmbeddedTracks = false
|
||||
if isNewVideo {
|
||||
desiredEmbeddedAudioTrackID = nil
|
||||
desiredEmbeddedSubtitleTrackID = nil
|
||||
}
|
||||
|
||||
// Clear sponsor block state from previous video
|
||||
state.sponsorSegments = []
|
||||
state.currentSegment = nil
|
||||
@@ -619,6 +659,10 @@ final class PlayerService {
|
||||
preDownloadedSubtitleFolders.removeAll()
|
||||
cleanupAllTempSubtitles()
|
||||
currentCaption = nil
|
||||
embeddedTracks = []
|
||||
desiredEmbeddedAudioTrackID = nil
|
||||
desiredEmbeddedSubtitleTrackID = nil
|
||||
didAutoSelectEmbeddedTracks = false
|
||||
lastSkippedSegmentID = nil
|
||||
nowPlayingService.clearNowPlaying()
|
||||
}
|
||||
@@ -1303,6 +1347,8 @@ final class PlayerService {
|
||||
|
||||
mpvBackend.loadCaption(caption)
|
||||
currentCaption = caption
|
||||
// An explicit external pick (or Off) overrides any embedded-subtitle intent
|
||||
desiredEmbeddedSubtitleTrackID = nil
|
||||
|
||||
if let caption {
|
||||
LoggingService.shared.logPlayer("Loaded caption: \(caption.displayName)")
|
||||
@@ -1311,6 +1357,70 @@ final class PlayerService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Selects an embedded audio track by mpv track id (live, no reload).
|
||||
/// Only works with MPV backend.
|
||||
func selectEmbeddedAudioTrack(_ trackID: Int) {
|
||||
guard let mpvBackend = currentBackend as? MPVBackend else { return }
|
||||
|
||||
desiredEmbeddedAudioTrackID = trackID
|
||||
mpvBackend.selectEmbeddedAudioTrack(trackID)
|
||||
LoggingService.shared.logPlayer("Selected embedded audio track \(trackID)")
|
||||
}
|
||||
|
||||
/// Selects an embedded subtitle track by mpv track id (nil = off).
|
||||
/// Only works with MPV backend.
|
||||
func selectEmbeddedSubtitleTrack(_ trackID: Int?) {
|
||||
guard let mpvBackend = currentBackend as? MPVBackend else { return }
|
||||
|
||||
desiredEmbeddedSubtitleTrackID = trackID
|
||||
currentCaption = nil
|
||||
mpvBackend.selectEmbeddedSubtitleTrack(trackID)
|
||||
LoggingService.shared.logPlayer("Selected embedded subtitle track \(trackID.map(String.init) ?? "off")")
|
||||
}
|
||||
|
||||
/// Re-applies a sticky embedded-track pick after a reload, or auto-selects
|
||||
/// preferred languages once per load. Called whenever mpv's track list changes.
|
||||
private func reapplyOrAutoSelectEmbeddedTracks() {
|
||||
guard let mpvBackend = currentBackend as? MPVBackend else { return }
|
||||
|
||||
// Re-apply sticky user picks. Loop-safe: selecting fires one more
|
||||
// track-list update in which the track is already selected.
|
||||
if let desired = desiredEmbeddedAudioTrackID,
|
||||
let track = embeddedAudioTracks.first(where: { $0.trackID == desired }),
|
||||
!track.isSelected {
|
||||
mpvBackend.selectEmbeddedAudioTrack(desired)
|
||||
}
|
||||
if let desired = desiredEmbeddedSubtitleTrackID,
|
||||
let track = embeddedSubtitleTracks.first(where: { $0.trackID == desired }),
|
||||
!track.isSelected {
|
||||
mpvBackend.selectEmbeddedSubtitleTrack(desired)
|
||||
}
|
||||
|
||||
// Auto-select preferred languages once per load. Skipped while the
|
||||
// list is empty so the reset delivered at load start doesn't consume
|
||||
// the one-shot flag.
|
||||
guard !didAutoSelectEmbeddedTracks, !embeddedTracks.isEmpty else { return }
|
||||
didAutoSelectEmbeddedTracks = true
|
||||
|
||||
if desiredEmbeddedAudioTrackID == nil,
|
||||
embeddedAudioTracks.count > 1,
|
||||
let match = embeddedAudioTracks.first(where: { $0.matchesLanguage(settingsManager?.preferredAudioLanguage) }),
|
||||
!match.isSelected {
|
||||
mpvBackend.selectEmbeddedAudioTrack(match.trackID)
|
||||
LoggingService.shared.logPlayer("Auto-selected embedded audio track \(match.trackID) (\(match.displayName))")
|
||||
}
|
||||
|
||||
// External captions win: the preferred-caption flow sets currentCaption
|
||||
// before mpv reports any tracks.
|
||||
if desiredEmbeddedSubtitleTrackID == nil,
|
||||
currentCaption == nil,
|
||||
let match = embeddedSubtitleTracks.first(where: { !$0.isForced && $0.matchesLanguage(settingsManager?.preferredSubtitlesLanguage) }),
|
||||
!match.isSelected {
|
||||
mpvBackend.selectEmbeddedSubtitleTrack(match.trackID)
|
||||
LoggingService.shared.logPlayer("Auto-selected embedded subtitle track \(match.trackID) (\(match.displayName))")
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads a local subtitle file from disk.
|
||||
/// Only works with MPV backend.
|
||||
/// - Parameter url: The local file URL of the caption file
|
||||
@@ -2902,6 +3012,11 @@ extension PlayerService: PlayerBackendDelegate {
|
||||
state.setRetryState(retryState)
|
||||
}
|
||||
|
||||
func backend(_ backend: any PlayerBackend, didUpdateTracks tracks: [MPVTrack]) {
|
||||
embeddedTracks = tracks
|
||||
reapplyOrAutoSelectEmbeddedTracks()
|
||||
}
|
||||
|
||||
func backend(_ backend: any PlayerBackend, didRequestStreamRefresh atTime: TimeInterval?) {
|
||||
LoggingService.shared.logPlayer("Stream refresh requested at time: \(atTime ?? -1)")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user