Skip to content

Commit

Permalink
move MP4 to the bottom and hide disabled formats
Browse files Browse the repository at this point in the history
Currently, mp4 is one of the first formats chosen. But there are some issues when it comes to scrubbing/seeking. Therefore we only use mp4 as the last resort, this should give a better user experience.

sort of fixes yattee#590 & yattee#626 & yattee#487

Also when choosing the AVPlayer in the Quality settings, disabled formats are hidden.

first step to make formats sortable

hls and stream are now comparable formats

better naming for streams

move bestPlayable to PlayerBackend

Revert "move bestPlayable to PlayerBackend"

This reverts commit 7daf7fcf36cbf1b0001c18a657220777643d354a.

Reapply "move bestPlayable to PlayerBackend"

This reverts commit dd129fe5a96cc8d7cab3371aec9e8f9bfb65cfa2.

changing to wording

some finetuning
  • Loading branch information
stonerl committed Apr 29, 2024
1 parent d1cf45c commit c37029d
Show file tree
Hide file tree
Showing 10 changed files with 188 additions and 148 deletions.
10 changes: 0 additions & 10 deletions Model/Player/Backends/AVPlayerBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,16 +116,6 @@ final class AVPlayerBackend: PlayerBackend {
#endif
}

func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting) -> Stream? {
let sortedByResolution = streams
.filter { ($0.kind == .adaptive || $0.kind == .stream) && $0.resolution <= maxResolution.value }
.sorted { $0.resolution > $1.resolution }

return streams.first { $0.kind == .hls } ??
sortedByResolution.first { $0.kind == .stream } ??
sortedByResolution.first
}

func canPlay(_ stream: Stream) -> Bool {
stream.kind == .hls || stream.kind == .stream || (stream.kind == .adaptive && stream.format == .mp4)
}
Expand Down
23 changes: 0 additions & 23 deletions Model/Player/Backends/MPVBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -201,29 +201,6 @@ final class MPVBackend: PlayerBackend {

typealias AreInIncreasingOrder = (Stream, Stream) -> Bool

func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting) -> Stream? {
streams
.filter { $0.kind != .hls && $0.resolution <= maxResolution.value }
.max { lhs, rhs in
let predicates: [AreInIncreasingOrder] = [
{ $0.resolution < $1.resolution },
{ $0.format > $1.format }
]

for predicate in predicates {
if !predicate(lhs, rhs), !predicate(rhs, lhs) {
continue
}

return predicate(lhs, rhs)
}

return false
} ??
streams.first { $0.kind == .hls } ??
streams.first
}

func canPlay(_ stream: Stream) -> Bool {
stream.resolution != .unknown && stream.format != .av1
}
Expand Down
33 changes: 32 additions & 1 deletion Model/Player/Backends/PlayerBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ protocol PlayerBackend {
var videoWidth: Double? { get }
var videoHeight: Double? { get }

func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting) -> Stream?
func canPlay(_ stream: Stream) -> Bool
func canPlayAtRate(_ rate: Double) -> Bool

Expand Down Expand Up @@ -131,6 +130,38 @@ extension PlayerBackend {
}
}

func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting, formatOrder: [QualityProfile.Format]) -> Stream? {
return streams.map { stream in
if stream.kind == .hls {
stream.resolution = maxResolution.value
stream.format = .hls
} else if stream.kind == .stream {
stream.format = .stream
}
return stream
}
.filter { stream in
stream.resolution <= maxResolution.value
}
.max { lhs, rhs in
if lhs.resolution == rhs.resolution {
guard let lhsFormat = QualityProfile.Format(rawValue: lhs.format.rawValue),
let rhsFormat = QualityProfile.Format(rawValue: rhs.format.rawValue)
else {
print("Failed to extract lhsFormat or rhsFormat")
return false
}

let lhsFormatIndex = formatOrder.firstIndex(of: lhsFormat) ?? Int.max
let rhsFormatIndex = formatOrder.firstIndex(of: rhsFormat) ?? Int.max

return lhsFormatIndex > rhsFormatIndex
}

return lhs.resolution < rhs.resolution
}
}

