fix(playback): support Apple DV conversion
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user