Merge pull request #747 from stonerl/fix-endless-loading-of-streams

Improved stream resolution handling
This commit is contained in:
Arkadiusz Fal 2024-08-26 08:16:19 +02:00 committed by GitHub
commit d948ea6887
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 276 additions and 22 deletions

View File

@ -1,6 +1,7 @@
import CoreMedia import CoreMedia
import Defaults import Defaults
import Foundation import Foundation
import Logging
#if !os(macOS) #if !os(macOS)
import UIKit import UIKit
#endif #endif
@ -75,6 +76,10 @@ protocol PlayerBackend {
} }
extension PlayerBackend { extension PlayerBackend {
var logger: Logger {
return Logger(label: "stream.yattee.player.backend")
}
func seek(to time: CMTime, seekType: SeekType, completionHandler: ((Bool) -> Void)? = nil) { func seek(to time: CMTime, seekType: SeekType, completionHandler: ((Bool) -> Void)? = nil) {
model.seek.registerSeek(at: time, type: seekType, restore: currentTime) model.seek.registerSeek(at: time, type: seekType, restore: currentTime)
seek(to: time, seekType: seekType, completionHandler: completionHandler) seek(to: time, seekType: seekType, completionHandler: completionHandler)
@ -140,55 +145,87 @@ extension PlayerBackend {
} }
func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting, formatOrder: [QualityProfile.Format]) -> Stream? { func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting, formatOrder: [QualityProfile.Format]) -> Stream? {
// filter out non-HLS streams and streams with resolution more than maxResolution logger.info("Starting bestPlayable function")
let nonHLSStreams = streams.filter { logger.info("Total streams received: \(streams.count)")
$0.kind != .hls && $0.resolution <= maxResolution.value 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 bestResolutionStream = nonHLSStreams.max { $0.resolution < $1.resolution }
let bestBitrateStream = nonHLSStreams.max { $0.bitrate ?? 0 < $1.bitrate ?? 0 } 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 bestResolution = bestResolutionStream?.resolution ?? maxResolution.value
let bestBitrate = bestBitrateStream?.bitrate ?? bestResolutionStream?.resolution.bitrate ?? maxResolution.value.bitrate 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 { if stream.kind == .hls {
logger.info("Adjusting HLS stream ID: \(stream.id)")
stream.resolution = bestResolution stream.resolution = bestResolution
stream.bitrate = bestBitrate stream.bitrate = bestBitrate
stream.format = .hls stream.format = .hls
} else if stream.kind == .stream { } else if stream.kind == .stream {
logger.info("Adjusting non-HLS stream ID: \(stream.id)")
stream.format = .stream stream.format = .stream
} }
return 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 { if lhs.resolution == rhs.resolution {
guard let lhsFormat = QualityProfile.Format(rawValue: lhs.format.rawValue), guard let lhsFormat = QualityProfile.Format(rawValue: lhs.format.rawValue),
let rhsFormat = QualityProfile.Format(rawValue: rhs.format.rawValue) let rhsFormat = QualityProfile.Format(rawValue: rhs.format.rawValue)
else { 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 return false
} }
let lhsFormatIndex = formatOrder.firstIndex(of: lhsFormat) ?? Int.max let lhsFormatIndex = formatOrder.firstIndex(of: lhsFormat) ?? Int.max
let rhsFormatIndex = formatOrder.firstIndex(of: rhsFormat) ?? 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 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 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) { func updateControls(completionHandler: (() -> Void)? = nil) {
print("updating controls") logger.info("updating controls")
guard model.presentingPlayer, !model.controls.presentingOverlays else { guard model.presentingPlayer, !model.controls.presentingOverlays else {
print("ignored controls update") logger.info("ignored controls update")
completionHandler?() completionHandler?()
return return
} }
@ -196,7 +233,7 @@ extension PlayerBackend {
DispatchQueue.main.async(qos: .userInteractive) { DispatchQueue.main.async(qos: .userInteractive) {
#if !os(macOS) #if !os(macOS)
guard UIApplication.shared.applicationState != .background else { guard UIApplication.shared.applicationState != .background else {
print("not performing controls updates in background") logger.info("not performing controls updates in background")
completionHandler?() completionHandler?()
return return
} }

View File

@ -127,6 +127,7 @@ extension PlayerModel {
var streamByQualityProfile: Stream? { var streamByQualityProfile: Stream? {
let profile = qualityProfile ?? .defaultProfile let profile = qualityProfile ?? .defaultProfile
// First attempt: Filter by both `canPlay` and `isPreferred`
if let streamPreferredForProfile = backend.bestPlayable( if let streamPreferredForProfile = backend.bestPlayable(
availableStreams.filter { backend.canPlay($0) && profile.isPreferred($0) }, availableStreams.filter { backend.canPlay($0) && profile.isPreferred($0) },
maxResolution: profile.resolution, formatOrder: profile.formats maxResolution: profile.resolution, formatOrder: profile.formats
@ -134,7 +135,24 @@ extension PlayerModel {
return streamPreferredForProfile 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() { func advanceToNextItem() {

View File

@ -5,26 +5,153 @@ 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: 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 hd2160p60
case hd2160p50 case hd2160p50
case hd2160p48 case hd2160p48
case hd2160p30 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 hd1440p60
case hd1440p50 case hd1440p50
case hd1440p48 case hd1440p48
case hd1440p30 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 hd1080p60
case hd1080p50 case hd1080p50
case hd1080p48 case hd1080p48
case hd1080p30 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 hd720p60
case hd720p50 case hd720p50
case hd720p48 case hd720p48
case hd720p30 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 sd480p30
case sd480p25
case sd428p30
case sd428p25
case sd360p30 case sd360p30
case sd360p25
case sd320p30
case sd320p25
case sd240p30 case sd240p30
case sd240p25
case sd214p30
case sd214p25
case sd144p30 case sd144p30
case sd144p25
case sd128p30
case sd128p25
case unknown case unknown
var name: String { var name: String {
@ -59,22 +186,94 @@ class Stream: Equatable, Hashable, Identifiable {
var bitrate: Int { var bitrate: Int {
switch self { 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 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 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 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 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 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 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 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 return 600_000 // 0.6 Mbit/s
case .sd128p30, .sd128p25:
return 400_000 // 0.4 Mbit/s
case .unknown: case .unknown:
return 0 return 0
} }