mirror of
				https://github.com/yattee/yattee.git
				synced 2025-10-31 12:41:57 +00:00 
			
		
		
		
	Merge pull request #667 from stonerl/hls-set-target-quality
HLS: set target bitrate / AVPlayer: higher resolution
This commit is contained in:
		| @@ -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() { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Arkadiusz Fal
					Arkadiusz Fal