Files
yattee/Model/QualityProfile.swift
Arkadiusz Fal 33377f7e0e Fix nil crash when accessing stream.format
This commit addresses crashes caused by accessing nil format values on streams:

- QualityProfile.swift: Add guard check for stream.format to prevent nil access crash
- MPVBackend.swift: Add nil check in canPlay method before comparing format
- PlayerStreams.swift: Add nil check before comparing format in asset processing

The crashes occurred when stream.format was nil and accessed as an implicitly unwrapped optional, causing "Unexpectedly found nil while implicitly unwrapping an Optional value" errors.
2025-11-19 18:03:43 +01:00

137 lines
4.2 KiB
Swift

import Defaults
import Foundation
struct QualityProfile: Hashable, Identifiable, Defaults.Serializable {
static var bridge = QualityProfileBridge()
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 avc1
case stream
case webm
case mp4
case av1
case hls
var id: String {
rawValue
}
var description: String {
switch self {
case .stream:
return "Stream"
case .webm:
return "WebM"
default:
return rawValue.uppercased()
}
}
var streamFormat: Stream.Format? {
switch self {
case .avc1:
return .avc1
case .stream:
return nil
case .webm:
return .webm
case .mp4:
return .mp4
case .av1:
return .av1
case .hls:
return nil
}
}
}
var id = UUID().uuidString
var name: String?
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)"
}
var formatsDescription: String {
switch formats.count {
case Format.allCases.count:
return "Any format".localized()
case 0:
return "No format selected".localized()
case 1 ... 3:
return formats.map(\.description).joined(separator: ", ")
default:
return String(format: "%@ formats".localized(), String(formats.count))
}
}
func isPreferred(_ stream: Stream) -> Bool {
if formats.contains(.hls), stream.kind == .hls {
return true
}
// Safety check: Ensure stream has a resolution
guard let streamResolution = stream.resolution else {
return false
}
// Safety check: Ensure stream has a format
guard let streamFormat = stream.format else {
return false
}
let defaultResolution = Stream.Resolution.custom(height: 720, refreshRate: 30)
let resolutionMatch = resolution.value ?? defaultResolution >= streamResolution
if resolutionMatch, formats.contains(.stream), stream.kind == .stream {
return true
}
let formatMatch = formats.compactMap(\.streamFormat).contains(streamFormat)
return resolutionMatch && formatMatch
}
}
struct QualityProfileBridge: Defaults.Bridge {
static let formatsSeparator = ","
typealias Value = QualityProfile
typealias Serializable = [String: String]
func serialize(_ value: Value?) -> Serializable? {
guard let value else { return nil }
return [
"id": value.id,
"name": value.name ?? "",
"backend": value.backend.rawValue,
"resolution": value.resolution.rawValue,
"formats": value.formats.map(\.rawValue).joined(separator: Self.formatsSeparator),
"order": value.order.map { String($0) }.joined(separator: Self.formatsSeparator) // New line
]
}
func deserialize(_ object: Serializable?) -> Value? {
guard let object,
let id = object["id"],
let backend = PlayerBackendType(rawValue: object["backend"] ?? ""),
let resolution = ResolutionSetting(rawValue: object["resolution"] ?? "")
else {
return nil
}
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, order: order)
}
}