mirror of
				https://github.com/yattee/yattee.git
				synced 2025-11-04 06:32:03 +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),
 | 
			
		||||
                kind: .adaptive,
 | 
			
		||||
                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 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 {
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -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()
 | 
			
		||||
                    }
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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 {
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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() {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user