From 6511d4c9ba7c98401236fbd76456c597d3321ab5 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Fri, 14 Nov 2025 18:58:27 +0100 Subject: [PATCH] Add nil safety checks for stream resolution handling Added comprehensive nil checks for stream resolution values across PlayerBackend, QualityProfile, and PlayerQueue to prevent crashes when streams have missing resolution metadata. Also added backend nil checks in PlayerQueue. --- Model/Player/Backends/PlayerBackend.swift | 30 ++++++++++++++++++----- Model/Player/PlayerQueue.swift | 12 +++++++-- Model/QualityProfile.swift | 7 +++++- 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/Model/Player/Backends/PlayerBackend.swift b/Model/Player/Backends/PlayerBackend.swift index d5130799..e9a11fd7 100644 --- a/Model/Player/Backends/PlayerBackend.swift +++ b/Model/Player/Backends/PlayerBackend.swift @@ -154,9 +154,14 @@ extension PlayerBackend { let nonHLSStreams = streams.filter { let isHLS = $0.kind == .hls // Check if the stream's resolution is within the maximum allowed resolution - let isWithinResolution = $0.resolution.map { $0 <= maxResolution.value } ?? false + // Safety: Ensure resolution exists before comparing + guard let streamResolution = $0.resolution else { + logger.info("Stream ID: \($0.id) has nil resolution, skipping") + return false + } + let isWithinResolution = streamResolution <= maxResolution.value - 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: streamResolution)) - Bitrate: \($0.bitrate ?? 0)") logger.info("Is HLS: \(isHLS), Is within resolution: \(isWithinResolution)") logger.info("video url: \($0.videoAsset?.url.absoluteString ?? "nil"), audio url: \($0.audioAsset?.url.absoluteString ?? "nil")") @@ -191,8 +196,13 @@ extension PlayerBackend { } let filteredStreams = adjustedStreams.filter { stream in + // Safety check: Ensure stream has a resolution + guard let streamResolution = stream.resolution else { + logger.info("Filtered stream ID: \(stream.id) has nil resolution, excluding") + return false + } // Check if the stream's resolution is within the maximum allowed resolution - let isWithinResolution = stream.resolution <= maxResolution.value + let isWithinResolution = streamResolution <= maxResolution.value logger.info("Filtered stream ID: \(stream.id) - Is within max resolution: \(isWithinResolution)") return isWithinResolution } @@ -200,7 +210,15 @@ extension PlayerBackend { logger.info("Filtered streams count after adjustments: \(filteredStreams.count)") let bestStream = filteredStreams.max { lhs, rhs in - if lhs.resolution == rhs.resolution { + // Safety check: Ensure both streams have resolutions + guard let lhsResolution = lhs.resolution, let rhsResolution = rhs.resolution else { + logger.info("One or both streams missing resolution - LHS: \(lhs.id), RHS: \(rhs.id)") + // If lhs has no resolution, it's "less than" rhs (prefer rhs) + // If rhs has no resolution, it's "less than" lhs (prefer lhs) + return lhs.resolution == nil + } + + if lhsResolution == rhsResolution { guard let lhsFormat = QualityProfile.Format(rawValue: lhs.format.rawValue), let rhsFormat = QualityProfile.Format(rawValue: rhs.format.rawValue) else { @@ -216,9 +234,9 @@ extension PlayerBackend { return lhsFormatIndex > rhsFormatIndex } - logger.info("Comparing resolutions for streams \(lhs.id) and \(rhs.id) - LHS Resolution: \(String(describing: lhs.resolution)), RHS Resolution: \(String(describing: rhs.resolution))") + logger.info("Comparing resolutions for streams \(lhs.id) and \(rhs.id) - LHS Resolution: \(String(describing: lhsResolution)), RHS Resolution: \(String(describing: rhsResolution))") - return lhs.resolution < rhs.resolution + return lhsResolution < rhsResolution } logger.info("Best stream selected: \(String(describing: bestStream?.id)) with resolution: \(String(describing: bestStream?.resolution)) and format: \(String(describing: bestStream?.format))") diff --git a/Model/Player/PlayerQueue.swift b/Model/Player/PlayerQueue.swift index e75bf1fe..8e2ad023 100644 --- a/Model/Player/PlayerQueue.swift +++ b/Model/Player/PlayerQueue.swift @@ -52,7 +52,7 @@ extension PlayerModel { func playItem(_ item: PlayerQueueItem, at time: CMTime? = nil) { advancing = false - if !playingInPictureInPicture, !currentItem.isNil { + if !playingInPictureInPicture, !currentItem.isNil, backend != nil { backend.closeItem() } @@ -125,6 +125,12 @@ extension PlayerModel { } var streamByQualityProfile: Stream? { + // Safety check: Ensure backend is available + guard backend != nil else { + logger.error("Backend is nil when trying to select stream by quality profile") + return nil + } + let profile = qualityProfile ?? .defaultProfile // First attempt: Filter by both `canPlay` and `isPreferred` @@ -229,7 +235,9 @@ extension PlayerModel { self.removeQueueItems() } - backend.closeItem() + if backend != nil { + backend.closeItem() + } } @discardableResult func enqueueVideo( diff --git a/Model/QualityProfile.swift b/Model/QualityProfile.swift index c731b2da..178da97d 100644 --- a/Model/QualityProfile.swift +++ b/Model/QualityProfile.swift @@ -76,8 +76,13 @@ struct QualityProfile: Hashable, Identifiable, Defaults.Serializable { return true } + // Safety check: Ensure stream has a resolution + guard let streamResolution = stream.resolution else { + return false + } + let defaultResolution = Stream.Resolution.custom(height: 720, refreshRate: 30) - let resolutionMatch = resolution.value ?? defaultResolution >= stream.resolution + let resolutionMatch = resolution.value ?? defaultResolution >= streamResolution if resolutionMatch, formats.contains(.stream), stream.kind == .stream { return true