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.
This commit is contained in:
Toni Förster
2024-04-26 12:27:25 +02:00
parent d1cf45c6a1
commit 54915dcea1
10 changed files with 191 additions and 151 deletions

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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")

View File

@@ -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()

View File

@@ -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() {

View File

@@ -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)
}
}

View File

@@ -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 {