mirror of
				https://github.com/yattee/yattee.git
				synced 2025-10-31 04:31:54 +00:00 
			
		
		
		
	Merge pull request #747 from stonerl/fix-endless-loading-of-streams
Improved stream resolution handling
This commit is contained in:
		| @@ -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 | ||||
|                 } | ||||
|   | ||||
| @@ -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() { | ||||
|   | ||||
| @@ -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 | ||||
|             } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Arkadiusz Fal
					Arkadiusz Fal