From 54915dcea167a46b5d0d694af2335abdf7f9abe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20F=C3=B6rster?= Date: Fri, 26 Apr 2024 12:27:25 +0200 Subject: [PATCH 1/4] rework quality settings - The order of the formats can now be changed in the Quality Settings. - sortingOrder is now part of QualitiyProfile. - bestPlayable is now part of PlayerBackend. - hls and stream aren't treated as unknown anymore. --- Model/Player/Backends/AVPlayerBackend.swift | 10 -- Model/Player/Backends/MPVBackend.swift | 23 ---- Model/Player/Backends/PlayerBackend.swift | 33 ++++- Model/Player/PlayerModel.swift | 2 +- Model/Player/PlayerQueue.swift | 4 +- Model/QualityProfile.swift | 21 +-- Model/Stream.swift | 62 ++++----- Shared/Defaults.swift | 10 +- Shared/Settings/MultiselectRow.swift | 34 ++--- Shared/Settings/QualityProfileForm.swift | 143 +++++++++++++------- 10 files changed, 191 insertions(+), 151 deletions(-) diff --git a/Model/Player/Backends/AVPlayerBackend.swift b/Model/Player/Backends/AVPlayerBackend.swift index 7e71208f..af5dc8c3 100644 --- a/Model/Player/Backends/AVPlayerBackend.swift +++ b/Model/Player/Backends/AVPlayerBackend.swift @@ -116,16 +116,6 @@ final class AVPlayerBackend: PlayerBackend { #endif } - func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting) -> Stream? { - let sortedByResolution = streams - .filter { ($0.kind == .adaptive || $0.kind == .stream) && $0.resolution <= maxResolution.value } - .sorted { $0.resolution > $1.resolution } - - return streams.first { $0.kind == .hls } ?? - sortedByResolution.first { $0.kind == .stream } ?? - sortedByResolution.first - } - func canPlay(_ stream: Stream) -> Bool { stream.kind == .hls || stream.kind == .stream || (stream.kind == .adaptive && stream.format == .mp4) } diff --git a/Model/Player/Backends/MPVBackend.swift b/Model/Player/Backends/MPVBackend.swift index c8457946..5216e5ae 100644 --- a/Model/Player/Backends/MPVBackend.swift +++ b/Model/Player/Backends/MPVBackend.swift @@ -201,29 +201,6 @@ final class MPVBackend: PlayerBackend { typealias AreInIncreasingOrder = (Stream, Stream) -> Bool - func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting) -> Stream? { - streams - .filter { $0.kind != .hls && $0.resolution <= maxResolution.value } - .max { lhs, rhs in - let predicates: [AreInIncreasingOrder] = [ - { $0.resolution < $1.resolution }, - { $0.format > $1.format } - ] - - for predicate in predicates { - if !predicate(lhs, rhs), !predicate(rhs, lhs) { - continue - } - - return predicate(lhs, rhs) - } - - return false - } ?? - streams.first { $0.kind == .hls } ?? - streams.first - } - func canPlay(_ stream: Stream) -> Bool { stream.resolution != .unknown && stream.format != .av1 } diff --git a/Model/Player/Backends/PlayerBackend.swift b/Model/Player/Backends/PlayerBackend.swift index 3b09e957..88890ffb 100644 --- a/Model/Player/Backends/PlayerBackend.swift +++ b/Model/Player/Backends/PlayerBackend.swift @@ -29,7 +29,6 @@ protocol PlayerBackend { var videoWidth: Double? { get } var videoHeight: Double? { get } - func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting) -> Stream? func canPlay(_ stream: Stream) -> Bool func canPlayAtRate(_ rate: Double) -> Bool @@ -131,6 +130,38 @@ extension PlayerBackend { } } + func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting, formatOrder: [QualityProfile.Format]) -> Stream? { + return streams.map { stream in + if stream.kind == .hls { + stream.resolution = maxResolution.value + stream.format = .hls + } else if stream.kind == .stream { + stream.format = .stream + } + return stream + } + .filter { stream in + stream.resolution <= maxResolution.value + } + .max { lhs, rhs in + if lhs.resolution == rhs.resolution { + guard let lhsFormat = QualityProfile.Format(rawValue: lhs.format.rawValue), + let rhsFormat = QualityProfile.Format(rawValue: rhs.format.rawValue) + else { + print("Failed to extract lhsFormat or rhsFormat") + return false + } + + let lhsFormatIndex = formatOrder.firstIndex(of: lhsFormat) ?? Int.max + let rhsFormatIndex = formatOrder.firstIndex(of: rhsFormat) ?? Int.max + + return lhsFormatIndex > rhsFormatIndex + } + + return lhs.resolution < rhs.resolution + } + } + func updateControls(completionHandler: (() -> Void)? = nil) { print("updating controls") diff --git a/Model/Player/PlayerModel.swift b/Model/Player/PlayerModel.swift index 6f3aff7a..7816ea7a 100644 --- a/Model/Player/PlayerModel.swift +++ b/Model/Player/PlayerModel.swift @@ -673,7 +673,7 @@ final class PlayerModel: ObservableObject { } guard let video = currentVideo else { return } - guard let stream = avPlayerBackend.bestPlayable(availableStreams, maxResolution: .hd720p30) else { return } + guard let stream = backend.bestPlayable(availableStreams, maxResolution: .hd720p30, formatOrder: qualityProfile!.formats) else { return } exitFullScreen() diff --git a/Model/Player/PlayerQueue.swift b/Model/Player/PlayerQueue.swift index c2f8256e..cbec911d 100644 --- a/Model/Player/PlayerQueue.swift +++ b/Model/Player/PlayerQueue.swift @@ -127,12 +127,12 @@ extension PlayerModel { if let streamPreferredForProfile = backend.bestPlayable( availableStreams.filter { backend.canPlay($0) && profile.isPreferred($0) }, - maxResolution: profile.resolution + maxResolution: profile.resolution, formatOrder: profile.formats ) { return streamPreferredForProfile } - return backend.bestPlayable(availableStreams.filter { backend.canPlay($0) }, maxResolution: profile.resolution) + return backend.bestPlayable(availableStreams.filter { backend.canPlay($0) }, maxResolution: profile.resolution, formatOrder: profile.formats) } func advanceToNextItem() { diff --git a/Model/QualityProfile.swift b/Model/QualityProfile.swift index 28775eb0..6906d581 100644 --- a/Model/QualityProfile.swift +++ b/Model/QualityProfile.swift @@ -3,13 +3,13 @@ import Foundation struct QualityProfile: Hashable, Identifiable, Defaults.Serializable { static var bridge = QualityProfileBridge() - static var defaultProfile = Self(id: "default", backend: .mpv, resolution: .hd720p60, formats: [.stream]) + static var defaultProfile = Self(id: "default", backend: .mpv, resolution: .hd720p60, formats: [.stream], order: Array(Format.allCases.indices)) enum Format: String, CaseIterable, Identifiable, Defaults.Serializable { case hls case stream - case mp4 case avc1 + case mp4 case av1 case webm @@ -23,7 +23,6 @@ struct QualityProfile: Hashable, Identifiable, Defaults.Serializable { return "Stream" case .webm: return "WebM" - default: return rawValue.uppercased() } @@ -35,14 +34,14 @@ struct QualityProfile: Hashable, Identifiable, Defaults.Serializable { return nil case .stream: return nil - case .mp4: - return .mp4 - case .webm: - return .webm case .avc1: return .avc1 + case .mp4: + return .mp4 case .av1: return .av1 + case .webm: + return .webm } } } @@ -53,7 +52,7 @@ struct QualityProfile: Hashable, Identifiable, Defaults.Serializable { var backend: PlayerBackendType var resolution: ResolutionSetting var formats: [Format] - + var order: [Int] var description: String { if let name, !name.isEmpty { return name } return "\(backend.label) - \(resolution.description) - \(formatsDescription)" @@ -101,7 +100,8 @@ struct QualityProfileBridge: Defaults.Bridge { "name": value.name ?? "", "backend": value.backend.rawValue, "resolution": value.resolution.rawValue, - "formats": value.formats.map { $0.rawValue }.joined(separator: Self.formatsSeparator) + "formats": value.formats.map { $0.rawValue }.joined(separator: Self.formatsSeparator), + "order": value.order.map { String($0) }.joined(separator: Self.formatsSeparator) // New line ] } @@ -116,7 +116,8 @@ struct QualityProfileBridge: Defaults.Bridge { let name = object["name"] let formats = (object["formats"] ?? "").components(separatedBy: Self.formatsSeparator).compactMap { QualityProfile.Format(rawValue: $0) } + let order = (object["order"] ?? "").components(separatedBy: Self.formatsSeparator).compactMap { Int($0) } - return .init(id: id, name: name, backend: backend, resolution: resolution, formats: formats) + return .init(id: id, name: name, backend: backend, resolution: resolution, formats: formats, order: order) } } diff --git a/Model/Stream.swift b/Model/Stream.swift index 4f58e637..04536f16 100644 --- a/Model/Stream.swift +++ b/Model/Stream.swift @@ -64,7 +64,7 @@ class Stream: Equatable, Hashable, Identifiable { } enum Kind: String, Comparable { - case stream, adaptive, hls + case hls, adaptive, stream private var sortOrder: Int { switch self { @@ -82,37 +82,23 @@ class Stream: Equatable, Hashable, Identifiable { } } - enum Format: String, Comparable { - case webm + enum Format: String { case avc1 - case av1 case mp4 + case av1 + case webm + case hls + case stream case unknown - private var sortOrder: Int { - switch self { - case .mp4: - return 0 - case .avc1: - return 1 - case .av1: - return 2 - case .webm: - return 3 - case .unknown: - return 4 - } - } - - static func < (lhs: Self, rhs: Self) -> Bool { - lhs.sortOrder < rhs.sortOrder - } - var description: String { switch self { case .webm: return "WebM" - + case .hls: + return "adaptive (HLS)" + case .stream: + return "Stream" default: return rawValue.uppercased() } @@ -121,17 +107,23 @@ class Stream: Equatable, Hashable, Identifiable { static func from(_ string: String) -> Self { let lowercased = string.lowercased() - if lowercased.contains("webm") { - return .webm - } if lowercased.contains("avc1") { return .avc1 } + if lowercased.contains("mpeg_4") || lowercased.contains("mp4") { + return .mp4 + } if lowercased.contains("av01") { return .av1 } - if lowercased.contains("mpeg_4") || lowercased.contains("mp4") { - return .mp4 + if lowercased.contains("webm") { + return .webm + } + if lowercased.contains("stream") { + return .stream + } + if lowercased.contains("hls") { + return .hls } return .unknown } @@ -184,22 +176,26 @@ class Stream: Equatable, Hashable, Identifiable { var quality: String { guard localURL.isNil else { return "Opened File" } - return kind == .hls ? "adaptive (HLS)" : "\(resolution.name)\(kind == .stream ? " (\(kind.rawValue))" : "")" + return resolution.name } var shortQuality: String { guard localURL.isNil else { return "File" } if kind == .hls { - return "HLS" + return format.description } - return resolution?.name ?? "?" + + if kind == .stream { + return resolution.name + } + return resolutionAndFormat } var description: String { guard localURL.isNil else { return resolutionAndFormat } let instanceString = instance.isNil ? "" : " - (\(instance!.description))" - return "\(resolutionAndFormat)\(instanceString)" + return format != .hls ? "\(resolutionAndFormat)\(instanceString)" : "\(format.description)\(instanceString)" } var resolutionAndFormat: String { diff --git a/Shared/Defaults.swift b/Shared/Defaults.swift index 7df4c47d..fb216b34 100644 --- a/Shared/Defaults.swift +++ b/Shared/Defaults.swift @@ -165,11 +165,11 @@ extension Defaults.Keys { // MARK: GROUP - Quality - static let hd2160pMPVProfile = QualityProfile(id: "hd2160pMPVProfile", backend: .mpv, resolution: .hd2160p60, formats: QualityProfile.Format.allCases) - static let hd1080pMPVProfile = QualityProfile(id: "hd1080pMPVProfile", backend: .mpv, resolution: .hd1080p60, formats: QualityProfile.Format.allCases) - static let hd720pMPVProfile = QualityProfile(id: "hd720pMPVProfile", backend: .mpv, resolution: .hd720p60, formats: QualityProfile.Format.allCases) - static let hd720pAVPlayerProfile = QualityProfile(id: "hd720pAVPlayerProfile", backend: .appleAVPlayer, resolution: .hd720p60, formats: [.hls, .stream]) - static let sd360pAVPlayerProfile = QualityProfile(id: "sd360pAVPlayerProfile", backend: .appleAVPlayer, resolution: .sd360p30, formats: [.hls, .stream]) + static let hd2160pMPVProfile = QualityProfile(id: "hd2160pMPVProfile", backend: .mpv, resolution: .hd2160p60, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices)) + static let hd1080pMPVProfile = QualityProfile(id: "hd1080pMPVProfile", backend: .mpv, resolution: .hd1080p60, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices)) + static let hd720pMPVProfile = QualityProfile(id: "hd720pMPVProfile", backend: .mpv, resolution: .hd720p60, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices)) + static let hd720pAVPlayerProfile = QualityProfile(id: "hd720pAVPlayerProfile", backend: .appleAVPlayer, resolution: .hd720p60, formats: [.hls, .stream], order: Array(QualityProfile.Format.allCases.indices)) + static let sd360pAVPlayerProfile = QualityProfile(id: "sd360pAVPlayerProfile", backend: .appleAVPlayer, resolution: .sd360p30, formats: [.hls, .stream], order: Array(QualityProfile.Format.allCases.indices)) #if os(iOS) static let qualityProfilesDefault = UIDevice.current.userInterfaceIdiom == .pad ? [ diff --git a/Shared/Settings/MultiselectRow.swift b/Shared/Settings/MultiselectRow.swift index 826868e0..24c1a05d 100644 --- a/Shared/Settings/MultiselectRow.swift +++ b/Shared/Settings/MultiselectRow.swift @@ -9,9 +9,23 @@ struct MultiselectRow: View { @State private var toggleChecked = false var body: some View { - #if os(macOS) + #if os(tvOS) + Button(action: { action(!selected) }) { + HStack { + Text(self.title) + Spacer() + if selected { + Image(systemName: "checkmark") + } + } + .contentShape(Rectangle()) + } + .disabled(disabled) + #else Toggle(title, isOn: $toggleChecked) + #if os(macOS) .toggleStyle(.checkbox) + #endif .onAppear { guard !disabled else { return } toggleChecked = selected @@ -19,24 +33,6 @@ struct MultiselectRow: View { .onChange(of: toggleChecked) { new in action(new) } - #else - Button(action: { action(!selected) }) { - HStack { - Text(self.title) - Spacer() - if selected { - Image(systemName: "checkmark") - #if os(iOS) - .foregroundColor(.accentColor) - #endif - } - } - .contentShape(Rectangle()) - } - .disabled(disabled) - #if !os(tvOS) - .buttonStyle(.plain) - #endif #endif } } diff --git a/Shared/Settings/QualityProfileForm.swift b/Shared/Settings/QualityProfileForm.swift index daade631..18a3fe48 100644 --- a/Shared/Settings/QualityProfileForm.swift +++ b/Shared/Settings/QualityProfileForm.swift @@ -1,6 +1,11 @@ import Defaults import SwiftUI +struct FormatState: Equatable { + let format: QualityProfile.Format + var isActive: Bool +} + struct QualityProfileForm: View { @Binding var qualityProfileID: QualityProfile.ID? @@ -15,6 +20,7 @@ struct QualityProfileForm: View { @State private var backend = PlayerBackendType.mpv @State private var resolution = ResolutionSetting.hd1080p60 @State private var formats = [QualityProfile.Format]() + @State private var orderedFormats: [FormatState] = [] @Default(.qualityProfiles) private var qualityProfiles @@ -26,6 +32,7 @@ struct QualityProfileForm: View { return nil } + // swiftlint:disable trailing_closure var body: some View { VStack { Group { @@ -40,8 +47,11 @@ struct QualityProfileForm: View { #endif .onAppear(perform: initializeForm) - .onChange(of: backend, perform: backendChanged) - .onChange(of: formats) { _ in validate() } + .onChange(of: backend, perform: { _ in backendChanged(self.backend); updateActiveFormats(); validate() }) + + .onChange(of: name, perform: { _ in validate() }) + .onChange(of: resolution, perform: { _ in validate() }) + .onChange(of: orderedFormats, perform: { _ in validate() }) #if os(iOS) .padding(.vertical) #elseif os(tvOS) @@ -53,6 +63,8 @@ struct QualityProfileForm: View { #endif } + // swiftlint:enable trailing_closure + var header: some View { HStack { Text(editing ? "Edit Quality Profile" : "Add Quality Profile") @@ -124,9 +136,16 @@ struct QualityProfileForm: View { } var formatsFooter: some View { - Text("Formats will be selected in order as listed.\nHLS is an adaptive format (resolution setting does not apply).") - .foregroundColor(.secondary) - .fixedSize(horizontal: false, vertical: true) + VStack(alignment: .leading) { + Text("Formats can be reordered and will be selected in this order.") + .foregroundColor(.secondary) + .fixedSize(horizontal: false, vertical: true) + Text("**Note:** HLS is an adaptive format, resolution setting doesn't apply.") + .foregroundColor(.secondary) + .fixedSize(horizontal: false, vertical: true) + .padding(.top, 0.1) + } + .padding(.top, 2) } @ViewBuilder var qualityPicker: some View { @@ -199,17 +218,25 @@ struct QualityProfileForm: View { #endif } + var filteredFormatList: some View { + ForEach(Array(orderedFormats.enumerated()), id: \.element.format) { idx, element in + let format = element.format + MultiselectRow( + title: format.description, + selected: element.isActive + ) { value in + orderedFormats[idx].isActive = value + } + } + .onMove { source, destination in + orderedFormats.move(fromOffsets: source, toOffset: destination) + validate() + } + } + @ViewBuilder var formatsPicker: some View { #if os(macOS) - let list = ForEach(QualityProfile.Format.allCases, id: \.self) { format in - MultiselectRow( - title: format.description, - selected: isFormatSelected(format), - disabled: isFormatDisabled(format) - ) { value in - toggleFormat(format, value: value) - } - } + let list = filteredFormatList Group { if #available(macOS 12.0, *) { @@ -222,28 +249,19 @@ struct QualityProfileForm: View { } Spacer() #else - ForEach(QualityProfile.Format.allCases, id: \.self) { format in - MultiselectRow( - title: format.description, - selected: isFormatSelected(format), - disabled: isFormatDisabled(format) - ) { value in - toggleFormat(format, value: value) - } - } + filteredFormatList #endif } func isFormatSelected(_ format: QualityProfile.Format) -> Bool { - (initialized || qualityProfile.isNil ? formats : qualityProfile.formats).contains(format) + return orderedFormats.first { $0.format == format }?.isActive ?? false } func toggleFormat(_ format: QualityProfile.Format, value: Bool) { - if let index = formats.firstIndex(where: { $0 == format }), !value { - formats.remove(at: index) - } else if value { - formats.append(format) + if let index = orderedFormats.firstIndex(where: { $0.format == format }) { + orderedFormats[index].isActive = value } + validate() // Check validity after a toggle operation } var footer: some View { @@ -274,6 +292,12 @@ struct QualityProfileForm: View { return !avPlayerFormats.contains(format) } + func updateActiveFormats() { + for (index, format) in orderedFormats.enumerated() where isFormatDisabled(format.format) { + orderedFormats[index].isActive = false + } + } + func isResolutionDisabled(_ resolution: ResolutionSetting) -> Bool { guard backend == .appleAVPlayer else { return false } @@ -281,27 +305,39 @@ struct QualityProfileForm: View { } func initializeForm() { - guard editing else { - validate() - return + if editing { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self.name = qualityProfile.name ?? "" + self.backend = qualityProfile.backend + self.resolution = qualityProfile.resolution + self.orderedFormats = qualityProfile.order.map { order in + let format = QualityProfile.Format.allCases[order] + let isActive = qualityProfile.formats.contains(format) + return FormatState(format: format, isActive: isActive) + } + self.initialized = true + } + } else { + name = "" + backend = .mpv + resolution = .hd720p60 + orderedFormats = QualityProfile.Format.allCases.map { + FormatState(format: $0, isActive: true) + } + initialized = true } - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - self.name = qualityProfile.name ?? "" - self.backend = qualityProfile.backend - self.resolution = qualityProfile.resolution - self.formats = .init(qualityProfile.formats) - self.initialized = true - } - validate() } func backendChanged(_: PlayerBackendType) { - formats.filter { isFormatDisabled($0) }.forEach { format in - if let index = formats.firstIndex(where: { $0 == format }) { - formats.remove(at: index) - } + let defaultFormats = QualityProfile.Format.allCases.map { + FormatState(format: $0, isActive: true) + } + + if backend == .appleAVPlayer { + orderedFormats = orderedFormats.filter { !isFormatDisabled($0.format) } + } else { + orderedFormats = defaultFormats } if isResolutionDisabled(resolution), @@ -312,20 +348,33 @@ struct QualityProfileForm: View { } func validate() { - valid = !formats.isEmpty + if !initialized { + valid = false + } else if editing { + let savedOrderFormats = qualityProfile.order.map { order in + let format = QualityProfile.Format.allCases[order] + let isActive = qualityProfile.formats.contains(format) + return FormatState(format: format, isActive: isActive) + } + valid = name != qualityProfile.name + || backend != qualityProfile.backend + || resolution != qualityProfile.resolution + || orderedFormats != savedOrderFormats + } else { valid = true } } func submitForm() { guard valid else { return } - formats = formats.unique() + let activeFormats = orderedFormats.filter { $0.isActive }.map { $0.format } let formProfile = QualityProfile( id: qualityProfile?.id ?? UUID().uuidString, name: name, backend: backend, resolution: resolution, - formats: Array(formats) + formats: activeFormats, + order: orderedFormats.map { QualityProfile.Format.allCases.firstIndex(of: $0.format)! } ) if editing { From ef7a486fd4cb190e56081fae9b36216cec5e18d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20F=C3=B6rster?= Date: Wed, 1 May 2024 14:30:19 +0200 Subject: [PATCH 2/4] add migration for old profiles to new format --- Shared/YatteeApp.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Shared/YatteeApp.swift b/Shared/YatteeApp.swift index 110d638e..b0097a05 100644 --- a/Shared/YatteeApp.swift +++ b/Shared/YatteeApp.swift @@ -204,6 +204,7 @@ struct YatteeApp: App { URLBookmarkModel.shared.refreshAll() migrateHomeHistoryItems() + migrateQualityProfiles() } func migrateHomeHistoryItems() { @@ -221,6 +222,16 @@ struct YatteeApp: App { Defaults[.homeHistoryItems] = -1 } + @Default(.qualityProfiles) private var qualityProfilesData + + func migrateQualityProfiles() { + for profile in qualityProfilesData where profile.order.isEmpty { + var updatedProfile = profile + updatedProfile.order = Array(QualityProfile.Format.allCases.indices) + QualityProfilesModel.shared.update(profile, updatedProfile) + } + } + var navigationStyle: NavigationStyle { #if os(iOS) return horizontalSizeClass == .compact ? .tab : .sidebar From d8c8f8084b4d2201fdd274a2abbd4b924e69f719 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20F=C3=B6rster?= Date: Thu, 9 May 2024 21:15:14 +0200 Subject: [PATCH 3/4] fix a crash when format is hls --- Model/Stream.swift | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Model/Stream.swift b/Model/Stream.swift index 04536f16..65e58993 100644 --- a/Model/Stream.swift +++ b/Model/Stream.swift @@ -176,6 +176,11 @@ class Stream: Equatable, Hashable, Identifiable { var quality: String { guard localURL.isNil else { return "Opened File" } + + if kind == .hls { + return "adaptive (HLS)" + } + return resolution.name } @@ -183,7 +188,7 @@ class Stream: Equatable, Hashable, Identifiable { guard localURL.isNil else { return "File" } if kind == .hls { - return format.description + return "adaptive (HLS)" } if kind == .stream { @@ -195,7 +200,7 @@ class Stream: Equatable, Hashable, Identifiable { var description: String { guard localURL.isNil else { return resolutionAndFormat } let instanceString = instance.isNil ? "" : " - (\(instance!.description))" - return format != .hls ? "\(resolutionAndFormat)\(instanceString)" : "\(format.description)\(instanceString)" + return format != .hls ? "\(resolutionAndFormat)\(instanceString)" : "adaptive (HLS)\(instanceString)" } var resolutionAndFormat: String { From fba01e35a341dd775e8e931634b86ba8655c88c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20F=C3=B6rster?= Date: Sat, 11 May 2024 14:08:40 +0200 Subject: [PATCH 4/4] apply best resolution from non HLS to HLS stream This makes sure that HLS Streams can be compared with non HLS streams, otherwise HLS would have been the first selected since the maxResolution.value might have been higher then what could actually be the highest resolution served. --- Model/Player/Backends/PlayerBackend.swift | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Model/Player/Backends/PlayerBackend.swift b/Model/Player/Backends/PlayerBackend.swift index 88890ffb..fce56900 100644 --- a/Model/Player/Backends/PlayerBackend.swift +++ b/Model/Player/Backends/PlayerBackend.swift @@ -131,9 +131,17 @@ extension PlayerBackend { } func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting, formatOrder: [QualityProfile.Format]) -> Stream? { + // filter out non HLS streams + let nonHLSStreams = streams.filter { $0.kind != .hls } + + // find max resolution from non HLS streams + let bestResolution = nonHLSStreams + .filter { $0.resolution <= maxResolution.value } + .max { $0.resolution < $1.resolution }?.resolution + return streams.map { stream in if stream.kind == .hls { - stream.resolution = maxResolution.value + stream.resolution = bestResolution ?? maxResolution.value stream.format = .hls } else if stream.kind == .stream { stream.format = .stream