feat: support YouTube VTT subtitle selection
This commit is contained in:
@@ -201,6 +201,10 @@ final class PlayerModel: ObservableObject {
|
||||
@Published private(set) var subtitleError: String?
|
||||
private var subtitleDocument: WebVTTDocument?
|
||||
private var subtitleTask: Task<Void, Never>?
|
||||
var canSelectSubtitle: Bool {
|
||||
if case .youtube? = currentMedia, castDevice != nil { return false }
|
||||
return !subtitleTracks.isEmpty
|
||||
}
|
||||
@Published var seekPosition = 0.0
|
||||
@Published var isSeeking = false
|
||||
@Published private(set) var volume = 1.0
|
||||
@@ -380,7 +384,9 @@ final class PlayerModel: ObservableObject {
|
||||
guard canCast, let source = currentMedia, let handle else { return }
|
||||
cancelStreamResolution()
|
||||
commandError = nil
|
||||
casting.start(to: device, source: source, title: fileName ?? "Terebi", subtitle: selectedSubtitle,
|
||||
// YouTube captions remain local-only: never prefetch optional tracks during Cast handoff.
|
||||
let subtitle: Int? = if case .youtube = source { nil } else { selectedSubtitle }
|
||||
casting.start(to: device, source: source, title: fileName ?? "Terebi", subtitle: subtitle,
|
||||
handle: handle, snapshot: state, playing: playing ?? effectivePlaying,
|
||||
volume: volume, audibleVolume: lastAudibleVolume)
|
||||
revealControls()
|
||||
@@ -404,7 +410,7 @@ final class PlayerModel: ObservableObject {
|
||||
state = projection.state
|
||||
volume = projection.volume
|
||||
if volume > 0 { lastAudibleVolume = volume }
|
||||
selectedSubtitle = projection.subtitle
|
||||
if case .resolved? = currentMedia { selectedSubtitle = projection.subtitle }
|
||||
if !isSeeking && !state.seek_pending { seekPosition = state.position }
|
||||
if let handle { _ = player_report_remote_state(handle, &state) }
|
||||
saveProgress()
|
||||
@@ -597,6 +603,11 @@ final class PlayerModel: ObservableObject {
|
||||
|
||||
private func resolveStream(_ stream: StreamURL, change: MediaChange) {
|
||||
replaceMetadata(for: nil)
|
||||
if !change.isQuality {
|
||||
if castDevice != nil { stopCasting(resume: false) }
|
||||
selectSubtitle(nil)
|
||||
subtitleTracks = []
|
||||
}
|
||||
switch stream {
|
||||
case .youtube(let url): resolveYouTube(url, height: nil, change: change)
|
||||
case .miruro(let mediaID, let episode): resolveMiruro(mediaID: mediaID, episode: episode, change: change)
|
||||
@@ -620,6 +631,7 @@ final class PlayerModel: ObservableObject {
|
||||
|
||||
func selectSubtitle(_ index: Int?) {
|
||||
if casting.active {
|
||||
guard canSelectSubtitle else { return }
|
||||
if !casting.setSubtitle(index) { commandError = "Could not change Cast subtitles." }
|
||||
return
|
||||
}
|
||||
@@ -1103,16 +1115,22 @@ final class PlayerModel: ObservableObject {
|
||||
if let fileName, !source.url.isFileURL, let id = queue.currentID {
|
||||
queue.updateLabel(fileName, for: id)
|
||||
}
|
||||
let previousSubtitleLanguage = selectedSubtitle.flatMap {
|
||||
subtitleTracks.indices.contains($0) ? subtitleTracks[$0].language : nil
|
||||
let previousSubtitle = selectedSubtitle.flatMap {
|
||||
subtitleTracks.indices.contains($0) ? subtitleTracks[$0] : nil
|
||||
}
|
||||
selectSubtitle(nil)
|
||||
if case .resolved(let resolved, _) = source {
|
||||
subtitleTracks = resolved.subtitles
|
||||
let defaultIndex = subtitleTracks.firstIndex(where: \.isDefault)
|
||||
selectSubtitle(change.isQuality ? previousSubtitleLanguage.flatMap { language in
|
||||
subtitleTracks.firstIndex { $0.language == language } ?? defaultIndex
|
||||
selectSubtitle(change.isQuality ? previousSubtitle.flatMap { previous in
|
||||
subtitleTracks.firstIndex { $0.language == previous.language } ?? defaultIndex
|
||||
} : defaultIndex)
|
||||
} else if case .youtube(let video, _) = source {
|
||||
subtitleTracks = video.subtitles
|
||||
// Off and a missing track both stay Off; never substitute manual/auto by language.
|
||||
if change.preservesPosition, let id = previousSubtitle?.selectionID {
|
||||
selectSubtitle(subtitleTracks.firstIndex { $0.selectionID == id })
|
||||
}
|
||||
} else {
|
||||
subtitleTracks = []
|
||||
}
|
||||
@@ -2987,6 +3005,7 @@ private struct CaptionSelector: NSViewRepresentable {
|
||||
required init?(coder: NSCoder) { nil }
|
||||
|
||||
func updateTracks() {
|
||||
isEnabled = model.canSelectSubtitle
|
||||
guard menu == nil || displayedSelection != model.selectedSubtitle ||
|
||||
displayedTracks.count != model.subtitleTracks.count ||
|
||||
zip(displayedTracks, model.subtitleTracks).contains(where: {
|
||||
@@ -3013,7 +3032,6 @@ private struct CaptionSelector: NSViewRepresentable {
|
||||
}
|
||||
menu = tracks
|
||||
(cell as? NSPopUpButtonCell)?.menuItem = captionItem
|
||||
isEnabled = !model.subtitleTracks.isEmpty
|
||||
}
|
||||
|
||||
@objc private func selectTrack(_ item: NSMenuItem) {
|
||||
@@ -3457,7 +3475,7 @@ private struct PlayerContent: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabled(model.subtitleTracks.isEmpty)
|
||||
.disabled(!model.canSelectSubtitle)
|
||||
Divider()
|
||||
}
|
||||
PlayerActionButton(model: model, action: model.bufferInfoAction,
|
||||
|
||||
Reference in New Issue
Block a user