Files
plezy/test/widgets/performance_stats_service_test.dart
T
edde746 8f893e7a67 fix(player): demux Android direct play with media3 again
The in-app FFmpeg container demuxer classified every DTS variant as
plain DTS (profile-blind MIME mapping at the demux boundary), so
DTS-HD MA lost its lossless identity downstream, and it ignored
container display-aspect-ratio overrides. media3's extractors get both
right.

Delete the FFmpeg demuxer (JNI, extractor, and its setting); ExoPlayer
direct play demuxes with media3's DefaultExtractorsFactory again, and
mpv - the Android default - reads container display dimensions itself.

close #2124
close #2115
2026-08-26 18:59:28 +02:00

184 lines
5.8 KiB
Dart

import 'dart:async';
import 'package:flutter_test/flutter_test.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';
/// Player fake that answers mpv property queries from a fixed map.
class _PropertyPlayer implements Player {
_PropertyPlayer(this.properties);
final Map<String, String> properties;
@override
final PlayerStreams streams = const PlayerStreams(
playing: Stream.empty(),
completed: Stream.empty(),
buffering: Stream.empty(),
position: Stream.empty(),
duration: Stream.empty(),
seekable: Stream.empty(),
buffer: Stream.empty(),
volume: Stream.empty(),
rate: Stream.empty(),
tracks: Stream.empty(),
track: Stream.empty(),
log: Stream.empty(),
error: Stream.empty(),
audioDevice: Stream.empty(),
audioDevices: Stream.empty(),
bufferRanges: Stream.empty(),
playbackRestart: Stream.empty(),
fileStarted: Stream.empty(),
fileLoaded: Stream.empty(),
fileLoadFailed: Stream.empty(),
primaryMediaReady: Stream.empty(),
backendSwitched: Stream.empty(),
);
@override
bool get providesNativeStats => false;
@override
Future<String?> getProperty(String name) async => properties[name];
@override
Future<String> runtimePlayerType() async => 'mpv';
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
/// Player fake that reports native (Android ExoPlayer) stats.
class _NativeStatsPlayer extends _PropertyPlayer {
_NativeStatsPlayer(this.stats) : super(const {});
final Map<String, dynamic> stats;
@override
bool get providesNativeStats => true;
@override
Future<Map<String, dynamic>> getStats() async => stats;
@override
Future<String> runtimePlayerType() async => 'exoplayer';
}
Future<PerformanceStats> _firstStats(_PropertyPlayer player) async {
final service = PerformanceStatsService(player);
try {
final first = service.statsStream.first;
service.startPolling();
return await first;
} finally {
service.dispose();
}
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('PerformanceStatsService audio display', () {
test('spdif passthrough reports the source track, not the IEC carrier (#1300)', () async {
// mpv's audio-params during E-AC-3 bitstreaming: the 192 kHz "stereo"
// IEC 61937 carrier. The overlay must show the 48 kHz 5.1 source track.
final stats = await _firstStats(
_PropertyPlayer({
'audio-codec-name': 'eac3',
'audio-params/format': 'spdif-eac3',
'audio-params/samplerate': '192000',
'audio-params/hr-channels': 'stereo',
'current-tracks/audio/demux-samplerate': '48000',
'current-tracks/audio/demux-channel-count': '6',
}),
);
expect(stats.audioSamplerate, 48000);
expect(stats.audioChannels, '5.1');
expect(stats.audioPassthrough, isTrue);
expect(stats.audioPassthroughFormatted, 'E-AC3');
});
test('locally decoded audio keeps mpv audio-params verbatim', () async {
final stats = await _firstStats(
_PropertyPlayer({
'audio-codec-name': 'eac3',
'audio-params/format': 'floatp',
'audio-params/samplerate': '48000',
'audio-params/hr-channels': '5.1',
'current-tracks/audio/demux-samplerate': '48000',
'current-tracks/audio/demux-channel-count': '6',
}),
);
expect(stats.audioSamplerate, 48000);
expect(stats.audioChannels, '5.1');
expect(stats.audioPassthrough, isFalse);
});
test('passthrough with unavailable track metadata degrades to N/A, never the carrier', () async {
final stats = await _firstStats(
_PropertyPlayer({
'audio-params/format': 'spdif-dts-hd',
'audio-params/samplerate': '192000',
'audio-params/hr-channels': 'stereo',
}),
);
expect(stats.audioSamplerate, isNull);
expect(stats.audioChannels, isNull);
expect(stats.audioPassthrough, isTrue);
expect(stats.audioPassthroughFormatted, 'DTS-HD');
});
});
group('PerformanceStatsService ExoPlayer codec display (#2063)', () {
test('falls back to the sample MIME type when the container has no codecs string', () async {
// Matroska: Format.codecs is null; only the MIME types identify the
// streams.
final stats = await _firstStats(
_NativeStatsPlayer({
'playerType': 'exoplayer',
'videoCodec': null,
'videoMimeType': 'video/hevc',
'audioCodec': null,
'audioMimeType': 'audio/eac3',
'audioSampleRate': 48000,
'audioChannels': 6,
}),
);
expect(stats.videoCodec, 'HEVC');
expect(stats.audioCodec, 'E-AC3');
expect(stats.audioChannels, '5.1');
});
test('an explicit codecs string wins over the MIME type', () async {
final stats = await _firstStats(
_NativeStatsPlayer({
'playerType': 'exoplayer',
'videoCodec': 'hvc1.2.4.L153.B0',
'videoMimeType': 'video/hevc',
'audioCodec': 'mp4a.40.2',
'audioMimeType': 'audio/mp4a-latm',
}),
);
expect(stats.videoCodec, 'HEVC');
expect(stats.audioCodec, 'AAC');
});
test('measured audio bitrate from the native side is surfaced', () async {
final stats = await _firstStats(
_NativeStatsPlayer({'playerType': 'exoplayer', 'audioMimeType': 'audio/vnd.dts', 'audioBitrate': 1509000}),
);
expect(stats.audioCodec, 'DTS');
expect(stats.hasValidAudioBitrate, isTrue);
expect(stats.audioBitrateFormatted, '1509 kbps');
});
});
}