mirror of
https://github.com/R0m1k3/xtremobile.git
synced 2026-10-11 17:29:26 +02:00
feat: add lite and native IPTV player screens for video playback
This commit is contained in:
1 parent
3d88463c65
commit
18dc184388
2 files changed
+118
-56
No files matched your search
@@ -70,6 +70,8 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
|
||||
Duration _position = Duration.zero;
|
||||
Duration _duration = Duration.zero;
|
||||
bool _isSeeking = false;
|
||||
bool _isStabilizing = false; // Flag to mask initial glitches
|
||||
Timer? _stabilizationTimer;
|
||||
|
||||
// Focus Nodes
|
||||
final FocusNode _playPauseFocusNode = FocusNode();
|
||||
@@ -183,9 +185,16 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
|
||||
return;
|
||||
}
|
||||
|
||||
// Buffer Optimization: Wait for a pre-roll delay for Live TV
|
||||
// to let the internal buffer fill before playback starts.
|
||||
if (widget.streamType == StreamType.live) {
|
||||
debugPrint('[LitePlayer] Pre-roll buffer delay (3.5s)...');
|
||||
await Future.delayed(const Duration(milliseconds: 3500));
|
||||
}
|
||||
|
||||
await controller.play();
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_isStabilizing = true;
|
||||
_isPlaying = true;
|
||||
_duration = controller.value.duration;
|
||||
});
|
||||
@@ -206,14 +215,42 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
|
||||
void _videoListener() {
|
||||
if (_controller == null || !mounted) return;
|
||||
|
||||
final isPlaying = _controller!.value.isPlaying;
|
||||
final value = _controller!.value;
|
||||
|
||||
final isPlaying = value.isPlaying;
|
||||
if (isPlaying != _isPlaying) {
|
||||
setState(() => _isPlaying = isPlaying);
|
||||
}
|
||||
|
||||
// --- STABILIZATION MASKING LOGIC ---
|
||||
// Show spinner if buffering OR if we are in the initial stabilization phase
|
||||
final isBuffering = value.isBuffering;
|
||||
final shouldShowLoading = isBuffering || _isStabilizing;
|
||||
|
||||
if (shouldShowLoading != _isLoading) {
|
||||
setState(() => _isLoading = shouldShowLoading);
|
||||
}
|
||||
|
||||
// If we are playing smoothly and stabilizing, start/restart the timer
|
||||
if (isPlaying && !isBuffering && _isStabilizing) {
|
||||
_stabilizationTimer?.cancel();
|
||||
_stabilizationTimer = Timer(const Duration(milliseconds: 1500), () {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isStabilizing = false;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (isBuffering && _isStabilizing) {
|
||||
// If we hit a buffer during stabilization, reset the timer
|
||||
_stabilizationTimer?.cancel();
|
||||
}
|
||||
// ------------------------------------
|
||||
|
||||
if (!_isSeeking) {
|
||||
final pos = _controller!.value.position;
|
||||
final dur = _controller!.value.duration;
|
||||
final pos = value.position;
|
||||
final dur = value.duration;
|
||||
if (pos != _position || dur != _duration) {
|
||||
setState(() {
|
||||
_position = pos;
|
||||
@@ -222,8 +259,8 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
|
||||
}
|
||||
}
|
||||
|
||||
if (_controller!.value.hasError) {
|
||||
setState(() => _errorMessage = _controller!.value.errorDescription);
|
||||
if (value.hasError) {
|
||||
setState(() => _errorMessage = value.errorDescription);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -732,27 +769,34 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
Expanded(
|
||||
child: Slider(
|
||||
value: _position.inSeconds
|
||||
.toDouble()
|
||||
.clamp(0, _duration.inSeconds.toDouble()),
|
||||
min: 0,
|
||||
max: _duration.inSeconds.toDouble() > 0
|
||||
? _duration.inSeconds.toDouble()
|
||||
: 1.0,
|
||||
activeColor: AppColors.primary,
|
||||
inactiveColor: Colors.white24,
|
||||
onChanged: (val) {
|
||||
setState(() {
|
||||
_position = Duration(seconds: val.toInt());
|
||||
_isSeeking = true;
|
||||
});
|
||||
},
|
||||
onChangeEnd: (val) {
|
||||
_controller?.seekTo(Duration(seconds: val.toInt()));
|
||||
_isSeeking = false;
|
||||
_onUserInteraction();
|
||||
child: Shortcuts(
|
||||
shortcuts: <LogicalKeySet, Intent>{
|
||||
LogicalKeySet(LogicalKeyboardKey.arrowUp):
|
||||
const DirectionalFocusIntent(TraversalDirection.up),
|
||||
LogicalKeySet(LogicalKeyboardKey.arrowDown):
|
||||
const DirectionalFocusIntent(
|
||||
TraversalDirection.down),
|
||||
},
|
||||
child: Slider(
|
||||
value: _position.inSeconds
|
||||
.toDouble()
|
||||
.clamp(0, _duration.inSeconds.toDouble()),
|
||||
min: 0,
|
||||
max: _duration.inSeconds.toDouble(),
|
||||
activeColor: AppColors.primary,
|
||||
inactiveColor: Colors.white24,
|
||||
onChanged: (val) {
|
||||
setState(() {
|
||||
_position = Duration(seconds: val.toInt());
|
||||
_isSeeking = true;
|
||||
});
|
||||
},
|
||||
onChangeEnd: (val) {
|
||||
_controller?.seekTo(Duration(seconds: val.toInt()));
|
||||
_isSeeking = false;
|
||||
_onUserInteraction();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
|
||||
@@ -147,18 +147,22 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
|
||||
(_player.platform as dynamic)?.setProperty('cache', 'yes');
|
||||
|
||||
if (widget.streamType == StreamType.live) {
|
||||
// LIVE PROFILE: Faster startup, lower latency, smaller buffer
|
||||
debugPrint('MediaKitPlayer: Applying LIVE optimization profile');
|
||||
(_player.platform as dynamic)?.setProperty('cache-secs', '30');
|
||||
// LIVE PROFILE: Increased stability, no longer low-latency (prevents cuts)
|
||||
debugPrint(
|
||||
'MediaKitPlayer: Applying LIVE optimization profile (STABILITY)');
|
||||
(_player.platform as dynamic)
|
||||
?.setProperty('demuxer-max-bytes', '32000000'); // 32MB
|
||||
?.setProperty('cache-secs', '15'); // 15s cache
|
||||
(_player.platform as dynamic)
|
||||
?.setProperty('demuxer-readahead-secs', '20');
|
||||
?.setProperty('demuxer-max-bytes', '64000000'); // 64MB
|
||||
(_player.platform as dynamic)
|
||||
?.setProperty('demuxer-max-back-bytes', '10000000'); // 10MB back
|
||||
?.setProperty('demuxer-readahead-secs', '15');
|
||||
(_player.platform as dynamic)
|
||||
?.setProperty('demuxer-max-back-bytes', '10000000');
|
||||
|
||||
// Start playing as soon as possible, don't wait for cache fill
|
||||
(_player.platform as dynamic)?.setProperty('cache-pause-initial', 'no');
|
||||
// ALWAYS wait for buffer fill to prevent glitches
|
||||
(_player.platform as dynamic)?.setProperty('cache-pause-initial', 'yes');
|
||||
(_player.platform as dynamic)
|
||||
?.setProperty('cache-pause-wait', '3'); // Wait 3s max
|
||||
} else {
|
||||
// VOD PROFILE: Max stability, large buffer
|
||||
debugPrint('MediaKitPlayer: Applying VOD optimization profile');
|
||||
@@ -213,9 +217,12 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
|
||||
?.setProperty('vd-lavc-skiploopfilter', 'nonref'); // Balanced stability
|
||||
(_player.platform as dynamic)?.setProperty('framedrop', 'vo');
|
||||
|
||||
// Low-latency intentionally removed for stability
|
||||
/*
|
||||
if (widget.streamType == StreamType.live) {
|
||||
(_player.platform as dynamic)?.setProperty('profile', 'low-latency');
|
||||
}
|
||||
*/
|
||||
|
||||
// ============ AUDIO CODEC SUPPORT FOR VOD ============
|
||||
// V3: Explicit LAVC Downmix & Audiotrack
|
||||
@@ -1336,29 +1343,40 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Slider(
|
||||
value: _position.inSeconds
|
||||
.toDouble()
|
||||
.clamp(0, _duration.inSeconds.toDouble()),
|
||||
min: 0,
|
||||
max: _duration.inSeconds.toDouble(),
|
||||
// Add divisions for precise seeking (1 step = 10 seconds)
|
||||
divisions: _duration.inSeconds > 0
|
||||
? (_duration.inSeconds / 10).ceil()
|
||||
: null,
|
||||
activeColor: AppColors.primary,
|
||||
inactiveColor: Colors.white24,
|
||||
onChangeStart: (_) => _isSeeking = true,
|
||||
onChangeEnd: (value) async {
|
||||
await _player
|
||||
.seek(Duration(seconds: value.toInt()));
|
||||
_isSeeking = false;
|
||||
},
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_position = Duration(seconds: value.toInt());
|
||||
});
|
||||
child: Shortcuts(
|
||||
shortcuts: <LogicalKeySet, Intent>{
|
||||
LogicalKeySet(LogicalKeyboardKey.arrowUp):
|
||||
const DirectionalFocusIntent(
|
||||
TraversalDirection.up),
|
||||
LogicalKeySet(LogicalKeyboardKey.arrowDown):
|
||||
const DirectionalFocusIntent(
|
||||
TraversalDirection.down),
|
||||
},
|
||||
child: Slider(
|
||||
value: _position.inSeconds
|
||||
.toDouble()
|
||||
.clamp(0, _duration.inSeconds.toDouble()),
|
||||
min: 0,
|
||||
max: _duration.inSeconds.toDouble(),
|
||||
// Add divisions for precise seeking (1 step = 10 seconds)
|
||||
divisions: _duration.inSeconds > 0
|
||||
? (_duration.inSeconds / 10).ceil()
|
||||
: null,
|
||||
activeColor: AppColors.primary,
|
||||
inactiveColor: Colors.white24,
|
||||
onChangeStart: (_) => _isSeeking = true,
|
||||
onChangeEnd: (value) async {
|
||||
await _player
|
||||
.seek(Duration(seconds: value.toInt()));
|
||||
_isSeeking = false;
|
||||
},
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_position =
|
||||
Duration(seconds: value.toInt());
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
|
||||
Reference in new issue
Block a user