mirror of
https://github.com/yattee/yattee.git
synced 2025-11-24 18:28:20 +00:00
AVPlayer has a fundamental limitation with MP4/AVC1 progressive downloads where the moov atom position affects playback start time. When moov is at the end of the file, AVPlayer must download the entire file before starting playback. MPV doesn't have this limitation. This commit adds an advanced setting to optionally enable these formats in AVPlayer with appropriate warnings: - Added new setting: "Enable non-streamable formats (MP4/AVC1)" - Default: disabled (formats hidden, MPV handles them) - When enabled: MP4/AVC1 formats up to 1080p appear in AVPlayer quality selector - Resolution limit: 1080p maximum (higher resolutions can't be played properly) - Clear warning about slow loading and 1080p limitation - Automatic stream refresh when setting is toggled - Full import/export support for the setting
403 lines
15 KiB
Swift
403 lines
15 KiB
Swift
import Defaults
|
||
import SwiftUI
|
||
|
||
struct AdvancedSettings: View {
|
||
@Default(.showMPVPlaybackStats) private var showMPVPlaybackStats
|
||
@Default(.mpvCacheSecs) private var mpvCacheSecs
|
||
@Default(.mpvCachePauseWait) private var mpvCachePauseWait
|
||
@Default(.mpvCachePauseInital) private var mpvCachePauseInital
|
||
@Default(.mpvDeinterlace) private var mpvDeinterlace
|
||
@Default(.mpvEnableLogging) private var mpvEnableLogging
|
||
@Default(.mpvHWdec) private var mpvHWdec
|
||
@Default(.mpvDemuxerLavfProbeInfo) private var mpvDemuxerLavfProbeInfo
|
||
@Default(.mpvInitialAudioSync) private var mpvInitialAudioSync
|
||
@Default(.mpvSetRefreshToContentFPS) private var mpvSetRefreshToContentFPS
|
||
@Default(.showCacheStatus) private var showCacheStatus
|
||
@Default(.feedCacheSize) private var feedCacheSize
|
||
@Default(.showPlayNowInBackendContextMenu) private var showPlayNowInBackendContextMenu
|
||
@Default(.videoLoadingRetryCount) private var videoLoadingRetryCount
|
||
@Default(.avPlayerAllowsNonStreamableFormats) private var avPlayerAllowsNonStreamableFormats
|
||
|
||
@State private var filesToShare = [MPVClient.logFile]
|
||
@State private var presentingShareSheet = false
|
||
|
||
@ObservedObject private var player = PlayerModel.shared
|
||
private var settings = SettingsModel.shared
|
||
|
||
var body: some View {
|
||
VStack(alignment: .leading) {
|
||
#if os(macOS)
|
||
advancedSettings
|
||
Spacer()
|
||
#else
|
||
List {
|
||
advancedSettings
|
||
}
|
||
#if os(tvOS)
|
||
.listStyle(.plain)
|
||
#elseif os(iOS)
|
||
.sheet(isPresented: $presentingShareSheet) {
|
||
ShareSheet(activityItems: filesToShare)
|
||
.id("logs-\(filesToShare.count)")
|
||
}
|
||
.listStyle(.insetGrouped)
|
||
#endif
|
||
#endif
|
||
}
|
||
#if os(tvOS)
|
||
.buttonStyle(.plain)
|
||
.toggleStyle(TVOSPlainToggleStyle())
|
||
.frame(maxWidth: 1000)
|
||
#endif
|
||
.navigationTitle("Advanced")
|
||
}
|
||
|
||
var logButton: some View {
|
||
Button {
|
||
#if os(macOS)
|
||
NSWorkspace.shared.selectFile(MPVClient.logFile.path, inFileViewerRootedAtPath: YatteeApp.logsDirectory.path)
|
||
#else
|
||
presentingShareSheet = true
|
||
#endif
|
||
} label: {
|
||
#if os(macOS)
|
||
let labelText = "Open logs in Finder".localized()
|
||
#else
|
||
let labelText = "Share Logs...".localized()
|
||
#endif
|
||
Text(labelText)
|
||
}
|
||
}
|
||
|
||
@ViewBuilder var advancedSettings: some View {
|
||
Section(header: SettingsHeader(text: "Advanced")) {
|
||
showPlayNowInBackendButtonsToggle
|
||
videoLoadingRetryCountField
|
||
}
|
||
|
||
Section(header: SettingsHeader(text: "AVPlayer"), footer: avPlayerNonStreamableFormatsFooter) {
|
||
avPlayerAllowsNonStreamableFormatsToggle
|
||
}
|
||
|
||
Section(header: SettingsHeader(text: "MPV"), footer: mpvFooter) {
|
||
showMPVPlaybackStatsToggle
|
||
#if !os(tvOS)
|
||
mpvEnableLoggingToggle
|
||
#endif
|
||
|
||
Toggle(isOn: $mpvCachePauseInital) {
|
||
HStack {
|
||
Text("cache-pause-initial")
|
||
#if !os(tvOS)
|
||
Image(systemName: "link")
|
||
.font(.footnote)
|
||
#if os(iOS)
|
||
.accessibilityAddTraits([.isButton, .isLink])
|
||
.onTapGesture {
|
||
UIApplication.shared.open(URL(string: "https://mpv.io/manual/stable/#options-cache-pause-initial")!)
|
||
}
|
||
#elseif os(macOS)
|
||
.accessibilityAddTraits([.isButton, .isLink])
|
||
.onTapGesture {
|
||
NSWorkspace.shared.open(URL(string: "https://mpv.io/manual/stable/#options-cache-pause-initial")!)
|
||
}
|
||
.onHover(perform: onHover(_:))
|
||
#endif
|
||
#endif
|
||
}
|
||
}
|
||
|
||
HStack {
|
||
Text("cache-secs")
|
||
#if !os(tvOS)
|
||
Image(systemName: "link")
|
||
.font(.footnote)
|
||
#if os(iOS)
|
||
.accessibilityAddTraits([.isButton, .isLink])
|
||
.onTapGesture {
|
||
UIApplication.shared.open(URL(string: "https://mpv.io/manual/stable/#options-cache-secs")!)
|
||
}
|
||
#elseif os(macOS)
|
||
.accessibilityAddTraits([.isButton, .isLink])
|
||
.onTapGesture {
|
||
NSWorkspace.shared.open(URL(string: "https://mpv.io/manual/stable/#options-cache-secs")!)
|
||
}
|
||
.onHover(perform: onHover(_:))
|
||
#endif
|
||
|
||
#endif
|
||
TextField("cache-secs", text: $mpvCacheSecs)
|
||
#if !os(macOS)
|
||
.keyboardType(.numberPad)
|
||
#endif
|
||
}
|
||
.multilineTextAlignment(.trailing)
|
||
|
||
HStack {
|
||
Group {
|
||
Text("cache-pause-wait")
|
||
#if !os(tvOS)
|
||
Image(systemName: "link")
|
||
.font(.footnote)
|
||
#if os(iOS)
|
||
.accessibilityAddTraits([.isButton, .isLink])
|
||
.onTapGesture {
|
||
UIApplication.shared.open(URL(string: "https://mpv.io/manual/stable/#options-cache-pause-wait")!)
|
||
}
|
||
#elseif os(macOS)
|
||
.accessibilityAddTraits([.isButton, .isLink])
|
||
.onTapGesture {
|
||
NSWorkspace.shared.open(URL(string: "https://mpv.io/manual/stable/#options-cache-pause-wait")!)
|
||
}
|
||
.onHover(perform: onHover(_:))
|
||
#endif
|
||
#endif
|
||
}.frame(minWidth: 140, alignment: .leading)
|
||
|
||
TextField("cache-pause-wait", text: $mpvCachePauseWait)
|
||
#if !os(macOS)
|
||
.keyboardType(.numberPad)
|
||
#endif
|
||
}
|
||
.multilineTextAlignment(.trailing)
|
||
|
||
Toggle(isOn: $mpvDeinterlace) {
|
||
HStack {
|
||
Text("deinterlace")
|
||
#if !os(tvOS)
|
||
Image(systemName: "link")
|
||
.font(.footnote)
|
||
#if os(iOS)
|
||
.accessibilityAddTraits([.isButton, .isLink])
|
||
.onTapGesture {
|
||
UIApplication.shared.open(URL(string: "https://mpv.io/manual/stable/#options-deinterlace")!)
|
||
}
|
||
#elseif os(macOS)
|
||
.accessibilityAddTraits([.isButton, .isLink])
|
||
.onTapGesture {
|
||
NSWorkspace.shared.open(URL(string: "https://mpv.io/manual/stable/#options-deinterlace")!)
|
||
}
|
||
.onHover(perform: onHover(_:))
|
||
#endif
|
||
#endif
|
||
}
|
||
}
|
||
|
||
Toggle(isOn: $mpvInitialAudioSync) {
|
||
HStack {
|
||
Text("initial-audio-sync")
|
||
#if !os(tvOS)
|
||
Image(systemName: "link")
|
||
.font(.footnote)
|
||
#if os(iOS)
|
||
.accessibilityAddTraits([.isButton, .isLink])
|
||
.onTapGesture {
|
||
UIApplication.shared.open(URL(string: "https://mpv.io/manual/stable/#options-initial-audio-sync")!)
|
||
}
|
||
#elseif os(macOS)
|
||
.accessibilityAddTraits([.isButton, .isLink])
|
||
.onTapGesture {
|
||
NSWorkspace.shared.open(URL(string: "https://mpv.io/manual/stable/#options-initial-audio-sync")!)
|
||
}
|
||
.onHover(perform: onHover(_:))
|
||
#endif
|
||
#endif
|
||
}
|
||
}
|
||
|
||
HStack {
|
||
Text("hwdec")
|
||
|
||
#if !os(tvOS)
|
||
Image(systemName: "link")
|
||
.font(.footnote)
|
||
#if os(iOS)
|
||
.accessibilityAddTraits([.isButton, .isLink])
|
||
.onTapGesture {
|
||
UIApplication.shared.open(URL(string: "https://mpv.io/manual/stable/#options-hwdec")!)
|
||
}
|
||
#elseif os(macOS)
|
||
.accessibilityAddTraits([.isButton, .isLink])
|
||
.onTapGesture {
|
||
NSWorkspace.shared.open(URL(string: "https://mpv.io/manual/stable/#options-hwdec")!)
|
||
}
|
||
.onHover(perform: onHover(_:))
|
||
#endif
|
||
#endif
|
||
|
||
Picker("", selection: $mpvHWdec) {
|
||
ForEach(["auto", "auto-safe", "auto-copy"], id: \.self) {
|
||
Text($0)
|
||
}
|
||
}
|
||
#if !os(tvOS)
|
||
.pickerStyle(MenuPickerStyle())
|
||
#endif
|
||
}
|
||
|
||
HStack {
|
||
Text("demuxer-lavf-probe-info")
|
||
|
||
#if !os(tvOS)
|
||
Image(systemName: "link")
|
||
.font(.footnote)
|
||
#if os(iOS)
|
||
.accessibilityAddTraits([.isButton, .isLink])
|
||
.onTapGesture {
|
||
UIApplication.shared.open(URL(string: "https://mpv.io/manual/stable/#options-demuxer-lavf-probe-info")!)
|
||
}
|
||
#elseif os(macOS)
|
||
.accessibilityAddTraits([.isButton, .isLink])
|
||
.onTapGesture {
|
||
NSWorkspace.shared.open(URL(string: "https://mpv.io/manual/stable/#options-demuxer-lavf-probe-info")!)
|
||
}
|
||
.onHover(perform: onHover(_:))
|
||
#endif
|
||
#endif
|
||
|
||
Picker("", selection: $mpvDemuxerLavfProbeInfo) {
|
||
ForEach(["yes", "no", "auto", "nostreams"], id: \.self) {
|
||
Text($0)
|
||
}
|
||
}
|
||
#if !os(tvOS)
|
||
.pickerStyle(MenuPickerStyle())
|
||
#endif
|
||
}
|
||
|
||
Toggle(isOn: $mpvSetRefreshToContentFPS) {
|
||
HStack {
|
||
Text("Sync refresh rate with content FPS – EXPERIMENTAL")
|
||
}
|
||
}
|
||
|
||
if mpvEnableLogging {
|
||
logButton
|
||
}
|
||
}
|
||
|
||
Section(header: SettingsHeader(text: "Cache"), footer: cacheSize) {
|
||
showCacheStatusToggle
|
||
feedCacheSizeTextField
|
||
clearCacheButton
|
||
}
|
||
}
|
||
|
||
@ViewBuilder var mpvFooter: some View {
|
||
let url = "https://mpv.io/manual/stable/"
|
||
|
||
VStack(alignment: .leading) {
|
||
Text("Restart the app to apply the settings above.")
|
||
.padding(.bottom, 1)
|
||
VStack(alignment: .leading, spacing: 2) {
|
||
#if os(tvOS)
|
||
Text("More info can be found in MPV reference manual:")
|
||
Text(url)
|
||
#else
|
||
Text("Further information can be found in the ")
|
||
+ Text("MPV reference manual").underline().bold()
|
||
+ Text(" by clicking on the link icon next to the option.")
|
||
#endif
|
||
}
|
||
}
|
||
.foregroundColor(.secondary)
|
||
}
|
||
|
||
var showPlayNowInBackendButtonsToggle: some View {
|
||
Toggle("Show video context menu options to force selected backend", isOn: $showPlayNowInBackendContextMenu)
|
||
}
|
||
|
||
private var videoLoadingRetryCountField: some View {
|
||
HStack {
|
||
Text("Maximum retries for video loading")
|
||
.frame(minWidth: 200, alignment: .leading)
|
||
.multilineTextAlignment(.leading)
|
||
TextField("Limit", value: $videoLoadingRetryCount, formatter: NumberFormatter())
|
||
.multilineTextAlignment(.trailing)
|
||
#if !os(macOS)
|
||
.keyboardType(.numberPad)
|
||
#endif
|
||
}
|
||
}
|
||
|
||
var showMPVPlaybackStatsToggle: some View {
|
||
Toggle("Show playback statistics", isOn: $showMPVPlaybackStats)
|
||
}
|
||
|
||
var mpvEnableLoggingToggle: some View {
|
||
Toggle("Enable logging", isOn: $mpvEnableLogging)
|
||
}
|
||
|
||
#if os(macOS)
|
||
private func onHover(_ inside: Bool) {
|
||
if inside {
|
||
NSCursor.pointingHand.push()
|
||
} else {
|
||
NSCursor.pop()
|
||
}
|
||
}
|
||
#endif
|
||
|
||
private var feedCacheSizeTextField: some View {
|
||
HStack {
|
||
Text("Maximum feed items")
|
||
.frame(minWidth: 200, alignment: .leading)
|
||
.multilineTextAlignment(.leading)
|
||
TextField("Limit", text: $feedCacheSize)
|
||
.multilineTextAlignment(.trailing)
|
||
#if !os(macOS)
|
||
.keyboardType(.numberPad)
|
||
#endif
|
||
}
|
||
}
|
||
|
||
private var showCacheStatusToggle: some View {
|
||
Toggle("Show cache status", isOn: $showCacheStatus)
|
||
}
|
||
|
||
private var clearCacheButton: some View {
|
||
Button {
|
||
settings.presentAlert(
|
||
Alert(
|
||
title: Text(
|
||
"Are you sure you want to clear cache?"
|
||
),
|
||
primaryButton: .destructive(Text("Clear"), action: BaseCacheModel.shared.clear),
|
||
secondaryButton: .cancel()
|
||
)
|
||
)
|
||
} label: {
|
||
Text("Clear all")
|
||
.foregroundColor(.red)
|
||
}
|
||
}
|
||
|
||
var cacheSize: some View {
|
||
Text(String(format: "Total size: %@".localized(), BaseCacheModel.shared.totalSizeFormatted))
|
||
.foregroundColor(.secondary)
|
||
}
|
||
|
||
var avPlayerAllowsNonStreamableFormatsToggle: some View {
|
||
Toggle("Enable non-streamable formats (MP4/AVC1)", isOn: $avPlayerAllowsNonStreamableFormats)
|
||
.onChange(of: avPlayerAllowsNonStreamableFormats) { _ in
|
||
// Trigger refresh of available streams when setting changes
|
||
if let video = player.currentVideo {
|
||
player.loadAvailableStreams(video)
|
||
}
|
||
}
|
||
}
|
||
|
||
@ViewBuilder var avPlayerNonStreamableFormatsFooter: some View {
|
||
Text("Non-streamable video formats (MP4/AVC1) may take a long time to start playback with AVPlayer. These formats require downloading metadata before playback can begin. Limited to 1080p maximum. For better performance with these formats, use MPV backend instead.")
|
||
.foregroundColor(.secondary)
|
||
.fixedSize(horizontal: false, vertical: true)
|
||
}
|
||
}
|
||
|
||
struct AdvancedSettings_Previews: PreviewProvider {
|
||
static var previews: some View {
|
||
AdvancedSettings()
|
||
.injectFixtureEnvironmentObjects()
|
||
}
|
||
}
|