func updateControls(completionHandler: (() -> Void)? = nil) {
print("updating controls")

Expand Down
2 changes: 1 addition & 1 deletion Model/Player/PlayerModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -673,7 +673,7 @@ final class PlayerModel: ObservableObject {
}

guard let video = currentVideo else { return }
guard let stream = avPlayerBackend.bestPlayable(availableStreams, maxResolution: .hd720p30) else { return }
guard let stream = backend.bestPlayable(availableStreams, maxResolution: .hd720p30, formatOrder: qualityProfile!.formats) else { return }

exitFullScreen()

Expand Down
4 changes: 2 additions & 2 deletions Model/Player/PlayerQueue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,12 +127,12 @@ extension PlayerModel {

if let streamPreferredForProfile = backend.bestPlayable(
availableStreams.filter { backend.canPlay($0) && profile.isPreferred($0) },
maxResolution: profile.resolution
maxResolution: profile.resolution, formatOrder: profile.formats
) {
return streamPreferredForProfile
}

return backend.bestPlayable(availableStreams.filter { backend.canPlay($0) }, maxResolution: profile.resolution)
return backend.bestPlayable(availableStreams.filter { backend.canPlay($0) }, maxResolution: profile.resolution, formatOrder: profile.formats)
}

func advanceToNextItem() {
Expand Down
21 changes: 11 additions & 10 deletions Model/QualityProfile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import Foundation

struct QualityProfile: Hashable, Identifiable, Defaults.Serializable {
static var bridge = QualityProfileBridge()
static var defaultProfile = Self(id: "default", backend: .mpv, resolution: .hd720p60, formats: [.stream])
static var defaultProfile = Self(id: "default", backend: .mpv, resolution: .hd720p60, formats: [.stream], order: Array(Format.allCases.indices))

enum Format: String, CaseIterable, Identifiable, Defaults.Serializable {
case hls
case stream
case mp4
case avc1
case mp4
case av1
case webm

Expand All @@ -23,7 +23,6 @@ struct QualityProfile: Hashable, Identifiable, Defaults.Serializable {
return "Stream"
case .webm:
return "WebM"

default:
return rawValue.uppercased()
}
Expand All @@ -35,14 +34,14 @@ struct QualityProfile: Hashable, Identifiable, Defaults.Serializable {
return nil
case .stream:
return nil
case .mp4:
return .mp4
case .webm:
return .webm
case .avc1:
return .avc1
case .mp4:
return .mp4
case .av1:
return .av1
case .webm:
return .webm
}
}
}
Expand All @@ -53,7 +52,7 @@ struct QualityProfile: Hashable, Identifiable, Defaults.Serializable {
var backend: PlayerBackendType
var resolution: ResolutionSetting
var formats: [Format]

var order: [Int]
var description: String {
if let name, !name.isEmpty { return name }
return "\(backend.label) - \(resolution.description) - \(formatsDescription)"
Expand Down Expand Up @@ -101,7 +100,8 @@ struct QualityProfileBridge: Defaults.Bridge {
"name": value.name ?? "",
"backend": value.backend.rawValue,
"resolution": value.resolution.rawValue,
"formats": value.formats.map { $0.rawValue }.joined(separator: Self.formatsSeparator)
"formats": value.formats.map { $0.rawValue }.joined(separator: Self.formatsSeparator),
"order": value.order.map { String($0) }.joined(separator: Self.formatsSeparator) // New line
]
}

Expand All @@ -116,7 +116,8 @@ struct QualityProfileBridge: Defaults.Bridge {

let name = object["name"]
let formats = (object["formats"] ?? "").components(separatedBy: Self.formatsSeparator).compactMap { QualityProfile.Format(rawValue: $0) }
let order = (object["order"] ?? "").components(separatedBy: Self.formatsSeparator).compactMap { Int($0) }

return .init(id: id, name: name, backend: backend, resolution: resolution, formats: formats)
return .init(id: id, name: name, backend: backend, resolution: resolution, formats: formats, order: order)
}
}
62 changes: 29 additions & 33 deletions Model/Stream.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class Stream: Equatable, Hashable, Identifiable {
}

enum Kind: String, Comparable {
case stream, adaptive, hls
case hls, adaptive, stream

private var sortOrder: Int {
switch self {
Expand All @@ -82,37 +82,23 @@ class Stream: Equatable, Hashable, Identifiable {
}
}

enum Format: String, Comparable {
case webm
enum Format: String {
case avc1
case av1
case mp4
case av1
case webm
case hls
case stream
case unknown

private var sortOrder: Int {
switch self {
case .mp4:
return 0
case .avc1:
return 1
case .av1:
return 2
case .webm:
return 3
case .unknown:
return 4
}
}

static func < (lhs: Self, rhs: Self) -> Bool {
lhs.sortOrder < rhs.sortOrder
}

var description: String {
switch self {
case .webm:
return "WebM"

case .hls:
return "adaptive (HLS)"
case .stream:
return "Stream"
default:
return rawValue.uppercased()
}
Expand All @@ -121,17 +107,23 @@ class Stream: Equatable, Hashable, Identifiable {
static func from(_ string: String) -> Self {
let lowercased = string.lowercased()

if lowercased.contains("webm") {
return .webm
}
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("mpeg_4") || lowercased.contains("mp4") {
return .mp4
if lowercased.contains("webm") {
return .webm
}
if lowercased.contains("stream") {
return .stream
}
if lowercased.contains("hls") {
return .hls
}
return .unknown
}
Expand Down Expand Up @@ -184,22 +176,26 @@ class Stream: Equatable, Hashable, Identifiable {

var quality: String {
guard localURL.isNil else { return "Opened File" }
return kind == .hls ? "adaptive (HLS)" : "\(resolution.name)\(kind == .stream ? " (\(kind.rawValue))" : "")"
return resolution.name
}

var shortQuality: String {
guard localURL.isNil else { return "File" }

if kind == .hls {
return "HLS"
return format.description
}

if kind == .stream {
return resolution.name
}
return resolution?.name ?? "?"
return resolutionAndFormat
}

var description: String {
guard localURL.isNil else { return resolutionAndFormat }
let instanceString = instance.isNil ? "" : " - (\(instance!.description))"
return "\(resolutionAndFormat)\(instanceString)"
return format != .hls ? "\(resolutionAndFormat)\(instanceString)" : "\(format.description)\(instanceString)"
}

var resolutionAndFormat: String {
Expand Down
10 changes: 5 additions & 5 deletions Shared/Defaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,11 @@ extension Defaults.Keys {

// MARK: GROUP - Quality

static let hd2160pMPVProfile = QualityProfile(id: "hd2160pMPVProfile", backend: .mpv, resolution: .hd2160p60, formats: QualityProfile.Format.allCases)
static let hd1080pMPVProfile = QualityProfile(id: "hd1080pMPVProfile", backend: .mpv, resolution: .hd1080p60, formats: QualityProfile.Format.allCases)
static let hd720pMPVProfile = QualityProfile(id: "hd720pMPVProfile", backend: .mpv, resolution: .hd720p60, formats: QualityProfile.Format.allCases)
static let hd720pAVPlayerProfile = QualityProfile(id: "hd720pAVPlayerProfile", backend: .appleAVPlayer, resolution: .hd720p60, formats: [.hls, .stream])
static let sd360pAVPlayerProfile = QualityProfile(id: "sd360pAVPlayerProfile", backend: .appleAVPlayer, resolution: .sd360p30, formats: [.hls, .stream])
static let hd2160pMPVProfile = QualityProfile(id: "hd2160pMPVProfile", backend: .mpv, resolution: .hd2160p60, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices))
static let hd1080pMPVProfile = QualityProfile(id: "hd1080pMPVProfile", backend: .mpv, resolution: .hd1080p60, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices))
static let hd720pMPVProfile = QualityProfile(id: "hd720pMPVProfile", backend: .mpv, resolution: .hd720p60, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices))
static let hd720pAVPlayerProfile = QualityProfile(id: "hd720pAVPlayerProfile", backend: .appleAVPlayer, resolution: .hd720p60, formats: [.hls, .stream], order: Array(QualityProfile.Format.allCases.indices))
static let sd360pAVPlayerProfile = QualityProfile(id: "sd360pAVPlayerProfile", backend: .appleAVPlayer, resolution: .sd360p30, formats: [.hls, .stream], order: Array(QualityProfile.Format.allCases.indices))

#if os(iOS)
static let qualityProfilesDefault = UIDevice.current.userInterfaceIdiom == .pad ? [
Expand Down
28 changes: 12 additions & 16 deletions Shared/Settings/MultiselectRow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,30 @@ struct MultiselectRow: View {
@State private var toggleChecked = false

var body: some View {
#if os(macOS)
Toggle(title, isOn: $toggleChecked)
.toggleStyle(.checkbox)
.onAppear {
guard !disabled else { return }
toggleChecked = selected
}
.onChange(of: toggleChecked) { new in
action(new)
}
#else
#if os(tvOS)
Button(action: { action(!selected) }) {
HStack {
Text(self.title)
Spacer()
if selected {
Image(systemName: "checkmark")
#if os(iOS)
.foregroundColor(.accentColor)
#endif
}
}
.contentShape(Rectangle())
}
.disabled(disabled)
#if !os(tvOS)
.buttonStyle(.plain)
#else
Toggle(title, isOn: $toggleChecked)
#if os(macOS)
.toggleStyle(.checkbox)
#endif
.onAppear {
guard !disabled else { return }
toggleChecked = selected
}
.onChange(of: toggleChecked) { new in
action(new)
}
#endif
}
}
Expand Down
Loading

0 comments on commit c37029d

Please sign in to comment.