Merge pull request #807 from yattee/more-robust-resolution-handling

more robust resolution handling
This commit is contained in:
Arkadiusz Fal 2024-09-11 09:29:00 +02:00 committed by GitHub
commit 4202b27c03
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 222 additions and 273 deletions

View File

@ -515,7 +515,8 @@ final class PeerTubeAPI: Service, ObservableObject, VideosAPI {
.dictionaryValue["files"]?.arrayValue.first? .dictionaryValue["files"]?.arrayValue.first?
.dictionaryValue["fileUrl"]?.url .dictionaryValue["fileUrl"]?.url
{ {
streams.append(SingleAssetStream(instance: account.instance, avAsset: AVURLAsset(url: fileURL), resolution: .hd720p30, kind: .stream)) let resolution = Stream.Resolution.predefined(.hd720p30)
streams.append(SingleAssetStream(instance: account.instance, avAsset: AVURLAsset(url: fileURL), resolution: resolution, kind: .stream))
} }
return streams return streams

View File

@ -204,7 +204,7 @@ final class MPVBackend: PlayerBackend {
typealias AreInIncreasingOrder = (Stream, Stream) -> Bool typealias AreInIncreasingOrder = (Stream, Stream) -> Bool
func canPlay(_ stream: Stream) -> Bool { func canPlay(_ stream: Stream) -> Bool {
stream.resolution != .unknown && stream.format != .av1 stream.format != .av1
} }
func playStream(_ stream: Stream, of video: Video, preservingTime: Bool, upgrading: Bool) { func playStream(_ stream: Stream, of video: Video, preservingTime: Bool, upgrading: Bool) {

View File

@ -153,8 +153,9 @@ extension PlayerBackend {
// Filter out non-HLS streams and streams with resolution more than maxResolution // Filter out non-HLS streams and streams with resolution more than maxResolution
let nonHLSStreams = streams.filter { let nonHLSStreams = streams.filter {
let isHLS = $0.kind == .hls let isHLS = $0.kind == .hls
// Safely unwrap resolution and maxResolution.value to avoid crashes // Check if the stream's resolution is within the maximum allowed resolution
let isWithinResolution = ($0.resolution != nil && maxResolution.value != nil) ? $0.resolution! <= maxResolution.value! : false let isWithinResolution = $0.resolution.map { $0 <= maxResolution.value } ?? false
logger.info("Stream ID: \($0.id) - Kind: \(String(describing: $0.kind)) - Resolution: \(String(describing: $0.resolution)) - Bitrate: \($0.bitrate ?? 0)") logger.info("Stream ID: \($0.id) - Kind: \(String(describing: $0.kind)) - Resolution: \(String(describing: $0.resolution)) - Bitrate: \($0.bitrate ?? 0)")
logger.info("Is HLS: \(isHLS), Is within resolution: \(isWithinResolution)") logger.info("Is HLS: \(isHLS), Is within resolution: \(isWithinResolution)")
return !isHLS && isWithinResolution return !isHLS && isWithinResolution
@ -188,8 +189,8 @@ extension PlayerBackend {
} }
let filteredStreams = adjustedStreams.filter { stream in let filteredStreams = adjustedStreams.filter { stream in
// Safely unwrap resolution and maxResolution.value to avoid crashes // Check if the stream's resolution is within the maximum allowed resolution
let isWithinResolution = (stream.resolution != nil && maxResolution.value != nil) ? stream.resolution! <= maxResolution.value! : false let isWithinResolution = stream.resolution <= maxResolution.value
logger.info("Filtered stream ID: \(stream.id) - Is within max resolution: \(isWithinResolution)") logger.info("Filtered stream ID: \(stream.id) - Is within max resolution: \(isWithinResolution)")
return isWithinResolution return isWithinResolution
} }

View File

@ -76,7 +76,8 @@ struct QualityProfile: Hashable, Identifiable, Defaults.Serializable {
return true return true
} }
let resolutionMatch = !stream.resolution.isNil && resolution.value >= stream.resolution let defaultResolution = Stream.Resolution.custom(height: 720, refreshRate: 30)
let resolutionMatch = resolution.value ?? defaultResolution >= stream.resolution
if resolutionMatch, formats.contains(.stream), stream.kind == .stream { if resolutionMatch, formats.contains(.stream), stream.kind == .stream {
return true return true

View File

@ -4,292 +4,126 @@ import Foundation
// swiftlint:disable:next final_class // swiftlint:disable:next final_class
class Stream: Equatable, Hashable, Identifiable { class Stream: Equatable, Hashable, Identifiable {
enum Resolution: String, CaseIterable, Comparable, Defaults.Serializable { enum Resolution: Comparable, Codable, Defaults.Serializable {
// Some 16:19 and 16:10 resolutions are also used in 2:1 videos case predefined(PredefinedResolution)
case custom(height: Int, refreshRate: Int)
// 8K UHD (16:9) Resolutions enum PredefinedResolution: String, CaseIterable, Codable {
case hd4320p60 // 8K UHD (16:9) Resolutions
case hd4320p50 case hd4320p60, hd4320p30
case hd4320p48
case hd4320p30
case hd4320p25
case hd4320p24
// 5K (16:9) Resolutions // 4K UHD (16:9) Resolutions
case hd2560p60 case hd2160p60, hd2160p30
case hd2560p50
case hd2560p48
case hd2560p30
case hd2560p25
case hd2560p24
// 2:1 Aspect Ratio (Univisium) Resolutions // 1440p (16:9) Resolutions
case hd2880p60 case hd1440p60, hd1440p30
case hd2880p50
case hd2880p48
case hd2880p30
case hd2880p25
case hd2880p24
// 16:10 Resolutions // 1080p (Full HD, 16:9) Resolutions
case hd2400p60 case hd1080p60, hd1080p30
case hd2400p50
case hd2400p48
case hd2400p30
case hd2400p25
case hd2400p24
// 16:9 Resolutions // 720p (HD, 16:9) Resolutions
case hd2160p60 case hd720p60, hd720p30
case hd2160p50
case hd2160p48
case hd2160p30
case hd2160p25
case hd2160p24
// 16:10 Resolutions // Standard Definition (SD) Resolutions
case hd1600p60 case sd480p30
case hd1600p50 case sd360p30
case hd1600p48 case sd240p30
case hd1600p30 case sd144p30
case hd1600p25 }
case hd1600p24
// 16:9 Resolutions
case hd1440p60
case hd1440p50
case hd1440p48
case hd1440p30
case hd1440p25
case hd1440p24
// 16:10 Resolutions
case hd1280p60
case hd1280p50
case hd1280p48
case hd1280p30
case hd1280p25
case hd1280p24
// 16:10 Resolutions
case hd1200p60
case hd1200p50
case hd1200p48
case hd1200p30
case hd1200p25
case hd1200p24
// 16:9 Resolutions
case hd1080p60
case hd1080p50
case hd1080p48
case hd1080p30
case hd1080p25
case hd1080p24
// 16:10 Resolutions
case hd1050p60
case hd1050p50
case hd1050p48
case hd1050p30
case hd1050p25
case hd1050p24
// 16:9 Resolutions
case hd960p60
case hd960p50
case hd960p48
case hd960p30
case hd960p25
case hd960p24
// 16:10 Resolutions
case hd900p60
case hd900p50
case hd900p48
case hd900p30
case hd900p25
case hd900p24
// 16:10 Resolutions
case hd800p60
case hd800p50
case hd800p48
case hd800p30
case hd800p25
case hd800p24
// 16:9 Resolutions
case hd720p60
case hd720p50
case hd720p48
case hd720p30
case hd720p25
case hd720p24
// Standard Definition (SD) Resolutions
case sd854p30
case sd854p25
case sd768p30
case sd768p25
case sd640p30
case sd640p25
case sd480p30
case sd480p25
case sd428p30
case sd428p25
case sd426p30
case sd426p25
case sd360p30
case sd360p25
case sd320p30
case sd320p25
case sd256p30
case sd256p25
case sd240p30
case sd240p25
case sd214p30
case sd214p25
case sd144p30
case sd144p25
case sd128p30
case sd128p25
case unknown
var name: String { var name: String {
"\(height)p\(refreshRate != -1 && refreshRate != 30 ? ", \(refreshRate) fps" : "")" switch self {
case let .predefined(predefined):
return predefined.rawValue
case let .custom(height, refreshRate):
return "\(height)p\(refreshRate != 30 ? ", \(refreshRate) fps" : "")"
}
} }
var height: Int { var height: Int {
if self == .unknown { switch self {
return -1 case let .predefined(predefined):
return predefined.height
case let .custom(height, _):
return height
} }
let resolutionPart = rawValue.components(separatedBy: "p").first!
return Int(resolutionPart.components(separatedBy: CharacterSet.decimalDigits.inverted).joined())!
} }
var refreshRate: Int { var refreshRate: Int {
if self == .unknown { switch self {
return -1 case let .predefined(predefined):
return predefined.refreshRate
case let .custom(_, refreshRate):
return refreshRate
} }
let refreshRatePart = rawValue.components(separatedBy: "p")[1]
if refreshRatePart.isEmpty {
return 30
}
return Int(refreshRatePart.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()) ?? -1
} }
// These values are an approximation.
// https://support.google.com/youtube/answer/1722171?hl=en#zippy=%2Cbitrate
var bitrate: Int { var bitrate: Int {
switch self { switch self {
// 8K UHD (16:9) Resolutions case let .predefined(predefined):
case .hd4320p60, .hd4320p50, .hd4320p48, .hd4320p30, .hd4320p25, .hd4320p24: return predefined.bitrate
return 85_000_000 // 85 Mbit/s case let .custom(height, refreshRate):
// Find the closest predefined resolution based on height and refresh rate
// 5K (16:9) Resolutions let closestPredefined = Stream.Resolution.PredefinedResolution.allCases.min {
case .hd2880p60, .hd2880p50, .hd2880p48, .hd2880p30, .hd2880p25, .hd2880p24: abs($0.height - height) + abs($0.refreshRate - refreshRate) <
return 45_000_000 // 45 Mbit/s abs($1.height - height) + abs($1.refreshRate - refreshRate)
}
// 2:1 Aspect Ratio (Univisium) Resolutions // Return the bitrate of the closest predefined resolution or a default bitrate if no close match is found
case .hd2560p60, .hd2560p50, .hd2560p48, .hd2560p30, .hd2560p25, .hd2560p24: return closestPredefined?.bitrate ?? 5_000_000
return 30_000_000 // 30 Mbit/s
// 16:10 Resolutions
case .hd2400p60, .hd2400p50, .hd2400p48, .hd2400p30, .hd2400p25, .hd2400p24:
return 35_000_000 // 35 Mbit/s
// 4K UHD (16:9) Resolutions
case .hd2160p60, .hd2160p50, .hd2160p48, .hd2160p30, .hd2160p25, .hd2160p24:
return 56_000_000 // 56 Mbit/s
// 16:10 Resolutions
case .hd1600p60, .hd1600p50, .hd1600p48, .hd1600p30, .hd1600p25, .hd1600p24:
return 20_000_000 // 20 Mbit/s
// 1440p (16:9) Resolutions
case .hd1440p60, .hd1440p50, .hd1440p48, .hd1440p30, .hd1440p25, .hd1440p24:
return 24_000_000 // 24 Mbit/s
// 1280p (16:10) Resolutions
case .hd1280p60, .hd1280p50, .hd1280p48, .hd1280p30, .hd1280p25, .hd1280p24:
return 15_000_000 // 15 Mbit/s
// 1200p (16:10) Resolutions
case .hd1200p60, .hd1200p50, .hd1200p48, .hd1200p30, .hd1200p25, .hd1200p24:
return 18_000_000 // 18 Mbit/s
// 1080p (16:9) Resolutions
case .hd1080p60, .hd1080p50, .hd1080p48, .hd1080p30, .hd1080p25, .hd1080p24:
return 12_000_000 // 12 Mbit/s
// 1050p (16:10) Resolutions
case .hd1050p60, .hd1050p50, .hd1050p48, .hd1050p30, .hd1050p25, .hd1050p24:
return 10_000_000 // 10 Mbit/s
// 960p Resolutions
case .hd960p60, .hd960p50, .hd960p48, .hd960p30, .hd960p25, .hd960p24:
return 8_000_000 // 8 Mbit/s
// 900p (16:10) Resolutions
case .hd900p60, .hd900p50, .hd900p48, .hd900p30, .hd900p25, .hd900p24:
return 7_000_000 // 7 Mbit/s
// 800p (16:10) Resolutions
case .hd800p60, .hd800p50, .hd800p48, .hd800p30, .hd800p25, .hd800p24:
return 6_000_000 // 6 Mbit/s
// 720p (16:9) Resolutions
case .hd720p60, .hd720p50, .hd720p48, .hd720p30, .hd720p25, .hd720p24:
return 9_500_000 // 9.5 Mbit/s
// Standard Definition (SD) Resolutions
case .sd854p30, .sd854p25, .sd768p30, .sd768p25, .sd640p30, .sd640p25:
return 4_000_000 // 4 Mbit/s
case .sd480p30, .sd480p25:
return 2_500_000 // 2.5 Mbit/s
case .sd428p30, .sd428p25, .sd426p30, .sd426p25:
return 2_000_000 // 2 Mbit/s
case .sd360p30, .sd360p25:
return 1_500_000 // 1.5 Mbit/s
case .sd320p30, .sd320p25:
return 1_200_000 // 1.2 Mbit/s
case .sd256p30, .sd256p25, .sd240p30, .sd240p25:
return 1_000_000 // 1 Mbit/s
case .sd214p30, .sd214p25:
return 800_000 // 0.8 Mbit/s
case .sd144p30, .sd144p25:
return 600_000 // 0.6 Mbit/s
case .sd128p30, .sd128p25:
return 400_000 // 0.4 Mbit/s
case .unknown:
return 0
} }
} }
static func from(resolution: String, fps: Int? = nil) -> Self { static func from(resolution: String, fps: Int? = nil) -> Self {
allCases.first { $0.rawValue.contains(resolution) && $0.refreshRate == (fps ?? 30) } ?? .unknown if let predefined = PredefinedResolution(rawValue: resolution) {
return .predefined(predefined)
}
// Attempt to parse height and refresh rate
if let height = Int(resolution.components(separatedBy: "p").first ?? ""), height > 0 {
let refreshRate = fps ?? 30
return .custom(height: height, refreshRate: refreshRate)
}
// Default behavior if parsing fails
return .custom(height: 720, refreshRate: 30)
} }
static func < (lhs: Self, rhs: Self) -> Bool { static func < (lhs: Self, rhs: Self) -> Bool {
lhs.height == rhs.height ? (lhs.refreshRate < rhs.refreshRate) : (lhs.height < rhs.height) lhs.height == rhs.height ? (lhs.refreshRate < rhs.refreshRate) : (lhs.height < rhs.height)
} }
enum CodingKeys: String, CodingKey {
case predefined
case custom
case height
case refreshRate
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let predefinedValue = try? container.decode(PredefinedResolution.self, forKey: .predefined) {
self = .predefined(predefinedValue)
} else if let height = try? container.decode(Int.self, forKey: .height),
let refreshRate = try? container.decode(Int.self, forKey: .refreshRate)
{
self = .custom(height: height, refreshRate: refreshRate)
} else {
// Set default resolution to 720p 30 if decoding fails
self = .custom(height: 720, refreshRate: 30)
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case let .predefined(predefinedValue):
try container.encode(predefinedValue, forKey: .predefined)
case let .custom(height, refreshRate):
try container.encode(height, forKey: .height)
try container.encode(refreshRate, forKey: .refreshRate)
}
}
} }
enum Kind: String, Comparable { enum Kind: String, Comparable {
@ -482,3 +316,97 @@ class Stream: Equatable, Hashable, Identifiable {
} }
} }
} }
extension Stream.Resolution.PredefinedResolution {
var height: Int {
switch self {
// 8K UHD (16:9) Resolutions
case .hd4320p60, .hd4320p30:
return 4320
// 4K UHD (16:9) Resolutions
case .hd2160p60, .hd2160p30:
return 2160
// 1440p (16:9) Resolutions
case .hd1440p60, .hd1440p30:
return 1440
// 1080p (Full HD, 16:9) Resolutions
case .hd1080p60, .hd1080p30:
return 1080
// 720p (HD, 16:9) Resolutions
case .hd720p60, .hd720p30:
return 720
// Standard Definition (SD) Resolutions
case .sd480p30:
return 480
case .sd360p30:
return 360
case .sd240p30:
return 240
case .sd144p30:
return 144
}
}
var refreshRate: Int {
switch self {
// 60 fps Resolutions
case .hd4320p60, .hd2160p60, .hd1440p60, .hd1080p60, .hd720p60:
return 60
// 30 fps Resolutions
case .hd4320p30, .hd2160p30, .hd1440p30, .hd1080p30, .hd720p30,
.sd480p30, .sd360p30, .sd240p30, .sd144p30:
return 30
}
}
// These values are an approximation.
// https://support.google.com/youtube/answer/1722171?hl=en#zippy=%2Cbitrate
var bitrate: Int {
switch self {
// 8K UHD (16:9) Resolutions
case .hd4320p60:
return 180_000_000 // Midpoint between 120 Mbps and 240 Mbps
case .hd4320p30:
return 120_000_000 // Midpoint between 80 Mbps and 160 Mbps
// 4K UHD (16:9) Resolutions
case .hd2160p60:
return 60_500_000 // Midpoint between 53 Mbps and 68 Mbps
case .hd2160p30:
return 40_000_000 // Midpoint between 35 Mbps and 45 Mbps
// 1440p (2K) Resolutions
case .hd1440p60:
return 24_000_000 // 24 Mbps
case .hd1440p30:
return 16_000_000 // 16 Mbps
// 1080p (Full HD, 16:9) Resolutions
case .hd1080p60:
return 12_000_000 // 12 Mbps
case .hd1080p30:
return 8_000_000 // 8 Mbps
// 720p (HD, 16:9) Resolutions
case .hd720p60:
return 7_500_000 // 7.5 Mbps
case .hd720p30:
return 5_000_000 // 5 Mbps
// Standard Definition (SD) Resolutions
case .sd480p30:
return 2_500_000 // 2.5 Mbps
case .sd360p30:
return 1_000_000 // 1 Mbps
case .sd240p30:
return 1_000_000 // 1 Mbps
case .sd144p30:
return 600_000 // 0.6 Mbps
}
}
}

View File

@ -424,18 +424,34 @@ enum ResolutionSetting: String, CaseIterable, Defaults.Serializable {
case sd240p30 case sd240p30
case sd144p30 case sd144p30
var value: Stream.Resolution! { var value: Stream.Resolution {
.init(rawValue: rawValue) if let predefined = Stream.Resolution.PredefinedResolution(rawValue: rawValue) {
return .predefined(predefined)
}
// Provide a default value of 720p 30
return .custom(height: 720, refreshRate: 30)
} }
var description: String { var description: String {
switch self { let resolution = value
case .hd2160p60: let height = resolution.height
return "4K, 60fps" let refreshRate = resolution.refreshRate
case .hd2160p30:
return "4K" // Superscript labels
let superscript4K = "⁴ᴷ"
let superscriptHD = "ᴴᴰ"
// Special handling for specific resolutions
switch height {
case 2160:
// 4K superscript after the refresh rate
return refreshRate == 30 ? "2160p \(superscript4K)" : "2160p\(refreshRate) \(superscript4K)"
case 1440, 1080:
// HD superscript after the refresh rate
return refreshRate == 30 ? "\(height)p \(superscriptHD)" : "\(height)p\(refreshRate) \(superscriptHD)"
default: default:
return value.name // Default formatting for other resolutions
return refreshRate == 30 ? "\(height)p" : "\(height)p\(refreshRate)"
} }
} }
} }

View File

@ -315,7 +315,9 @@ struct QualityProfileForm: View {
func isResolutionDisabled(_ resolution: ResolutionSetting) -> Bool { func isResolutionDisabled(_ resolution: ResolutionSetting) -> Bool {
guard backend == .appleAVPlayer else { return false } guard backend == .appleAVPlayer else { return false }
return resolution.value > .hd720p30 let hd720p30 = Stream.Resolution.predefined(.hd720p30)
return resolution.value > hd720p30
} }
func initializeForm() { func initializeForm() {