perf(android): optimize DV conversion hot path

Remove avoidable per-RPU allocations and copies during DV fallback conversion, and add debug controls to force conversion modes while profiling playback.
This commit is contained in:
edde746
2026-05-01 17:50:59 +02:00
parent e78f7cee5e
commit 036d99f11f
10 changed files with 370 additions and 70 deletions
@@ -30,7 +30,7 @@ import '../widgets/sleep_timer_content.dart';
import '../../../i18n/strings.g.dart';
import 'base_video_control_sheet.dart';
enum _SettingsView { menu, speed, sleep, audioSync, subtitleSync, audioDevice, shader }
enum _SettingsView { menu, speed, sleep, audioSync, subtitleSync, audioDevice, shader, dvConversion }
/// Reusable menu item widget for settings sheet
class _SettingsMenuItem extends StatelessWidget {
@@ -137,6 +137,7 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
bool _autoPlayNextEpisode = true;
bool _audioPassthrough = false;
bool _audioNormalization = false;
String _dvConversionMode = 'auto';
@override
void initState() {
@@ -148,6 +149,9 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
Future<void> _loadSettings() async {
final settings = await SettingsService.getInstance();
final dvConversionMode = kDebugMode && Platform.isAndroid && widget.player.playerType == 'exoplayer'
? await widget.player.getProperty('dv-conversion-mode')
: null;
if (!mounted) return;
setState(() {
_enableHDR = settings.read(SettingsService.enableHDR);
@@ -155,6 +159,7 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
_autoPlayNextEpisode = settings.read(SettingsService.autoPlayNextEpisode);
_audioPassthrough = settings.read(SettingsService.audioPassthrough);
_audioNormalization = settings.read(SettingsService.audioNormalization);
_dvConversionMode = _normalizeDvConversionMode(dvConversionMode);
});
}
@@ -212,6 +217,15 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
await widget.player.setProperty('af', newValue ? 'loudnorm=I=-14:TP=-3:LRA=4' : '');
}
Future<void> _setDebugDvConversionMode(String mode) async {
await widget.player.setProperty('dv-conversion-mode', mode);
if (!mounted) return;
setState(() {
_dvConversionMode = mode;
});
OverlaySheetController.of(context).close();
}
void _navigateTo(_SettingsView view) {
// Sync views open as a compact top bar instead of a sub-view
if (view == _SettingsView.audioSync || view == _SettingsView.subtitleSync) {
@@ -297,6 +311,8 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
return t.videoSettings.audioOutput;
case _SettingsView.shader:
return t.shaders.title;
case _SettingsView.dvConversion:
return 'DV Conversion Mode';
}
}
@@ -316,9 +332,29 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
return Symbols.speaker_rounded;
case _SettingsView.shader:
return Symbols.auto_fix_high_rounded;
case _SettingsView.dvConversion:
return Symbols.hdr_strong_rounded;
}
}
String _normalizeDvConversionMode(String? mode) {
return switch (mode?.trim().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',
};
}
String _formatDvConversionMode(String mode) {
return switch (_normalizeDvConversionMode(mode)) {
'disabled' => 'Native / Disabled',
'dv81' => 'P7 → P8.1',
'hevc_strip' => 'P7 → HEVC',
_ => 'Auto',
};
}
String _formatSleepTimer(SleepTimerService sleepTimer) {
if (!sleepTimer.isActive) return 'Off';
final remaining = sleepTimer.remainingTime;
@@ -512,6 +548,15 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
onTap: _togglePerformanceOverlay,
),
if (kDebugMode && Platform.isAndroid && widget.player.playerType == 'exoplayer')
_SettingsMenuItem(
icon: Symbols.hdr_strong_rounded,
title: 'DV Conversion Mode',
valueText: _formatDvConversionMode(_dvConversionMode),
isHighlighted: _dvConversionMode != 'auto',
onTap: () => _navigateTo(_SettingsView.dvConversion),
),
// Debug: Trigger MPV Fallback (Android ExoPlayer only)
if (kDebugMode && Platform.isAndroid && widget.player.playerType == 'exoplayer')
FocusableListTile(
@@ -539,6 +584,28 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
);
}
Widget _buildDvConversionView() {
const modes = [
(value: 'auto', title: 'Auto', subtitle: 'Use device capability detection and normal fallback behavior'),
(value: 'disabled', title: 'Native / Disabled', subtitle: 'Force native DV7 and suppress DV conversion retry'),
(value: 'dv81', title: 'P7 → P8.1', subtitle: 'Force inline RPU conversion to Dolby Vision profile 8.1'),
(value: 'hevc_strip', title: 'P7 → HEVC', subtitle: 'Strip Dolby Vision RPU/EL layers and present plain HEVC'),
];
final primary = Theme.of(context).colorScheme.primary;
return ListView(
children: [
for (final mode in modes)
FocusableListTile(
title: Text(mode.title, style: TextStyle(color: _dvConversionMode == mode.value ? primary : null)),
subtitle: Text(mode.subtitle, style: TextStyle(color: tokens(context).textMuted, fontSize: 12)),
trailing: _dvConversionMode == mode.value ? AppIcon(Symbols.check_rounded, fill: 1, color: primary) : null,
onTap: () => _setDebugDvConversionMode(mode.value),
),
],
);
}
Widget _buildSpeedView() {
return StreamBuilder<double>(
stream: widget.player.streams.rate,
@@ -854,6 +921,8 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
return _buildAudioDeviceView();
case _SettingsView.shader:
return _buildShaderView();
case _SettingsView.dvConversion:
return _buildDvConversionView();
}
}(),
);