mirror of
https://github.com/yattee/yattee.git
synced 2025-04-26 08:36:29 +00:00
Merge pull request #667 from stonerl/hls-set-target-quality
HLS: set target bitrate / AVPlayer: higher resolution
This commit is contained in:
commit
c6724472a6
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
@ -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() {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user