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),
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

@ -307,6 +307,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

@ -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
}
} else {
@ -313,7 +313,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() {