From e6e69eb757ca9c0d6c9354b79a96f8a635acd970 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Sun, 23 Nov 2025 13:30:46 +0100 Subject: [PATCH] Add optional AVPlayer support for non-streamable MP4/AVC1 formats 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 --- .../AdvancedSettingsGroupExporter.swift | 1 + .../AdvancedSettingsGroupImporter.swift | 4 +++ Model/Player/Backends/AVPlayerBackend.swift | 31 +++++++++++++++++-- Shared/Defaults.swift | 7 +++-- Shared/Settings/AdvancedSettings.swift | 22 +++++++++++++ Shared/Settings/SettingsView.swift | 2 +- 6 files changed, 61 insertions(+), 6 deletions(-) diff --git a/Model/Import Export Settings/Exporters/AdvancedSettingsGroupExporter.swift b/Model/Import Export Settings/Exporters/AdvancedSettingsGroupExporter.swift index 89cffad2..ac94a101 100644 --- a/Model/Import Export Settings/Exporters/AdvancedSettingsGroupExporter.swift +++ b/Model/Import Export Settings/Exporters/AdvancedSettingsGroupExporter.swift @@ -6,6 +6,7 @@ final class AdvancedSettingsGroupExporter: SettingsGroupExporter { [ "showPlayNowInBackendContextMenu": Defaults[.showPlayNowInBackendContextMenu], "videoLoadingRetryCount": Defaults[.videoLoadingRetryCount], + "avPlayerAllowsNonStreamableFormats": Defaults[.avPlayerAllowsNonStreamableFormats], "showMPVPlaybackStats": Defaults[.showMPVPlaybackStats], "mpvEnableLogging": Defaults[.mpvEnableLogging], "mpvCacheSecs": Defaults[.mpvCacheSecs], diff --git a/Model/Import Export Settings/Importers/AdvancedSettingsGroupImporter.swift b/Model/Import Export Settings/Importers/AdvancedSettingsGroupImporter.swift index 6c22a3f8..467b671c 100644 --- a/Model/Import Export Settings/Importers/AdvancedSettingsGroupImporter.swift +++ b/Model/Import Export Settings/Importers/AdvancedSettingsGroupImporter.swift @@ -13,6 +13,10 @@ struct AdvancedSettingsGroupImporter { Defaults[.videoLoadingRetryCount] = videoLoadingRetryCount } + if let avPlayerAllowsNonStreamableFormats = json["avPlayerAllowsNonStreamableFormats"].bool { + Defaults[.avPlayerAllowsNonStreamableFormats] = avPlayerAllowsNonStreamableFormats + } + if let showMPVPlaybackStats = json["showMPVPlaybackStats"].bool { Defaults[.showMPVPlaybackStats] = showMPVPlaybackStats } diff --git a/Model/Player/Backends/AVPlayerBackend.swift b/Model/Player/Backends/AVPlayerBackend.swift index 2b2c1df0..2548ad1c 100644 --- a/Model/Player/Backends/AVPlayerBackend.swift +++ b/Model/Player/Backends/AVPlayerBackend.swift @@ -11,6 +11,8 @@ import SwiftUI final class AVPlayerBackend: PlayerBackend { static let assetKeysToLoad = ["tracks", "playable", "duration"] + @Default(.avPlayerAllowsNonStreamableFormats) private var allowsNonStreamableFormats + private var logger = Logger(label: "avplayer-backend") var model: PlayerModel { .shared } @@ -150,7 +152,30 @@ final class AVPlayerBackend: PlayerBackend { } func canPlay(_ stream: Stream) -> Bool { - stream.kind == .hls || stream.kind == .stream + // AVPlayer has a fundamental limitation with MP4/AVC1 progressive downloads: + // If the moov atom is at the end of the file (common case), it must download + // the entire file before playback can start. MPV doesn't have this limitation. + // By default, reject non-HLS MP4/AVC1 streams unless user explicitly enables them. + + // Check if this is a non-streamable format (MP4/AVC1) that isn't HLS + let isNonStreamableFormat = stream.kind != .hls && (stream.format == .mp4 || stream.format == .avc1) + + if isNonStreamableFormat && !allowsNonStreamableFormats { + return false + } + + // If non-streamable formats are enabled, allow MP4/AVC1 adaptive streams + // but limit to 1080p maximum (higher resolutions can't be played properly) + if isNonStreamableFormat && allowsNonStreamableFormats { + let maxHeight = 1080 + if let resolution = stream.resolution, resolution.height > maxHeight { + return false + } + return true + } + + // AVPlayer works well with HLS and stream formats + return stream.kind == .hls || stream.kind == .stream } func playStream( @@ -304,12 +329,12 @@ final class AVPlayerBackend: PlayerBackend { preservingTime: Bool = false, model: PlayerModel ) { + model.logger.info("loading \(type.rawValue) track") + asset.loadValuesAsynchronously(forKeys: Self.assetKeysToLoad) { [weak self] in guard let self else { return } - model.logger.info("loading \(type.rawValue) track") - let assetTracks = asset.tracks(withMediaType: type) guard let compositionTrack = self.composition.addMutableTrack( diff --git a/Shared/Defaults.swift b/Shared/Defaults.swift index a33ee284..79905aa4 100644 --- a/Shared/Defaults.swift +++ b/Shared/Defaults.swift @@ -192,7 +192,8 @@ extension Defaults.Keys { hd1080p60MPVProfile, hd1080pMPVProfile, hd720p60MPVProfile, - hd720pMPVProfile + hd720pMPVProfile, + hd720pAVPlayerProfile ] static let batteryCellularProfileDefault = hd720pMPVProfile.id @@ -208,7 +209,8 @@ extension Defaults.Keys { hd1080pMPVProfile, hd720p60MPVProfile, hd720pMPVProfile, - sd360pMPVProfile + sd360pMPVProfile, + hd720pAVPlayerProfile ] static let batteryCellularProfileDefault = sd360pMPVProfile.id @@ -361,6 +363,7 @@ extension Defaults.Keys { static let showPlayNowInBackendContextMenu = Key("showPlayNowInBackendContextMenu", default: false) static let videoLoadingRetryCount = Key("videoLoadingRetryCount", default: 10) + static let avPlayerAllowsNonStreamableFormats = Key("avPlayerAllowsNonStreamableFormats", default: false) static let showMPVPlaybackStats = Key("showMPVPlaybackStats", default: false) static let mpvEnableLogging = Key("mpvEnableLogging", default: false) diff --git a/Shared/Settings/AdvancedSettings.swift b/Shared/Settings/AdvancedSettings.swift index 6d67ecb6..c199019d 100644 --- a/Shared/Settings/AdvancedSettings.swift +++ b/Shared/Settings/AdvancedSettings.swift @@ -16,10 +16,12 @@ struct AdvancedSettings: View { @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 { @@ -73,6 +75,10 @@ struct AdvancedSettings: View { videoLoadingRetryCountField } + Section(header: SettingsHeader(text: "AVPlayer"), footer: avPlayerNonStreamableFormatsFooter) { + avPlayerAllowsNonStreamableFormatsToggle + } + Section(header: SettingsHeader(text: "MPV"), footer: mpvFooter) { showMPVPlaybackStatsToggle #if !os(tvOS) @@ -370,6 +376,22 @@ struct AdvancedSettings: 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 { diff --git a/Shared/Settings/SettingsView.swift b/Shared/Settings/SettingsView.swift index c1d0db0c..48801a53 100644 --- a/Shared/Settings/SettingsView.swift +++ b/Shared/Settings/SettingsView.swift @@ -360,7 +360,7 @@ struct SettingsView: View { case .locations: return 600 case .advanced: - return 630 + return 700 case .importExport: return 580 case .help: