From b54044cbc5902dfaff516847dc810d80abb2ade9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20F=C3=B6rster?= Date: Mon, 13 May 2024 07:54:24 +0200 Subject: [PATCH] HLS: set target bitrate / AVPlayer: higher resolution HLS: try matching the set resolution. This works okay with AVPlayer. With MPV it is hit and miss, most of the time MPV targets the highest available bitrate, instead of the set bitrate. AVPlayer now supports higher resolution up to 1080p60. --- Model/Applications/InvidiousAPI.swift | 3 +- Model/Applications/PipedAPI.swift | 4 ++- Model/Player/Backends/AVPlayerBackend.swift | 5 ++++ Model/Player/Backends/MPVBackend.swift | 4 +-- Model/Player/Backends/MPVClient.swift | 6 ++++ Model/Player/Backends/PlayerBackend.swift | 10 +++++-- Model/Stream.swift | 31 ++++++++++++++++++++- Shared/Constants.swift | 17 +++++++++++ Shared/Settings/QualityProfileForm.swift | 8 ++++-- 9 files changed, 79 insertions(+), 9 deletions(-) diff --git a/Model/Applications/InvidiousAPI.swift b/Model/Applications/InvidiousAPI.swift index e22445ad..d440b851 100644 --- a/Model/Applications/InvidiousAPI.swift +++ b/Model/Applications/InvidiousAPI.swift @@ -654,7 +654,8 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI { resolution: Stream.Resolution.from(resolution: videoStream["resolution"].stringValue), kind: .adaptive, encoding: videoStream["encoding"].string, - videoFormat: videoStream["type"].string + videoFormat: videoStream["type"].string, + bitrate: videoStream["bitrate"].int ) } } diff --git a/Model/Applications/PipedAPI.swift b/Model/Applications/PipedAPI.swift index f456d343..96df8bba 100644 --- a/Model/Applications/PipedAPI.swift +++ b/Model/Applications/PipedAPI.swift @@ -687,6 +687,7 @@ final class PipedAPI: Service, ObservableObject, VideosAPI { let fps = qualityComponents.count > 1 ? Int(qualityComponents[1]) : 30 let resolution = Stream.Resolution.from(resolution: quality, fps: fps) let videoFormat = videoStream.dictionaryValue["format"]?.string + let bitrate = videoStream.dictionaryValue["bitrate"]?.int if videoOnly { streams.append( @@ -696,7 +697,8 @@ final class PipedAPI: Service, ObservableObject, VideosAPI { videoAsset: videoAsset, resolution: resolution, kind: .adaptive, - videoFormat: videoFormat + videoFormat: videoFormat, + bitrate: bitrate ) ) } else { diff --git a/Model/Player/Backends/AVPlayerBackend.swift b/Model/Player/Backends/AVPlayerBackend.swift index af5dc8c3..78abbd73 100644 --- a/Model/Player/Backends/AVPlayerBackend.swift +++ b/Model/Player/Backends/AVPlayerBackend.swift @@ -298,6 +298,11 @@ final class AVPlayerBackend: PlayerBackend { removeItemDidPlayToEndTimeObserver() model.playerItem = playerItem(stream) + + if stream.isHLS { + model.playerItem?.preferredPeakBitRate = Double(model.qualityProfile?.resolution.value.bitrate ?? 0) + } + guard model.playerItem != nil else { return } diff --git a/Model/Player/Backends/MPVBackend.swift b/Model/Player/Backends/MPVBackend.swift index 5216e5ae..0a88387b 100644 --- a/Model/Player/Backends/MPVBackend.swift +++ b/Model/Player/Backends/MPVBackend.swift @@ -286,7 +286,7 @@ final class MPVBackend: PlayerBackend { } } - client.loadFile(url, sub: captions?.url, time: time, forceSeekable: stream.kind == .hls) { [weak self] _ in + client.loadFile(url, bitrate: stream.bitrate, kind: stream.kind, sub: captions?.url, time: time, forceSeekable: stream.kind == .hls) { [weak self] _ in self?.isLoadingVideo = true } } else { @@ -298,7 +298,7 @@ final class MPVBackend: PlayerBackend { let fileToLoad = self.model.musicMode ? stream.audioAsset.url : stream.videoAsset.url let audioTrack = self.model.musicMode ? nil : stream.audioAsset.url - client.loadFile(fileToLoad, audio: audioTrack, sub: captions?.url, time: time, forceSeekable: stream.kind == .hls) { [weak self] _ in + client.loadFile(fileToLoad, audio: audioTrack, bitrate: stream.bitrate, kind: stream.kind, sub: captions?.url, time: time, forceSeekable: stream.kind == .hls) { [weak self] _ in self?.isLoadingVideo = true self?.pause() } diff --git a/Model/Player/Backends/MPVClient.swift b/Model/Player/Backends/MPVClient.swift index f42b840a..804b16de 100644 --- a/Model/Player/Backends/MPVClient.swift +++ b/Model/Player/Backends/MPVClient.swift @@ -128,6 +128,8 @@ final class MPVClient: ObservableObject { func loadFile( _ url: URL, audio: URL? = nil, + bitrate: Int? = nil, + kind: Stream.Kind, sub: URL? = nil, time: CMTime? = nil, forceSeekable: Bool = false, @@ -160,6 +162,10 @@ final class MPVClient: ObservableObject { args.append(options.joined(separator: ",")) } + if kind == .hls, bitrate != 0 { + checkError(mpv_set_option_string(mpv, "hls-bitrate", String(describing: bitrate))) + } + command("loadfile", args: args, returnValueCallback: completionHandler) } diff --git a/Model/Player/Backends/PlayerBackend.swift b/Model/Player/Backends/PlayerBackend.swift index fce56900..d9469c09 100644 --- a/Model/Player/Backends/PlayerBackend.swift +++ b/Model/Player/Backends/PlayerBackend.swift @@ -137,11 +137,17 @@ extension PlayerBackend { // find max resolution from non HLS streams let bestResolution = nonHLSStreams .filter { $0.resolution <= maxResolution.value } - .max { $0.resolution < $1.resolution }?.resolution + .max { $0.resolution < $1.resolution } + + // finde max bitrate from non HLS streams + let bestBitrate = nonHLSStreams + .filter { $0.resolution <= maxResolution.value } + .max { $0.bitrate ?? 0 < $1.bitrate ?? 0 } return streams.map { stream in if stream.kind == .hls { - stream.resolution = bestResolution ?? maxResolution.value + stream.resolution = bestResolution?.resolution ?? maxResolution.value + stream.bitrate = bestBitrate?.bitrate ?? (bestResolution?.resolution.bitrate ?? maxResolution.value.bitrate) stream.format = .hls } else if stream.kind == .stream { stream.format = .stream diff --git a/Model/Stream.swift b/Model/Stream.swift index 65e58993..2d82d2ea 100644 --- a/Model/Stream.swift +++ b/Model/Stream.swift @@ -54,6 +54,32 @@ class Stream: Equatable, Hashable, Identifiable { return Int(refreshRatePart.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()) ?? -1 } + // These values are an approximation. + // https://support.google.com/youtube/answer/1722171?hl=en#zippy=%2Cbitrate + + var bitrate: Int { + switch self { + case .hd2160p60, .hd2160p50, .hd2160p48, .hd2160p30: + return 56000000 // 56 Mbit/s + case .hd1440p60, .hd1440p50, .hd1440p48, .hd1440p30: + return 24000000 // 24 Mbit/s + case .hd1080p60, .hd1080p50, .hd1080p48, .hd1080p30: + return 12000000 // 12 Mbit/s + case .hd720p60, .hd720p50, .hd720p48, .hd720p30: + return 9500000 // 9.5 Mbit/s + case .sd480p30: + return 4000000 // 4 Mbit/s + case .sd360p30: + return 1500000 // 1.5 Mbit/s + case .sd240p30: + return 1000000 // 1 Mbit/s + case .sd144p30: + return 600000 // 0.6 Mbit/s + case .unknown: + return 0 + } + } + static func from(resolution: String, fps: Int? = nil) -> Self { allCases.first { $0.rawValue.contains(resolution) && $0.refreshRate == (fps ?? 30) } ?? .unknown } @@ -143,6 +169,7 @@ class Stream: Equatable, Hashable, Identifiable { var encoding: String? var videoFormat: String? + var bitrate: Int? init( instance: Instance? = nil, @@ -153,7 +180,8 @@ class Stream: Equatable, Hashable, Identifiable { resolution: Resolution? = nil, kind: Kind = .hls, encoding: String? = nil, - videoFormat: String? = nil + videoFormat: String? = nil, + bitrate: Int? = nil ) { self.instance = instance self.audioAsset = audioAsset @@ -164,6 +192,7 @@ class Stream: Equatable, Hashable, Identifiable { self.kind = kind self.encoding = encoding format = .from(videoFormat ?? "") + self.bitrate = bitrate } var isLocal: Bool { diff --git a/Shared/Constants.swift b/Shared/Constants.swift index 59bae11f..c77e83d4 100644 --- a/Shared/Constants.swift +++ b/Shared/Constants.swift @@ -5,6 +5,23 @@ import SwiftUI enum Constants { static let yatteeProtocol = "yattee://" static let overlayAnimation = Animation.linear(duration: 0.2) + + static var isAppleTV: Bool { + #if os(iOS) + UIDevice.current.userInterfaceIdiom == .tv + #else + false + #endif + } + + static var isMac: Bool { + #if os(iOS) + UIDevice.current.userInterfaceIdiom == .mac + #else + false + #endif + } + static var isIPhone: Bool { #if os(iOS) UIDevice.current.userInterfaceIdiom == .phone diff --git a/Shared/Settings/QualityProfileForm.swift b/Shared/Settings/QualityProfileForm.swift index 18a3fe48..2607cbfa 100644 --- a/Shared/Settings/QualityProfileForm.swift +++ b/Shared/Settings/QualityProfileForm.swift @@ -140,7 +140,11 @@ struct QualityProfileForm: View { Text("Formats can be reordered and will be selected in this order.") .foregroundColor(.secondary) .fixedSize(horizontal: false, vertical: true) - Text("**Note:** HLS is an adaptive format, resolution setting doesn't apply.") + Text("**Note:** HLS is an adaptive format where specific resolution settings don't apply.") + .foregroundColor(.secondary) + .fixedSize(horizontal: false, vertical: true) + .padding(.top) + Text("Yattee attempts to match the quality that is closest to the set resolution, but exact results cannot be guaranteed.") .foregroundColor(.secondary) .fixedSize(horizontal: false, vertical: true) .padding(.top, 0.1) @@ -301,7 +305,7 @@ struct QualityProfileForm: View { func isResolutionDisabled(_ resolution: ResolutionSetting) -> Bool { guard backend == .appleAVPlayer else { return false } - return resolution.value > .hd720p30 + return resolution.value > .hd1080p60 } func initializeForm() {