feat: implement native video player screen using media_kit with optimized playback profiles

This commit is contained in:
Michael committed 2026-03-29 15:10:39 +02:00
1 parent 6c1e42cd5a
commit a1628a6b13
2 files changed
+44 -31

No files matched your search

+5 -1
View File
@@ -51,7 +51,11 @@
"Bash(powershell -Command \"Get-Command flutter -ErrorAction SilentlyContinue\")", "Bash(powershell -Command \"Get-Command flutter -ErrorAction SilentlyContinue\")",
"Bash(powershell -Command \"Get-ChildItem -Path ''C:\\\\'' -Recurse -Filter ''flutter.bat'' -ErrorAction SilentlyContinue | Select-Object -First 5 -ExpandProperty FullName\")", "Bash(powershell -Command \"Get-ChildItem -Path ''C:\\\\'' -Recurse -Filter ''flutter.bat'' -ErrorAction SilentlyContinue | Select-Object -First 5 -ExpandProperty FullName\")",
"Bash(powershell -Command \"Set-Location ''c:\\\\Users\\\\Michael\\\\Git\\\\xtremobile''; & ''C:\\\\Users\\\\Michael\\\\Git\\\\flutter\\\\bin\\\\flutter.bat'' build apk --release 2>&1\")", "Bash(powershell -Command \"Set-Location ''c:\\\\Users\\\\Michael\\\\Git\\\\xtremobile''; & ''C:\\\\Users\\\\Michael\\\\Git\\\\flutter\\\\bin\\\\flutter.bat'' build apk --release 2>&1\")",
"Bash(powershell -Command \":*)" "Bash(powershell -Command \":*)",
"WebFetch(domain:github.com)",
"WebFetch(domain:tivimatepremiums.com)",
"WebFetch(domain:tivimates.com)",
"WebFetch(domain:tv.dezzflix.com)"
] ]
} }
} }
@@ -183,7 +183,10 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
// Startup Speed: Disable initial cache pause to allow immediate playback // Startup Speed: Disable initial cache pause to allow immediate playback
(_player.platform as dynamic)?.setProperty('cache-pause-initial', 'no'); (_player.platform as dynamic)?.setProperty('cache-pause-initial', 'no');
// (_player.platform as dynamic)?.setProperty('cache-pause-wait', '3'); // Never pause for buffering — stay at live edge like ExoPlayer/TiviMate
(_player.platform as dynamic)?.setProperty('cache-pause', 'no');
// Prevent any seek attempt on live streams
(_player.platform as dynamic)?.setProperty('force-seekable', 'no');
} else { } else {
// [P2-2 FIX] VOD PROFILE: Adaptive buffer size based on device RAM // [P2-2 FIX] VOD PROFILE: Adaptive buffer size based on device RAM
// - Low-end (<2GB): 20MB (conservative, prevent OOM) // - Low-end (<2GB): 20MB (conservative, prevent OOM)
@@ -227,27 +230,22 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
(_player.platform as dynamic) (_player.platform as dynamic)
?.setProperty('http-header-fields', 'User-Agent: XtremFlow/1.0'); ?.setProperty('http-header-fields', 'User-Agent: XtremFlow/1.0');
// Live Stream optimizations // Network-level reconnection (FFmpeg internal — no app reload)
if (widget.streamType == model.StreamType.live) {
// For live: reconnect but do NOT use reconnect_streamed (causes rollbacks)
(_player.platform as dynamic)?.setProperty(
'stream-lavf-o',
'reconnect=1,reconnect_on_network_error=1,reconnect_delay_max=3',
);
} else {
// For VOD/series: full reconnect support
(_player.platform as dynamic)?.setProperty( (_player.platform as dynamic)?.setProperty(
'stream-lavf-o', 'stream-lavf-o',
'reconnect=1,reconnect_streamed=1,reconnect_on_network_error=1,reconnect_delay_max=5', 'reconnect=1,reconnect_streamed=1,reconnect_on_network_error=1,reconnect_delay_max=5',
); );
}
(_player.platform as dynamic)?.setProperty( (_player.platform as dynamic)?.setProperty(
'demuxer-lavf-o', 'demuxer-lavf-o',
'analyzeduration=2000000,probesize=1000000', 'analyzeduration=2000000,probesize=1000000,fflags=+discardcorrupt',
); );
// Optimized for performance and stability on progressive & interlaced streams // Seeking: disabled for live (prevents rollback), enabled for VOD
if (widget.streamType == model.StreamType.live) {
(_player.platform as dynamic)?.setProperty('hr-seek', 'no');
} else {
(_player.platform as dynamic)?.setProperty('hr-seek', 'yes'); (_player.platform as dynamic)?.setProperty('hr-seek', 'yes');
}
(_player.platform as dynamic)?.setProperty('hr-seek-framedrop', 'yes'); (_player.platform as dynamic)?.setProperty('hr-seek-framedrop', 'yes');
(_player.platform as dynamic)?.setProperty('vd-lavc-fast', 'yes'); (_player.platform as dynamic)?.setProperty('vd-lavc-fast', 'yes');
(_player.platform as dynamic) (_player.platform as dynamic)
@@ -347,27 +345,28 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
_player.stream.error.listen((error) async { _player.stream.error.listen((error) async {
if (!mounted) return; if (!mounted) return;
// Filter out non-errors or non-fatal messages if necessary
if (error.isEmpty) return; if (error.isEmpty) return;
debugPrint('MediaKitPlayer Error Stream: $error'); debugPrint('MediaKitPlayer Error Stream: $error');
// Smart Retry for Codec/Decoder errors // For LIVE TV: never reload the stream on errors — mpv handles recovery internally.
// If we haven't tried SW decoding yet, retry with it enabled. // Only show fatal errors that truly stop playback (detected by watchdog after 2min).
if (widget.streamType == model.StreamType.live) {
debugPrint('MediaKitPlayer: Live — ignoring error (mpv handles internally): $error');
return;
}
// For VOD/Series: retry with software decoding on first error
if (!_useSoftwareDecoder) { if (!_useSoftwareDecoder) {
debugPrint( debugPrint(
'MediaKitPlayer: Error encountered. Auto-retrying with Software Decoding...', 'MediaKitPlayer: VOD error — retrying with Software Decoding...',
); );
setState(() => _useSoftwareDecoder = true); setState(() => _useSoftwareDecoder = true);
// Short delay to allow player cleanup
await Future.delayed(const Duration(milliseconds: 500)); await Future.delayed(const Duration(milliseconds: 500));
if (mounted) _loadStream(); // Retry connection if (mounted) _loadStream();
return; return;
} }
// If SW decoding was already on or failed too, show error
setState(() { setState(() {
_errorMessage = error; _errorMessage = error;
_isLoading = false; _isLoading = false;
@@ -377,15 +376,16 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
// Listen for stream completion // Listen for stream completion
_player.stream.completed.listen((completed) { _player.stream.completed.listen((completed) {
if (completed && mounted) { if (completed && mounted) {
// Logic for Live TV // For Live TV: do NOT reconnect immediately — let mpv handle it internally.
// Only reconnect after a long pause (handled by watchdog).
if (widget.streamType == model.StreamType.live) { if (widget.streamType == model.StreamType.live) {
debugPrint( debugPrint(
'MediaKitPlayer: Live stream completed unexpectedly, attempting reconnect...', 'MediaKitPlayer: Live stream completed signal — ignoring (mpv handles reconnect internally)',
); );
_attemptReconnect(); return;
} }
// Logic for VOD/Series: Check if really finished or cut off // Logic for VOD/Series: Check if really finished or cut off
else if (_duration.inSeconds > 0) { if (_duration.inSeconds > 0) {
final progress = _position.inSeconds / _duration.inSeconds; final progress = _position.inSeconds / _duration.inSeconds;
if (progress < 0.95) { if (progress < 0.95) {
debugPrint( debugPrint(
@@ -652,22 +652,31 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
} }
} }
int _watchdogStallCount = 0;
/// Start watchdog timer for live streams /// Start watchdog timer for live streams
void _startLiveWatchdog() { void _startLiveWatchdog() {
_liveWatchdog?.cancel(); _liveWatchdog?.cancel();
_watchdogStallCount = 0;
if (widget.streamType != model.StreamType.live) return; if (widget.streamType != model.StreamType.live) return;
// Check every 30 seconds if stream is still playing // Check every 60 seconds — only reconnect after 2 consecutive stalls (2 min total)
_liveWatchdog = Timer.periodic(const Duration(seconds: 30), (_) { _liveWatchdog = Timer.periodic(const Duration(seconds: 60), (_) {
if (!mounted) return; if (!mounted) return;
// If we're supposedly playing but position hasn't changed, reconnect
if (!_isPlaying && !_isLoading && _errorMessage == null) { if (!_isPlaying && !_isLoading && _errorMessage == null) {
_watchdogStallCount++;
debugPrint( debugPrint(
'MediaKitPlayer: Watchdog detected stalled stream, reconnecting...', 'MediaKitPlayer: Watchdog stall count: $_watchdogStallCount/2',
); );
if (_watchdogStallCount >= 2) {
debugPrint('MediaKitPlayer: Watchdog — stream stalled for 2min, reconnecting...');
_watchdogStallCount = 0;
_attemptReconnect(); _attemptReconnect();
} }
} else {
_watchdogStallCount = 0; // Reset if playing normally
}
}); });
} }