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.
This commit is contained in:
Toni Förster 2024-05-13 07:54:24 +02:00
parent fba01e35a3
commit b54044cbc5
No known key found for this signature in database
GPG Key ID: 292F3E5086C83FC7
9 changed files with 79 additions and 9 deletions

View File

@ -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
)
}
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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()
}

View File

@ -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)
}

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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() {