Merge pull request #667 from stonerl/hls-set-target-quality

HLS: set target bitrate / AVPlayer: higher resolution
This commit is contained in:
Arkadiusz Fal 2024-05-16 18:23:05 +02:00 committed by GitHub
commit c6724472a6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
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), resolution: Stream.Resolution.from(resolution: videoStream["resolution"].stringValue),
kind: .adaptive, kind: .adaptive,
encoding: videoStream["encoding"].string, 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 fps = qualityComponents.count > 1 ? Int(qualityComponents[1]) : 30
let resolution = Stream.Resolution.from(resolution: quality, fps: fps) let resolution = Stream.Resolution.from(resolution: quality, fps: fps)
let videoFormat = videoStream.dictionaryValue["format"]?.string let videoFormat = videoStream.dictionaryValue["format"]?.string
let bitrate = videoStream.dictionaryValue["bitrate"]?.int
if videoOnly { if videoOnly {
streams.append( streams.append(
@ -696,7 +697,8 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
videoAsset: videoAsset, videoAsset: videoAsset,
resolution: resolution, resolution: resolution,
kind: .adaptive, kind: .adaptive,
videoFormat: videoFormat videoFormat: videoFormat,
bitrate: bitrate
) )
) )
} else { } else {

View File

@ -307,6 +307,11 @@ final class AVPlayerBackend: PlayerBackend {
removeItemDidPlayToEndTimeObserver() removeItemDidPlayToEndTimeObserver()
model.playerItem = playerItem(stream) model.playerItem = playerItem(stream)
if stream.isHLS {
model.playerItem?.preferredPeakBitRate = Double(model.qualityProfile?.resolution.value.bitrate ?? 0)
}
guard model.playerItem != nil else { guard model.playerItem != nil else {
return return
} }

View File

@ -301,7 +301,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 self?.isLoadingVideo = true
} }
} else { } else {
@ -313,7 +313,7 @@ final class MPVBackend: PlayerBackend {
let fileToLoad = self.model.musicMode ? stream.audioAsset.url : stream.videoAsset.url let fileToLoad = self.model.musicMode ? stream.audioAsset.url : stream.videoAsset.url
let audioTrack = self.model.musicMode ? nil : stream.audioAsset.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?.isLoadingVideo = true
self?.pause() self?.pause()
} }

View File

@ -128,6 +128,8 @@ final class MPVClient: ObservableObject {
func loadFile( func loadFile(
_ url: URL, _ url: URL,
audio: URL? = nil, audio: URL? = nil,
bitrate: Int? = nil,
kind: Stream.Kind,
sub: URL? = nil, sub: URL? = nil,
time: CMTime? = nil, time: CMTime? = nil,
forceSeekable: Bool = false, forceSeekable: Bool = false,
@ -160,6 +162,10 @@ final class MPVClient: ObservableObject {
args.append(options.joined(separator: ",")) 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) command("loadfile", args: args, returnValueCallback: completionHandler)
} }

View File

@ -137,11 +137,17 @@ extension PlayerBackend {
// find max resolution from non HLS streams // find max resolution from non HLS streams
let bestResolution = nonHLSStreams let bestResolution = nonHLSStreams
.filter { $0.resolution <= maxResolution.value } .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 return streams.map { stream in
if stream.kind == .hls { 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 stream.format = .hls
} else if stream.kind == .stream { } else if stream.kind == .stream {
stream.format = .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 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 { static func from(resolution: String, fps: Int? = nil) -> Self {
allCases.first { $0.rawValue.contains(resolution) && $0.refreshRate == (fps ?? 30) } ?? .unknown allCases.first { $0.rawValue.contains(resolution) && $0.refreshRate == (fps ?? 30) } ?? .unknown
} }
@ -143,6 +169,7 @@ class Stream: Equatable, Hashable, Identifiable {
var encoding: String? var encoding: String?
var videoFormat: String? var videoFormat: String?
var bitrate: Int?
init( init(
instance: Instance? = nil, instance: Instance? = nil,
@ -153,7 +180,8 @@ class Stream: Equatable, Hashable, Identifiable {
resolution: Resolution? = nil, resolution: Resolution? = nil,
kind: Kind = .hls, kind: Kind = .hls,
encoding: String? = nil, encoding: String? = nil,
videoFormat: String? = nil videoFormat: String? = nil,
bitrate: Int? = nil
) { ) {
self.instance = instance self.instance = instance
self.audioAsset = audioAsset self.audioAsset = audioAsset
@ -164,6 +192,7 @@ class Stream: Equatable, Hashable, Identifiable {
self.kind = kind self.kind = kind
self.encoding = encoding self.encoding = encoding
format = .from(videoFormat ?? "") format = .from(videoFormat ?? "")
self.bitrate = bitrate
} }
var isLocal: Bool { var isLocal: Bool {

View File

@ -5,6 +5,23 @@ import SwiftUI
enum Constants { enum Constants {
static let yatteeProtocol = "yattee://" static let yatteeProtocol = "yattee://"
static let overlayAnimation = Animation.linear(duration: 0.2) 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 { static var isIPhone: Bool {
#if os(iOS) #if os(iOS)
UIDevice.current.userInterfaceIdiom == .phone 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.") Text("Formats can be reordered and will be selected in this order.")
.foregroundColor(.secondary) .foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true) .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) .foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
.padding(.top, 0.1) .padding(.top, 0.1)
@ -301,7 +305,7 @@ struct QualityProfileForm: View {
func isResolutionDisabled(_ resolution: ResolutionSetting) -> Bool { func isResolutionDisabled(_ resolution: ResolutionSetting) -> Bool {
guard backend == .appleAVPlayer else { return false } guard backend == .appleAVPlayer else { return false }
return resolution.value > .hd720p30 return resolution.value > .hd1080p60
} }
func initializeForm() { func initializeForm() {