From ba62129414b78f60d0e229b12d2ed3df3ac37c75 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Thu, 28 May 2026 00:33:20 +0200 Subject: [PATCH] fix(playback): support Apple DV conversion --- lib/mpv/player/player_native.dart | 64 ++++++++++++- lib/screens/video_player_screen.dart | 5 + .../sheets/video_settings_sheet.dart | 10 +- .../apple/MpvPlayer/MpvPlayerCoreBase.swift | 95 +++++++++++++++++++ 4 files changed, 171 insertions(+), 3 deletions(-) diff --git a/lib/mpv/player/player_native.dart b/lib/mpv/player/player_native.dart index 0e996d26..e8c9c2ae 100644 --- a/lib/mpv/player/player_native.dart +++ b/lib/mpv/player/player_native.dart @@ -11,6 +11,8 @@ import 'player_base.dart'; /// or FlTextureGL (Linux). class PlayerNative extends PlayerBase { int? _textureIdValue; + String _dvConversionMode = 'auto'; + String _dvConversionLog = 'no'; @override int? get textureId => _textureIdValue; @@ -34,6 +36,52 @@ class PlayerNative extends PlayerBase { /// but as JSON strings on Android/Windows. static final String _nodeFormat = (Platform.isAndroid || Platform.isWindows) ? 'string' : 'node'; + static String _normalizeDvConversionMode(String value) { + return switch (value.toLowerCase()) { + 'disabled' || 'native' => 'disabled', + 'dv81' || 'p8' || 'p7_to_p8' || 'p7-to-p8' => 'dv81', + 'hevc' || 'hevc_strip' || 'p7_to_hevc' || 'p7-to-hevc' => 'hevc_strip', + _ => 'auto', + }; + } + + static String _normalizeBoolProperty(String value) { + return switch (value.toLowerCase()) { + '1' || 'true' || 'yes' || 'on' => 'yes', + _ => 'no', + }; + } + + MediaDisplayCriteria? _effectiveDisplayCriteria(MediaDisplayCriteria? criteria) { + if (criteria == null || (criteria.doviProfile ?? 0) != 7) return criteria; + + final convertToDv81 = _dvConversionMode == 'auto' || _dvConversionMode == 'dv81'; + if (convertToDv81) { + return MediaDisplayCriteria( + fps: criteria.fps, + width: criteria.width, + height: criteria.height, + doviProfile: 8, + doviLevel: criteria.doviLevel, + doviCompatibilityId: 1, + transfer: criteria.transfer ?? 'smpte2084', + primaries: criteria.primaries ?? 'bt2020', + matrix: criteria.matrix ?? 'bt2020nc', + ); + } + + return MediaDisplayCriteria( + fps: criteria.fps, + width: criteria.width, + height: criteria.height, + doviProfile: 0, + doviCompatibilityId: criteria.doviCompatibilityId ?? 1, + transfer: criteria.transfer ?? 'smpte2084', + primaries: criteria.primaries ?? 'bt2020', + matrix: criteria.matrix ?? 'bt2020nc', + ); + } + // Memoizes the in-flight init Future so concurrent callers (e.g. the // parallel `requestAudioFocus()` and `setProperty()` paths kicked off in // VideoPlayerScreen._initializePlayer) share one `invoke('initialize')`. @@ -209,6 +257,14 @@ class PlayerNative extends PlayerBase { @override Future setProperty(String name, String value) async { if (disposed) return; + if ((Platform.isIOS || Platform.isMacOS) && name == 'dv-conversion-mode') { + value = _normalizeDvConversionMode(value); + _dvConversionMode = value; + } + if ((Platform.isIOS || Platform.isMacOS) && name == 'dv-conversion-log') { + value = _normalizeBoolProperty(value); + _dvConversionLog = value; + } await _ensureInitialized(); await invoke('setProperty', {'name': name, 'value': value}); } @@ -216,6 +272,12 @@ class PlayerNative extends PlayerBase { @override Future getProperty(String name) async { if (disposed) return null; + if ((Platform.isIOS || Platform.isMacOS) && name == 'dv-conversion-mode') { + return _dvConversionMode; + } + if ((Platform.isIOS || Platform.isMacOS) && name == 'dv-conversion-log') { + return _dvConversionLog; + } await _ensureInitialized(); return await invoke('getProperty', {'name': name}); } @@ -231,7 +293,7 @@ class PlayerNative extends PlayerBase { Future setDisplayCriteria(MediaDisplayCriteria? criteria) async { if (disposed || !Platform.isIOS) return; await _ensureInitialized(); - await invoke('setDisplayCriteria', {'criteria': criteria?.toJson()}); + await invoke('setDisplayCriteria', {'criteria': _effectiveDisplayCriteria(criteria)?.toJson()}); } @override diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index 0f8a57fb..5eedd86b 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -647,9 +647,14 @@ class VideoPlayerScreenState extends State with WidgetsBindin if (Platform.isAndroid && useExoPlayer) { final tunneledPlayback = settingsService.read(SettingsService.tunneledPlayback); await currentPlayer.setProperty('tunneled-playback', tunneledPlayback ? 'yes' : 'no'); + } + if ((Platform.isAndroid && useExoPlayer) || Platform.isIOS || Platform.isMacOS) { final dvConversionMode = settingsService.read(SettingsService.dvConversionMode); await currentPlayer.setProperty('dv-conversion-mode', dvConversionMode.nativeValue); } + if (Platform.isIOS || Platform.isMacOS) { + await currentPlayer.setProperty('dv-conversion-log', debugLoggingEnabled ? 'yes' : 'no'); + } if (bufferSizeMB > 0) { final bufferSizeBytes = bufferSizeMB * 1024 * 1024; await currentPlayer.setProperty('demuxer-max-bytes', bufferSizeBytes.toString()); diff --git a/lib/widgets/video_controls/sheets/video_settings_sheet.dart b/lib/widgets/video_controls/sheets/video_settings_sheet.dart index 0a9c6014..2f54428e 100644 --- a/lib/widgets/video_controls/sheets/video_settings_sheet.dart +++ b/lib/widgets/video_controls/sheets/video_settings_sheet.dart @@ -185,6 +185,12 @@ class _VideoSettingsSheetState extends State { late int _subtitleSyncOffset; String _dvConversionMode = 'auto'; + bool get _showDebugDvConversionMode { + if (!kDebugMode) return false; + if (Platform.isAndroid) return widget.player.playerType == 'exoplayer'; + return (Platform.isIOS || Platform.isMacOS) && widget.player.playerType == 'mpv'; + } + @override void initState() { super.initState(); @@ -194,7 +200,7 @@ class _VideoSettingsSheetState extends State { } Future _loadDebugDvConversionMode() async { - if (!kDebugMode || !Platform.isAndroid || widget.player.playerType != 'exoplayer') return; + if (!_showDebugDvConversionMode) return; final dvConversionMode = await widget.player.getProperty('dv-conversion-mode'); if (!mounted) return; setState(() { @@ -535,7 +541,7 @@ class _VideoSettingsSheetState extends State { title: t.videoSettings.performanceOverlay, ), - if (kDebugMode && Platform.isAndroid && widget.player.playerType == 'exoplayer') + if (_showDebugDvConversionMode) _SettingsMenuItem( icon: Symbols.hdr_strong_rounded, title: 'DV Conversion Mode', diff --git a/shared/apple/MpvPlayer/MpvPlayerCoreBase.swift b/shared/apple/MpvPlayer/MpvPlayerCoreBase.swift index 69c2737e..250e9f97 100644 --- a/shared/apple/MpvPlayer/MpvPlayerCoreBase.swift +++ b/shared/apple/MpvPlayer/MpvPlayerCoreBase.swift @@ -97,6 +97,8 @@ class MpvPlayerCoreBase: NSObject { private var cachedVideoPrimaries: String? private var cachedVideoColorMatrix: String? private var serverDisplayCriteriaActive = false + private var cachedDvConversionMode = "auto" + private var cachedDvConversionLogEnabled = false var hdrEnabled: Bool { cacheLock.lock() defer { cacheLock.unlock() } @@ -284,6 +286,8 @@ class MpvPlayerCoreBase: NSObject { guard let renderLayer = videoLayer else { return false } #endif + applyDvConversionModeEnvironment() + mpv = mpv_create() guard let mpv else { print("[MpvPlayerCore] Failed to create MPV context") @@ -386,9 +390,90 @@ class MpvPlayerCoreBase: NSObject { return } + if name == "dv-conversion-mode" { + setDvConversionMode(value) + completion(.success(())) + return + } + + if name == "dv-conversion-log" { + setDvConversionLogEnabled(parseBoolProperty(value)) + completion(.success(())) + return + } + setRawStringPropertyAsync(name, value: value, completion: completion) } + private func parseBoolProperty(_ value: String) -> Bool { + switch value.lowercased() { + case "1", "true", "yes", "on": + return true + default: + return false + } + } + + private func normalizeDvConversionMode(_ value: String) -> String { + switch value.lowercased() { + case "disabled", "native": + return "disabled" + case "dv81", "p8", "p7_to_p8", "p7-to-p8": + return "dv81" + case "hevc", "hevc_strip", "p7_to_hevc", "p7-to-hevc": + return "hevc_strip" + default: + return "auto" + } + } + + private func applyDvConversionModeEnvironment() { + cacheLock.lock() + let mode = cachedDvConversionMode + let logEnabled = cachedDvConversionLogEnabled + cacheLock.unlock() + + setenv("PLEZY_DV_CONVERSION_MODE", mode, 1) + setenv("PLEZY_DV_CONVERSION_LOG", logEnabled ? "1" : "0", 1) + } + + func setDvConversionMode(_ mode: String) { + cacheLock.lock() + cachedDvConversionMode = normalizeDvConversionMode(mode) + let normalized = cachedDvConversionMode + let logEnabled = cachedDvConversionLogEnabled + cacheLock.unlock() + + applyDvConversionModeEnvironment() + if logEnabled { + print("[MpvPlayerCore] DV conversion mode: \(normalized)") + } + } + + func setDvConversionLogEnabled(_ enabled: Bool) { + cacheLock.lock() + cachedDvConversionLogEnabled = enabled + let mode = cachedDvConversionMode + cacheLock.unlock() + + applyDvConversionModeEnvironment() + if enabled { + print("[MpvPlayerCore] DV conversion logging enabled (mode: \(mode))") + } + } + + func getDvConversionMode() -> String { + cacheLock.lock() + defer { cacheLock.unlock() } + return cachedDvConversionMode + } + + func getDvConversionLogEnabled() -> Bool { + cacheLock.lock() + defer { cacheLock.unlock() } + return cachedDvConversionLogEnabled + } + func setInt64PropertyAsync( _ name: String, value: Int64, @@ -443,6 +528,16 @@ class MpvPlayerCoreBase: NSObject { } func getPropertyAsync(_ name: String, completion: @escaping (Result) -> Void) { + if name == "dv-conversion-mode" { + completion(.success(getDvConversionMode())) + return + } + + if name == "dv-conversion-log" { + completion(.success(getDvConversionLogEnabled() ? "yes" : "no")) + return + } + guard let mpv else { completion(.success(nil)) return