From a45522f710da7c9555b9dfebe37d8ff496bfe55a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20F=C3=B6rster?= Date: Sun, 25 Aug 2024 17:23:04 +0200 Subject: [PATCH] Improved stream resolution handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Invidious now reports the actual resolution and doesn’t hardcode them anymore. See: https://github.com/iv-org/invidious/pull/4586 - Extended the list of possible resolutions in the StreamModel - trigger videoLoadFailureHandler if no streams are available - more logging for backend.bestPlayable Signed-off-by: Toni Förster --- Model/Player/Backends/PlayerBackend.swift | 63 +++++-- Model/Player/PlayerQueue.swift | 20 +- Model/Stream.swift | 215 +++++++++++++++++++++- 3 files changed, 276 insertions(+), 22 deletions(-) diff --git a/Model/Player/Backends/PlayerBackend.swift b/Model/Player/Backends/PlayerBackend.swift index 96cd69cb..9f02cfcb 100644 --- a/Model/Player/Backends/PlayerBackend.swift +++ b/Model/Player/Backends/PlayerBackend.swift @@ -1,6 +1,7 @@ import CoreMedia import Defaults import Foundation +import Logging #if !os(macOS) import UIKit #endif @@ -75,6 +76,10 @@ protocol PlayerBackend { } extension PlayerBackend { + var logger: Logger { + return Logger(label: "stream.yattee.player.backend") + } + func seek(to time: CMTime, seekType: SeekType, completionHandler: ((Bool) -> Void)? = nil) { model.seek.registerSeek(at: time, type: seekType, restore: currentTime) seek(to: time, seekType: seekType, completionHandler: completionHandler) @@ -140,55 +145,87 @@ extension PlayerBackend { } func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting, formatOrder: [QualityProfile.Format]) -> Stream? { - // filter out non-HLS streams and streams with resolution more than maxResolution - let nonHLSStreams = streams.filter { - $0.kind != .hls && $0.resolution <= maxResolution.value - } + logger.info("Starting bestPlayable function") + logger.info("Total streams received: \(streams.count)") + logger.info("Max resolution allowed: \(String(describing: maxResolution.value))") + logger.info("Format order: \(formatOrder)") - // find max resolution and bitrate from non-HLS streams + // Filter out non-HLS streams and streams with resolution more than maxResolution + let nonHLSStreams = streams.filter { + let isHLS = $0.kind == .hls + let isWithinResolution = $0.resolution <= maxResolution.value + 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)") + return !isHLS && isWithinResolution + } + logger.info("Non-HLS streams after filtering: \(nonHLSStreams.count)") + + // Find max resolution and bitrate from non-HLS streams let bestResolutionStream = nonHLSStreams.max { $0.resolution < $1.resolution } let bestBitrateStream = nonHLSStreams.max { $0.bitrate ?? 0 < $1.bitrate ?? 0 } + logger.info("Best resolution stream: \(String(describing: bestResolutionStream?.id)) with resolution: \(String(describing: bestResolutionStream?.resolution))") + logger.info("Best bitrate stream: \(String(describing: bestBitrateStream?.id)) with bitrate: \(String(describing: bestBitrateStream?.bitrate))") + let bestResolution = bestResolutionStream?.resolution ?? maxResolution.value let bestBitrate = bestBitrateStream?.bitrate ?? bestResolutionStream?.resolution.bitrate ?? maxResolution.value.bitrate - return streams.map { stream in + logger.info("Final best resolution selected: \(String(describing: bestResolution))") + logger.info("Final best bitrate selected: \(bestBitrate)") + + let adjustedStreams = streams.map { stream in if stream.kind == .hls { + logger.info("Adjusting HLS stream ID: \(stream.id)") stream.resolution = bestResolution stream.bitrate = bestBitrate stream.format = .hls } else if stream.kind == .stream { + logger.info("Adjusting non-HLS stream ID: \(stream.id)") stream.format = .stream } return stream } - .filter { stream in - stream.resolution <= maxResolution.value + + let filteredStreams = adjustedStreams.filter { stream in + let isWithinResolution = stream.resolution <= maxResolution.value + logger.info("Filtered stream ID: \(stream.id) - Is within max resolution: \(isWithinResolution)") + return isWithinResolution } - .max { lhs, rhs in + + logger.info("Filtered streams count after adjustments: \(filteredStreams.count)") + + let bestStream = filteredStreams.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") + logger.info("Failed to extract lhsFormat or rhsFormat for streams \(lhs.id) and \(rhs.id)") return false } let lhsFormatIndex = formatOrder.firstIndex(of: lhsFormat) ?? Int.max let rhsFormatIndex = formatOrder.firstIndex(of: rhsFormat) ?? Int.max + logger.info("Comparing formats for streams \(lhs.id) and \(rhs.id) - LHS Format Index: \(lhsFormatIndex), RHS Format Index: \(rhsFormatIndex)") + 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))") + return lhs.resolution < rhs.resolution } + + logger.info("Best stream selected: \(String(describing: bestStream?.id)) with resolution: \(String(describing: bestStream?.resolution)) and format: \(String(describing: bestStream?.format))") + + return bestStream } func updateControls(completionHandler: (() -> Void)? = nil) { - print("updating controls") + logger.info("updating controls") guard model.presentingPlayer, !model.controls.presentingOverlays else { - print("ignored controls update") + logger.info("ignored controls update") completionHandler?() return } @@ -196,7 +233,7 @@ extension PlayerBackend { DispatchQueue.main.async(qos: .userInteractive) { #if !os(macOS) guard UIApplication.shared.applicationState != .background else { - print("not performing controls updates in background") + logger.info("not performing controls updates in background") completionHandler?() return } diff --git a/Model/Player/PlayerQueue.swift b/Model/Player/PlayerQueue.swift index 90e2c582..0180884d 100644 --- a/Model/Player/PlayerQueue.swift +++ b/Model/Player/PlayerQueue.swift @@ -127,6 +127,7 @@ extension PlayerModel { var streamByQualityProfile: Stream? { let profile = qualityProfile ?? .defaultProfile + // First attempt: Filter by both `canPlay` and `isPreferred` if let streamPreferredForProfile = backend.bestPlayable( availableStreams.filter { backend.canPlay($0) && profile.isPreferred($0) }, maxResolution: profile.resolution, formatOrder: profile.formats @@ -134,7 +135,24 @@ extension PlayerModel { return streamPreferredForProfile } - return backend.bestPlayable(availableStreams.filter { backend.canPlay($0) }, maxResolution: profile.resolution, formatOrder: profile.formats) + // Fallback: Filter by `canPlay` only + let fallbackStream = backend.bestPlayable( + availableStreams.filter { backend.canPlay($0) }, + maxResolution: profile.resolution, formatOrder: profile.formats + ) + + // If no stream is found, trigger the error handler + guard let finalStream = fallbackStream else { + let error = RequestError( + userMessage: "No supported streams available.", + cause: NSError(domain: "stream.yatte.app", code: -1, userInfo: [NSLocalizedDescriptionKey: "No supported streams available"]) + ) + videoLoadFailureHandler(error, video: currentVideo) + return nil + } + + // Return the found stream + return finalStream } func advanceToNextItem() { diff --git a/Model/Stream.swift b/Model/Stream.swift index 8c7465a3..584046d1 100644 --- a/Model/Stream.swift +++ b/Model/Stream.swift @@ -5,26 +5,153 @@ import Foundation // swiftlint:disable:next final_class class Stream: Equatable, Hashable, Identifiable { enum Resolution: String, CaseIterable, Comparable, Defaults.Serializable { + // Some 16:19 and 16:10 resolutions are also used in 2:1 videos + + // 8K UHD (16:9) Resolutions + case hd4320p60 + case hd4320p50 + case hd4320p48 + case hd4320p30 + case hd4320p25 + case hd4320p24 + + // 5K (16:9) Resolutions + case hd2560p60 + case hd2560p50 + case hd2560p48 + case hd2560p30 + case hd2560p25 + case hd2560p24 + + // 2:1 Aspect Ratio (Univisium) Resolutions + case hd2880p60 + case hd2880p50 + case hd2880p48 + case hd2880p30 + case hd2880p25 + case hd2880p24 + + // 16:10 Resolutions + case hd2400p60 + case hd2400p50 + case hd2400p48 + case hd2400p30 + case hd2400p25 + case hd2400p24 + + // 16:9 Resolutions case hd2160p60 case hd2160p50 case hd2160p48 case hd2160p30 + case hd2160p25 + case hd2160p24 + + // 16:10 Resolutions + case hd1600p60 + case hd1600p50 + case hd1600p48 + case hd1600p30 + 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 sd360p30 + case sd360p25 + case sd320p30 + case sd320p25 case sd240p30 + case sd240p25 + case sd214p30 + case sd214p25 case sd144p30 + case sd144p25 + case sd128p30 + case sd128p25 + case unknown var name: String { @@ -59,22 +186,94 @@ class Stream: Equatable, Hashable, Identifiable { var bitrate: Int { switch self { - case .hd2160p60, .hd2160p50, .hd2160p48, .hd2160p30: + // 8K UHD (16:9) Resolutions + case .hd4320p60, .hd4320p50, .hd4320p48, .hd4320p30, .hd4320p25, .hd4320p24: + return 85_000_000 // 85 Mbit/s + + // 5K (16:9) Resolutions + case .hd2880p60, .hd2880p50, .hd2880p48, .hd2880p30, .hd2880p25, .hd2880p24: + return 45_000_000 // 45 Mbit/s + + // 2:1 Aspect Ratio (Univisium) Resolutions + case .hd2560p60, .hd2560p50, .hd2560p48, .hd2560p30, .hd2560p25, .hd2560p24: + 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 - case .hd1440p60, .hd1440p50, .hd1440p48, .hd1440p30: + + // 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 - case .hd1080p60, .hd1080p50, .hd1080p48, .hd1080p30: + + // 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 - case .hd720p60, .hd720p50, .hd720p48, .hd720p30: + + // 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 - case .sd480p30: + + // Standard Definition (SD) Resolutions + case .sd854p30, .sd854p25, .sd768p30, .sd768p25, .sd640p30, .sd640p25: return 4_000_000 // 4 Mbit/s - case .sd360p30: + + case .sd480p30, .sd480p25: + return 2_500_000 // 2.5 Mbit/s + + case .sd428p30, .sd428p25: + return 2_000_000 // 2 Mbit/s + + case .sd360p30, .sd360p25: return 1_500_000 // 1.5 Mbit/s - case .sd240p30: + + case .sd320p30, .sd320p25: + return 1_200_000 // 1.2 Mbit/s + + case .sd240p30, .sd240p25: return 1_000_000 // 1 Mbit/s - case .sd144p30: + + 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 }