feat: choose YouTube quality with a 1080p default

This commit is contained in:
2026-10-11 02:38:06 +02:00
parent 5f0287f489
commit 4d4c3e6b0a
4 changed files with 193 additions and 21 deletions
+71 -11
View File
@@ -12,6 +12,9 @@ final class PlayerModel: ObservableObject {
@Published private(set) var pendingRevision: UInt64?
@Published private(set) var resolvingYouTube = false
@Published private(set) var isLiveStream = false
@Published private(set) var youtubeHeights: [Int] = []
@Published private(set) var youtubeHeight: Int?
@Published private(set) var youtubeQuality: Int?
@Published var seekPosition = 0.0
@Published var isSeeking = false
@Published private(set) var volume = 1.0
@@ -47,6 +50,7 @@ final class PlayerModel: ObservableObject {
private var playbackProgress: PlaybackProgress
private var progressURL: URL?
private var resumePosition: Double?
private var resumePlaying = true
private var lastSavedPosition: Double?
private var systemPlayback: SystemPlaybackControls?
@@ -76,6 +80,9 @@ final class PlayerModel: ObservableObject {
}
var canSeek: Bool { canTogglePlayback && state.can_seek && state.duration.isFinite && state.duration > 0 }
var canStepFrame: Bool { canTogglePlayback && state.has_video && state.can_seek && !isSeeking }
var canChangeYouTubeQuality: Bool {
canTogglePlayback && !youtubeHeights.isEmpty && !isSeeking && !seekPending && !state.seek_pending
}
var canOpen: Bool { handle != nil }
var canClose: Bool { handle != nil && (isBusy || state.status != PLAYER_EMPTY || fileName != nil) }
var canSetVolume: Bool { handle != nil && !isBusy && state.has_audio && state.status != PLAYER_ERROR }
@@ -122,8 +129,7 @@ final class PlayerModel: ObservableObject {
}
func openYouTube(_ text: String) {
guard let handle else { return }
cancelYouTubeResolution()
guard canOpen else { return }
let url: URL
do {
url = try YouTubeResolver.validateURL(text)
@@ -132,6 +138,25 @@ final class PlayerModel: ObservableObject {
revealControls()
return
}
resolveYouTube(url, height: nil)
}
func setYouTubeQuality(_ height: Int?) {
guard canChangeYouTubeQuality, let url = progressURL, let handle,
height != youtubeQuality, height == nil || youtubeHeights.contains(height!) else { return }
let target = height ?? youtubeHeights.filter { $0 <= 1080 }.max()
if target == youtubeHeight {
youtubeQuality = height
return
}
let current = player_get_state(handle)
let position = !isLiveStream && current.can_seek ? current.position : nil
resolveYouTube(url, height: height, restoring: (position, current.status == PLAYER_PLAYING))
}
private func resolveYouTube(_ url: URL, height: Int?, restoring: (position: Double?, playing: Bool)? = nil) {
guard let handle else { return }
cancelYouTubeResolution()
if !player_pause(handle) {
commandError = "Could not pause the current media to open YouTube. Please try again."
revealControls()
@@ -145,18 +170,21 @@ final class PlayerModel: ObservableObject {
revealControls()
youtubeTask = Task { [weak self] in
do {
let video = try await YouTubeResolver.resolve(url)
let video = try await YouTubeResolver.resolve(url, height: height)
guard !Task.isCancelled, let self, self.youtubeRequest == request, self.handle != nil else { return }
self.youtubeTask = nil
self.youtubeRequest = nil
self.resolvingYouTube = false
self.openResolvedYouTube(video)
self.openResolvedYouTube(video, height: height, restoring: restoring)
} catch {
guard !Task.isCancelled, let self, self.youtubeRequest == request else { return }
self.youtubeTask = nil
self.youtubeRequest = nil
self.resolvingYouTube = false
self.commandError = error.localizedDescription
if restoring?.playing == true, let handle = self.handle, !player_play(handle) {
self.commandError = error.localizedDescription + "\nCould not resume the previous playback."
}
self.revealControls()
}
}
@@ -169,7 +197,7 @@ final class PlayerModel: ObservableObject {
resolvingYouTube = false
}
private func openResolvedYouTube(_ video: YouTubeVideo) {
private func openResolvedYouTube(_ video: YouTubeVideo, height: Int?, restoring: (position: Double?, playing: Bool)?) {
guard let handle else { return }
saveProgress()
let revision = player_get_state(handle).revision
@@ -191,23 +219,39 @@ final class PlayerModel: ObservableObject {
}
guard accepted else {
commandError = "Could not queue the YouTube video to open. Please try again."
if restoring?.playing == true, !player_play(handle) {
commandError = "Could not queue the YouTube video or resume the previous playback. Please try again."
}
revealControls()
return
}
didAcceptOpen(revision: revision, title: video.title, icon: nil, url: video.webpageURL, live: video.isLive)
didAcceptOpen(revision: revision, title: video.title, icon: nil, url: video.webpageURL,
live: video.isLive, restoring: restoring)
youtubeHeights = video.availableHeights
youtubeHeight = video.height
youtubeQuality = height
}
private func didAcceptOpen(revision: UInt64, title: String, icon: NSImage?, url: URL, live: Bool = false) {
private func didAcceptOpen(revision: UInt64, title: String, icon: NSImage?, url: URL, live: Bool = false,
restoring: (position: Double?, playing: Bool)? = nil) {
guard let handle else { return }
pendingRevision = revision
fileName = title
fileIcon = icon
isLiveStream = live
youtubeHeights = []
youtubeHeight = nil
youtubeQuality = nil
progressURL = url
resumePosition = live ? nil : playbackProgress.position(for: url)
if let restoring {
resumePosition = restoring.position
} else {
resumePosition = live ? nil : playbackProgress.position(for: url)
}
resumePlaying = restoring?.playing ?? true
lastSavedPosition = nil
needsVideoFit = true
commandError = resumePosition != nil || player_play(handle) ? nil :
needsVideoFit = restoring == nil
commandError = resumePosition != nil || !resumePlaying || player_play(handle) ? nil :
"Could not queue playback. Please try again."
isSeeking = false
seekPending = false
@@ -274,6 +318,9 @@ final class PlayerModel: ObservableObject {
fileName = nil
fileIcon = nil
isLiveStream = false
youtubeHeights = []
youtubeHeight = nil
youtubeQuality = nil
progressURL = nil
resumePosition = nil
lastSavedPosition = nil
@@ -503,7 +550,7 @@ final class PlayerModel: ObservableObject {
playbackProgress.save(nil, for: url)
}
}
commandError = player_play(handle) ? nil : "Could not queue playback. Please try again."
commandError = !resumePlaying || player_play(handle) ? nil : "Could not queue playback. Please try again."
}
}
@@ -733,6 +780,19 @@ private struct PlayerContent: View {
Button("Open File…", action: model.openPanel).disabled(!model.canOpen)
Button("Open YouTube URL…", action: model.openYouTubePanel).disabled(!model.canOpen)
Button("Close File", action: model.closeFile).disabled(!model.canClose)
if !model.youtubeHeights.isEmpty {
Divider()
Picker("Quality", selection: Binding(
get: { model.youtubeQuality ?? 0 },
set: { model.setYouTubeQuality($0 == 0 ? nil : $0) })) {
Text("Default (up to 1080p)").tag(0)
ForEach(model.youtubeHeights, id: \.self) { height in
Text("\(height)p").tag(height)
}
}
.disabled(!model.canChangeYouTubeQuality)
Text(model.youtubeHeight.map { "Current: \($0)p" } ?? "Current resolution unknown")
}
Divider()
Button(model.isCompact ? "Exit Compact Mode" : "Enter Compact Mode", action: model.toggleCompact)
.disabled(!model.canToggleCompact)