diff --git a/lib/mpv/player/platform/player_tizen.dart b/lib/mpv/player/platform/player_tizen.dart index c69ba34d9..96ffbd1ee 100644 --- a/lib/mpv/player/platform/player_tizen.dart +++ b/lib/mpv/player/platform/player_tizen.dart @@ -315,7 +315,6 @@ class PlayerTizen extends PlayerBase with VideoRectSupport { 'videoWidth': _width, 'videoHeight': _height, 'playerType': 'tizen', - 'videoDecoder': 'unknown', }; @override Future getProperty(String name) async => switch (name) { diff --git a/lib/widgets/video_controls/widgets/performance_overlay/performance_overlay.dart b/lib/widgets/video_controls/widgets/performance_overlay/performance_overlay.dart index a96286735..05eb84ca3 100644 --- a/lib/widgets/video_controls/widgets/performance_overlay/performance_overlay.dart +++ b/lib/widgets/video_controls/widgets/performance_overlay/performance_overlay.dart @@ -68,6 +68,10 @@ class _PlayerPerformanceOverlayState extends State { @override Widget build(BuildContext context) { final isMpv = _stats.playerType == 'mpv'; + // `!isMpv` used to mean ExoPlayer, back when those were the only two + // backends. Tizen is a third, and these metrics are ExoPlayer's alone, so + // ask for it by name rather than by elimination. + final isExo = _stats.playerType == 'exoplayer'; final sections = [ _buildSection(Symbols.videocam_rounded, t.fileInfo.video, [ @@ -76,8 +80,8 @@ class _PlayerPerformanceOverlayState extends State { if (_stats.hasValidVideoFps) _metric(t.performanceOverlay.fps, _stats.videoFpsFormatted), if (_stats.hasValidVideoBitrate) _metric(t.fileInfo.bitrate, _stats.videoBitrateFormatted), _metric(t.performanceOverlay.decoder, _stats.hwdecFormatted), - if (!isMpv && _stats.videoDecoderName != null) _metric(t.performanceOverlay.rawDecoder, _stats.videoDecoderRaw), - if (!isMpv) _metric(t.performanceOverlay.tunneling, _stats.tunneledPlaybackFormatted), + if (isExo && _stats.videoDecoderName != null) _metric(t.performanceOverlay.rawDecoder, _stats.videoDecoderRaw), + if (isExo) _metric(t.performanceOverlay.tunneling, _stats.tunneledPlaybackFormatted), if (_stats.aspectName != null && _stats.aspectName!.isNotEmpty) _metric(t.performanceOverlay.aspect, _stats.aspectName!), if (_stats.rotate != null && _stats.rotate != 0) _metric(t.performanceOverlay.rotation, _stats.rotateFormatted), @@ -91,7 +95,7 @@ class _PlayerPerformanceOverlayState extends State { _metric(t.performanceOverlay.sampleRate, _stats.sampleRateFormatted), _metric(t.fileInfo.channels, _stats.audioChannels ?? t.common.notAvailable), if (_stats.hasValidAudioBitrate) _metric(t.fileInfo.bitrate, _stats.audioBitrateFormatted), - if (!isMpv && _stats.audioDecoderName != null) + if (isExo && _stats.audioDecoderName != null) _metric(t.performanceOverlay.decoder, _stats.audioDecoderFormatted), ]), if (isMpv) diff --git a/lib/widgets/video_controls/widgets/performance_overlay/performance_stats.dart b/lib/widgets/video_controls/widgets/performance_overlay/performance_stats.dart index 1b64a51e2..35a8e57b2 100644 --- a/lib/widgets/video_controls/widgets/performance_overlay/performance_stats.dart +++ b/lib/widgets/video_controls/widgets/performance_overlay/performance_stats.dart @@ -259,10 +259,13 @@ class PerformanceStats { } return t.performanceOverlay.decoderSoftware; } - // For MPV, use hwdec-current property - if (hwdecCurrent == null || hwdecCurrent!.isEmpty || hwdecCurrent == 'no') { - return t.performanceOverlay.decoderSoftware; - } + // For MPV, use hwdec-current property. Only mpv's own `no` is evidence of + // software decoding; an absent or empty value means the backend never told + // us, which is not the same claim. Backends that decode outside the app + // (Tizen's Capi plane) never report one, and asserting "Software" there is + // the exact error ACCEPTANCE.md forbids in the other direction. + if (hwdecCurrent == 'no') return t.performanceOverlay.decoderSoftware; + if (hwdecCurrent == null || hwdecCurrent!.isEmpty) return t.common.notAvailable; return hwdecCurrent!; } @@ -372,6 +375,7 @@ class PerformanceStats { return switch (playerType.toLowerCase()) { 'mpv' => 'MPV', 'exoplayer' => 'ExoPlayer', + 'tizen' => 'Tizen', _ => playerType, }; } diff --git a/lib/widgets/video_controls/widgets/performance_overlay/performance_stats_service.dart b/lib/widgets/video_controls/widgets/performance_overlay/performance_stats_service.dart index b48bd7409..36a7ba7d6 100644 --- a/lib/widgets/video_controls/widgets/performance_overlay/performance_stats_service.dart +++ b/lib/widgets/video_controls/widgets/performance_overlay/performance_stats_service.dart @@ -196,6 +196,21 @@ class PerformanceStatsService { uiFps: _currentUiFps, ); _emit(stats); + } else if (playerType == 'tizen') { + // The Capi backend decodes on the TV's own SoC, outside Flutter, and + // exposes no decoder name, hwdec state or frame counters. Report only + // what it actually returns; every other metric stays null so the + // overlay prints N/A rather than inventing a value. Without this branch + // Tizen fell through to the ExoPlayer parse below, which labelled the + // backend "ExoPlayer" and left hwdec null - read as "Software". + final stats = PerformanceStats( + playerType: 'tizen', + videoWidth: statsMap['videoWidth'] as int?, + videoHeight: statsMap['videoHeight'] as int?, + appMemoryBytes: appMemory, + uiFps: _currentUiFps, + ); + _emit(stats); } else { // Parse ExoPlayer stats format final stats = PerformanceStats( diff --git a/test/widgets/performance_stats_service_test.dart b/test/widgets/performance_stats_service_test.dart index 35d3bec2c..64e708667 100644 --- a/test/widgets/performance_stats_service_test.dart +++ b/test/widgets/performance_stats_service_test.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter_test/flutter_test.dart'; +import 'package:plezy/i18n/strings.g.dart'; import 'package:plezy/mpv/mpv.dart'; import 'package:plezy/widgets/video_controls/widgets/performance_overlay/performance_stats.dart'; import 'package:plezy/widgets/video_controls/widgets/performance_overlay/performance_stats_service.dart'; @@ -292,4 +293,31 @@ void main() { expect(stats.droppedFramesFormatted, '3'); }); }); + + group('a backend that decodes outside the app', () { + test('Tizen reports itself, not ExoPlayer', () async { + final stats = await _firstStats( + _NativeStatsPlayer({'playerType': 'tizen', 'videoWidth': 1920, 'videoHeight': 1080}), + ); + + expect(stats.playerType, 'tizen'); + expect(stats.playerTypeFormatted, 'Tizen'); + expect(stats.videoWidth, 1920); + // Falling through to the ExoPlayer parse used to invent these. + expect(stats.videoDecoderName, isNull); + }); + + test('no hwdec signal reads as unavailable, never as software', () { + // The Capi plane exposes no hwdec state. Claiming software decoding + // there asserts a measurement nobody took. + const stats = PerformanceStats(playerType: 'tizen'); + expect(stats.hwdecFormatted, isNot(t.performanceOverlay.decoderSoftware)); + expect(stats.hwdecFormatted, t.common.notAvailable); + }); + + test("mpv's own 'no' is still real evidence of software decoding", () { + const stats = PerformanceStats(playerType: 'mpv', hwdecCurrent: 'no'); + expect(stats.hwdecFormatted, t.performanceOverlay.decoderSoftware); + }); + }); }