mirror of
https://github.com/yattee/yattee.git
synced 2024-12-22 21:43:41 +00:00
a45522f710
Invidious now reports the actual resolution and doesn’t hardcode them anymore. See: https://github.com/iv-org/invidious/pull/4586 - Extended the list of possible resolutions in the StreamModel - trigger videoLoadFailureHandler if no streams are available - more logging for backend.bestPlayable Signed-off-by: Toni Förster <toni.foerster@gmail.com>
481 lines
13 KiB
Swift
481 lines
13 KiB
Swift
import AVFoundation
|
|
import Defaults
|
|
import Foundation
|
|
|
|
// swiftlint:disable:next final_class
|
|
class Stream: Equatable, Hashable, Identifiable {
|
|
enum Resolution: String, CaseIterable, Comparable, Defaults.Serializable {
|
|
// Some 16:19 and 16:10 resolutions are also used in 2:1 videos
|
|
|
|
// 8K UHD (16:9) Resolutions
|
|
case hd4320p60
|
|
case hd4320p50
|
|
case hd4320p48
|
|
case hd4320p30
|
|
case hd4320p25
|
|
case hd4320p24
|
|
|
|
// 5K (16:9) Resolutions
|
|
case hd2560p60
|
|
case hd2560p50
|
|
case hd2560p48
|
|
case hd2560p30
|
|
case hd2560p25
|
|
case hd2560p24
|
|
|
|
// 2:1 Aspect Ratio (Univisium) Resolutions
|
|
case hd2880p60
|
|
case hd2880p50
|
|
case hd2880p48
|
|
case hd2880p30
|
|
case hd2880p25
|
|
case hd2880p24
|
|
|
|
// 16:10 Resolutions
|
|
case hd2400p60
|
|
case hd2400p50
|
|
case hd2400p48
|
|
case hd2400p30
|
|
case hd2400p25
|
|
case hd2400p24
|
|
|
|
// 16:9 Resolutions
|
|
case hd2160p60
|
|
case hd2160p50
|
|
case hd2160p48
|
|
case hd2160p30
|
|
case hd2160p25
|
|
case hd2160p24
|
|
|
|
// 16:10 Resolutions
|
|
case hd1600p60
|
|
case hd1600p50
|
|
case hd1600p48
|
|
case hd1600p30
|
|
case hd1600p25
|
|
case hd1600p24
|
|
|
|
// 16:9 Resolutions
|
|
case hd1440p60
|
|
case hd1440p50
|
|
case hd1440p48
|
|
case hd1440p30
|
|
case hd1440p25
|
|
case hd1440p24
|
|
|
|
// 16:10 Resolutions
|
|
case hd1280p60
|
|
case hd1280p50
|
|
case hd1280p48
|
|
case hd1280p30
|
|
case hd1280p25
|
|
case hd1280p24
|
|
|
|
// 16:10 Resolutions
|
|
case hd1200p60
|
|
case hd1200p50
|
|
case hd1200p48
|
|
case hd1200p30
|
|
case hd1200p25
|
|
case hd1200p24
|
|
|
|
// 16:9 Resolutions
|
|
case hd1080p60
|
|
case hd1080p50
|
|
case hd1080p48
|
|
case hd1080p30
|
|
case hd1080p25
|
|
case hd1080p24
|
|
|
|
// 16:10 Resolutions
|
|
case hd1050p60
|
|
case hd1050p50
|
|
case hd1050p48
|
|
case hd1050p30
|
|
case hd1050p25
|
|
case hd1050p24
|
|
|
|
// 16:9 Resolutions
|
|
case hd960p60
|
|
case hd960p50
|
|
case hd960p48
|
|
case hd960p30
|
|
case hd960p25
|
|
case hd960p24
|
|
|
|
// 16:10 Resolutions
|
|
case hd900p60
|
|
case hd900p50
|
|
case hd900p48
|
|
case hd900p30
|
|
case hd900p25
|
|
case hd900p24
|
|
|
|
// 16:10 Resolutions
|
|
case hd800p60
|
|
case hd800p50
|
|
case hd800p48
|
|
case hd800p30
|
|
case hd800p25
|
|
case hd800p24
|
|
|
|
// 16:9 Resolutions
|
|
case hd720p60
|
|
case hd720p50
|
|
case hd720p48
|
|
case hd720p30
|
|
case hd720p25
|
|
case hd720p24
|
|
|
|
// Standard Definition (SD) Resolutions
|
|
case sd854p30
|
|
case sd854p25
|
|
case sd768p30
|
|
case sd768p25
|
|
case sd640p30
|
|
case sd640p25
|
|
case sd480p30
|
|
case sd480p25
|
|
|
|
case sd428p30
|
|
case sd428p25
|
|
case sd360p30
|
|
case sd360p25
|
|
case sd320p30
|
|
case sd320p25
|
|
case sd240p30
|
|
case sd240p25
|
|
case sd214p30
|
|
case sd214p25
|
|
case sd144p30
|
|
case sd144p25
|
|
case sd128p30
|
|
case sd128p25
|
|
|
|
case unknown
|
|
|
|
var name: String {
|
|
"\(height)p\(refreshRate != -1 && refreshRate != 30 ? ", \(refreshRate) fps" : "")"
|
|
}
|
|
|
|
var height: Int {
|
|
if self == .unknown {
|
|
return -1
|
|
}
|
|
|
|
let resolutionPart = rawValue.components(separatedBy: "p").first!
|
|
return Int(resolutionPart.components(separatedBy: CharacterSet.decimalDigits.inverted).joined())!
|
|
}
|
|
|
|
var refreshRate: Int {
|
|
if self == .unknown {
|
|
return -1
|
|
}
|
|
|
|
let refreshRatePart = rawValue.components(separatedBy: "p")[1]
|
|
|
|
if refreshRatePart.isEmpty {
|
|
return 30
|
|
}
|
|
|
|
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 {
|
|
// 8K UHD (16:9) Resolutions
|
|
case .hd4320p60, .hd4320p50, .hd4320p48, .hd4320p30, .hd4320p25, .hd4320p24:
|
|
return 85_000_000 // 85 Mbit/s
|
|
|
|
// 5K (16:9) Resolutions
|
|
case .hd2880p60, .hd2880p50, .hd2880p48, .hd2880p30, .hd2880p25, .hd2880p24:
|
|
return 45_000_000 // 45 Mbit/s
|
|
|
|
// 2:1 Aspect Ratio (Univisium) Resolutions
|
|
case .hd2560p60, .hd2560p50, .hd2560p48, .hd2560p30, .hd2560p25, .hd2560p24:
|
|
return 30_000_000 // 30 Mbit/s
|
|
|
|
// 16:10 Resolutions
|
|
case .hd2400p60, .hd2400p50, .hd2400p48, .hd2400p30, .hd2400p25, .hd2400p24:
|
|
return 35_000_000 // 35 Mbit/s
|
|
|
|
// 4K UHD (16:9) Resolutions
|
|
case .hd2160p60, .hd2160p50, .hd2160p48, .hd2160p30, .hd2160p25, .hd2160p24:
|
|
return 56_000_000 // 56 Mbit/s
|
|
|
|
// 16:10 Resolutions
|
|
case .hd1600p60, .hd1600p50, .hd1600p48, .hd1600p30, .hd1600p25, .hd1600p24:
|
|
return 20_000_000 // 20 Mbit/s
|
|
|
|
// 1440p (16:9) Resolutions
|
|
case .hd1440p60, .hd1440p50, .hd1440p48, .hd1440p30, .hd1440p25, .hd1440p24:
|
|
return 24_000_000 // 24 Mbit/s
|
|
|
|
// 1280p (16:10) Resolutions
|
|
case .hd1280p60, .hd1280p50, .hd1280p48, .hd1280p30, .hd1280p25, .hd1280p24:
|
|
return 15_000_000 // 15 Mbit/s
|
|
|
|
// 1200p (16:10) Resolutions
|
|
case .hd1200p60, .hd1200p50, .hd1200p48, .hd1200p30, .hd1200p25, .hd1200p24:
|
|
return 18_000_000 // 18 Mbit/s
|
|
|
|
// 1080p (16:9) Resolutions
|
|
case .hd1080p60, .hd1080p50, .hd1080p48, .hd1080p30, .hd1080p25, .hd1080p24:
|
|
return 12_000_000 // 12 Mbit/s
|
|
|
|
// 1050p (16:10) Resolutions
|
|
case .hd1050p60, .hd1050p50, .hd1050p48, .hd1050p30, .hd1050p25, .hd1050p24:
|
|
return 10_000_000 // 10 Mbit/s
|
|
|
|
// 960p Resolutions
|
|
case .hd960p60, .hd960p50, .hd960p48, .hd960p30, .hd960p25, .hd960p24:
|
|
return 8_000_000 // 8 Mbit/s
|
|
|
|
// 900p (16:10) Resolutions
|
|
case .hd900p60, .hd900p50, .hd900p48, .hd900p30, .hd900p25, .hd900p24:
|
|
return 7_000_000 // 7 Mbit/s
|
|
|
|
// 800p (16:10) Resolutions
|
|
case .hd800p60, .hd800p50, .hd800p48, .hd800p30, .hd800p25, .hd800p24:
|
|
return 6_000_000 // 6 Mbit/s
|
|
|
|
// 720p (16:9) Resolutions
|
|
case .hd720p60, .hd720p50, .hd720p48, .hd720p30, .hd720p25, .hd720p24:
|
|
return 9_500_000 // 9.5 Mbit/s
|
|
|
|
// Standard Definition (SD) Resolutions
|
|
case .sd854p30, .sd854p25, .sd768p30, .sd768p25, .sd640p30, .sd640p25:
|
|
return 4_000_000 // 4 Mbit/s
|
|
|
|
case .sd480p30, .sd480p25:
|
|
return 2_500_000 // 2.5 Mbit/s
|
|
|
|
case .sd428p30, .sd428p25:
|
|
return 2_000_000 // 2 Mbit/s
|
|
|
|
case .sd360p30, .sd360p25:
|
|
return 1_500_000 // 1.5 Mbit/s
|
|
|
|
case .sd320p30, .sd320p25:
|
|
return 1_200_000 // 1.2 Mbit/s
|
|
|
|
case .sd240p30, .sd240p25:
|
|
return 1_000_000 // 1 Mbit/s
|
|
|
|
case .sd214p30, .sd214p25:
|
|
return 800_000 // 0.8 Mbit/s
|
|
|
|
case .sd144p30, .sd144p25:
|
|
return 600_000 // 0.6 Mbit/s
|
|
|
|
case .sd128p30, .sd128p25:
|
|
return 400_000 // 0.4 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
|
|
}
|
|
|
|
static func < (lhs: Self, rhs: Self) -> Bool {
|
|
lhs.height == rhs.height ? (lhs.refreshRate < rhs.refreshRate) : (lhs.height < rhs.height)
|
|
}
|
|
}
|
|
|
|
enum Kind: String, Comparable {
|
|
case hls, adaptive, stream
|
|
|
|
private var sortOrder: Int {
|
|
switch self {
|
|
case .hls:
|
|
return 0
|
|
case .stream:
|
|
return 1
|
|
case .adaptive:
|
|
return 2
|
|
}
|
|
}
|
|
|
|
static func < (lhs: Self, rhs: Self) -> Bool {
|
|
lhs.sortOrder < rhs.sortOrder
|
|
}
|
|
}
|
|
|
|
enum Format: String {
|
|
case avc1
|
|
case mp4
|
|
case av1
|
|
case webm
|
|
case hls
|
|
case stream
|
|
case unknown
|
|
|
|
var description: String {
|
|
switch self {
|
|
case .webm:
|
|
return "WebM"
|
|
case .hls:
|
|
return "adaptive (HLS)"
|
|
case .stream:
|
|
return "Stream"
|
|
default:
|
|
return rawValue.uppercased()
|
|
}
|
|
}
|
|
|
|
static func from(_ string: String) -> Self {
|
|
let lowercased = string.lowercased()
|
|
|
|
if lowercased.contains("avc1") {
|
|
return .avc1
|
|
}
|
|
if lowercased.contains("mpeg_4") || lowercased.contains("mp4") {
|
|
return .mp4
|
|
}
|
|
if lowercased.contains("av01") {
|
|
return .av1
|
|
}
|
|
if lowercased.contains("webm") {
|
|
return .webm
|
|
}
|
|
if lowercased.contains("stream") {
|
|
return .stream
|
|
}
|
|
if lowercased.contains("hls") {
|
|
return .hls
|
|
}
|
|
return .unknown
|
|
}
|
|
}
|
|
|
|
let id = UUID()
|
|
|
|
var instance: Instance!
|
|
var audioAsset: AVURLAsset!
|
|
var videoAsset: AVURLAsset!
|
|
var hlsURL: URL!
|
|
var localURL: URL!
|
|
|
|
var resolution: Resolution!
|
|
var kind: Kind!
|
|
var format: Format!
|
|
|
|
var encoding: String?
|
|
var videoFormat: String?
|
|
var bitrate: Int?
|
|
var requestRange: String?
|
|
|
|
init(
|
|
instance: Instance? = nil,
|
|
audioAsset: AVURLAsset? = nil,
|
|
videoAsset: AVURLAsset? = nil,
|
|
hlsURL: URL? = nil,
|
|
localURL: URL? = nil,
|
|
resolution: Resolution? = nil,
|
|
kind: Kind = .hls,
|
|
encoding: String? = nil,
|
|
videoFormat: String? = nil,
|
|
bitrate: Int? = nil,
|
|
requestRange: String? = nil
|
|
) {
|
|
self.instance = instance
|
|
self.audioAsset = audioAsset
|
|
self.videoAsset = videoAsset
|
|
self.hlsURL = hlsURL
|
|
self.localURL = localURL
|
|
self.resolution = resolution
|
|
self.kind = kind
|
|
self.encoding = encoding
|
|
format = .from(videoFormat ?? "")
|
|
self.bitrate = bitrate
|
|
self.requestRange = requestRange
|
|
}
|
|
|
|
var isLocal: Bool {
|
|
localURL != nil
|
|
}
|
|
|
|
var isHLS: Bool {
|
|
hlsURL != nil
|
|
}
|
|
|
|
var quality: String {
|
|
guard localURL.isNil else { return "Opened File" }
|
|
|
|
if kind == .hls {
|
|
return "adaptive (HLS)"
|
|
}
|
|
|
|
return resolution.name
|
|
}
|
|
|
|
var shortQuality: String {
|
|
guard localURL.isNil else { return "File" }
|
|
|
|
if kind == .hls {
|
|
return "adaptive (HLS)"
|
|
}
|
|
|
|
if kind == .stream {
|
|
return resolution.name
|
|
}
|
|
return resolutionAndFormat
|
|
}
|
|
|
|
var description: String {
|
|
guard localURL.isNil else { return resolutionAndFormat }
|
|
let instanceString = instance.isNil ? "" : " - (\(instance!.description))"
|
|
return format != .hls ? "\(resolutionAndFormat)\(instanceString)" : "adaptive (HLS)\(instanceString)"
|
|
}
|
|
|
|
var resolutionAndFormat: String {
|
|
let formatString = format == .unknown ? "" : " (\(format.description))"
|
|
return "\(quality)\(formatString)"
|
|
}
|
|
|
|
var assets: [AVURLAsset] {
|
|
[audioAsset, videoAsset]
|
|
}
|
|
|
|
var videoAssetContainsAudio: Bool {
|
|
assets.dropFirst().allSatisfy { $0.url == assets.first!.url }
|
|
}
|
|
|
|
var singleAssetURL: URL? {
|
|
guard localURL.isNil else {
|
|
return URLBookmarkModel.shared.loadBookmark(localURL) ?? localURL
|
|
}
|
|
|
|
if kind == .hls {
|
|
return hlsURL
|
|
}
|
|
if videoAssetContainsAudio {
|
|
return videoAsset.url
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
static func == (lhs: Stream, rhs: Stream) -> Bool {
|
|
lhs.id == rhs.id
|
|
}
|
|
|
|
func hash(into hasher: inout Hasher) {
|
|
if let url = videoAsset?.url {
|
|
hasher.combine(url)
|
|
}
|
|
if let url = audioAsset?.url {
|
|
hasher.combine(url)
|
|
}
|
|
if let url = hlsURL {
|
|
hasher.combine(url)
|
|
}
|
|
}
|
|
}
|