fix(playback): support Apple DV conversion

This commit is contained in:
edde746
2026-05-28 00:33:20 +02:00
parent e559a6125d
commit ba62129414
4 changed files with 171 additions and 3 deletions
+63 -1
View File
@@ -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<void> 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<String?> 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<String>('getProperty', {'name': name});
}
@@ -231,7 +293,7 @@ class PlayerNative extends PlayerBase {
Future<void> 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
+5
View File
@@ -647,9 +647,14 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> 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());
@@ -185,6 +185,12 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
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<VideoSettingsSheet> {
}
Future<void> _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<VideoSettingsSheet> {
title: t.videoSettings.performanceOverlay,
),
if (kDebugMode && Platform.isAndroid && widget.player.playerType == 'exoplayer')
if (_showDebugDvConversionMode)
_SettingsMenuItem(
icon: Symbols.hdr_strong_rounded,
title: 'DV Conversion Mode',
@@ -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<String?, Error>) -> 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