mirror of
https://github.com/R0m1k3/xtremobile.git
synced 2026-10-11 17:29:26 +02:00
feat: Implement native video player screen for IPTV streams using media_kit with extensive playback optimizations.
This commit is contained in:
1 parent
9a0bda4b7d
commit
4d93cc246d
2 files changed
+177
-157
No files matched your search
@@ -71,6 +71,7 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
|
|||||||
bool _isStabilizing = false;
|
bool _isStabilizing = false;
|
||||||
Timer? _stabilizationTimer;
|
Timer? _stabilizationTimer;
|
||||||
Timer? _controlsTimer;
|
Timer? _controlsTimer;
|
||||||
|
int _loadId = 0;
|
||||||
|
|
||||||
// Focus
|
// Focus
|
||||||
final FocusNode _playPauseFocusNode = FocusNode();
|
final FocusNode _playPauseFocusNode = FocusNode();
|
||||||
@@ -164,8 +165,11 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
|
|||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
_errorMessage = null;
|
_errorMessage = null;
|
||||||
_isStabilizing = true;
|
_isStabilizing = true;
|
||||||
|
_loadId++;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final currentLoadId = _loadId;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final currentStreamId =
|
final currentStreamId =
|
||||||
widget.channels != null && widget.channels!.isNotEmpty
|
widget.channels != null && widget.channels!.isNotEmpty
|
||||||
@@ -215,7 +219,7 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
|
|||||||
_resetControlsTimer();
|
_resetControlsTimer();
|
||||||
controller.addListener(_videoListener);
|
controller.addListener(_videoListener);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted)
|
if (mounted && currentLoadId == _loadId)
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
_errorMessage = "Playback Error: $e";
|
_errorMessage = "Playback Error: $e";
|
||||||
@@ -235,6 +239,7 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
|
|||||||
// Force hide loader immediately when playing
|
// Force hide loader immediately when playing
|
||||||
if (value.isPlaying) {
|
if (value.isPlaying) {
|
||||||
if (_isLoading) setState(() => _isLoading = false);
|
if (_isLoading) setState(() => _isLoading = false);
|
||||||
|
if (_errorMessage != null) setState(() => _errorMessage = null);
|
||||||
if (_isStabilizing) _isStabilizing = false;
|
if (_isStabilizing) _isStabilizing = false;
|
||||||
_stabilizationTimer?.cancel();
|
_stabilizationTimer?.cancel();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
|
|||||||
final FocusNode _playPauseFocusNode = FocusNode();
|
final FocusNode _playPauseFocusNode = FocusNode();
|
||||||
final FocusNode _prevFocusNode = FocusNode();
|
final FocusNode _prevFocusNode = FocusNode();
|
||||||
final FocusNode _nextFocusNode = FocusNode();
|
final FocusNode _nextFocusNode = FocusNode();
|
||||||
|
final FocusNode _sliderFocusNode = FocusNode();
|
||||||
final FocusNode _backFocusNode = FocusNode();
|
final FocusNode _backFocusNode = FocusNode();
|
||||||
final FocusNode _audioFocusNode = FocusNode();
|
final FocusNode _audioFocusNode = FocusNode();
|
||||||
|
|
||||||
@@ -250,10 +251,9 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
|
|||||||
(_player.platform as dynamic)?.setProperty('mute', 'no');
|
(_player.platform as dynamic)?.setProperty('mute', 'no');
|
||||||
|
|
||||||
// Explicitly enable LAVC downmixing to stereo (Crucial for 5.1/7.1 on Stereo devices)
|
// Explicitly enable LAVC downmixing to stereo (Crucial for 5.1/7.1 on Stereo devices)
|
||||||
(_player.platform as dynamic)?.setProperty('ad-lavc-downmix', 'yes');
|
// (_player.platform as dynamic)?.setProperty('ad-lavc-downmix', 'yes');
|
||||||
(_player.platform as dynamic)?.setProperty('audio-channels', 'stereo');
|
// (_player.platform as dynamic)?.setProperty('audio-channels', 'stereo');
|
||||||
(_player.platform as dynamic)
|
// (_player.platform as dynamic)?.setProperty('audio-normalize-downmix', 'yes');
|
||||||
?.setProperty('audio-normalize-downmix', 'yes');
|
|
||||||
|
|
||||||
// Force software decoding for audio tracks (Maximum compatibility)
|
// Force software decoding for audio tracks (Maximum compatibility)
|
||||||
(_player.platform as dynamic)?.setProperty('ad', 'lavc:*');
|
(_player.platform as dynamic)?.setProperty('ad', 'lavc:*');
|
||||||
@@ -418,7 +418,7 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
// Remove lifecycle observer
|
// Remove lifecycle observer
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
// WidgetsBinding.instance.removeObserver(this);
|
||||||
|
|
||||||
// Unregister keyboard handler
|
// Unregister keyboard handler
|
||||||
HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
|
HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
|
||||||
@@ -437,6 +437,12 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
|
|||||||
_player.stop();
|
_player.stop();
|
||||||
_player.dispose();
|
_player.dispose();
|
||||||
_xtreamService?.dispose();
|
_xtreamService?.dispose();
|
||||||
|
_playPauseFocusNode.dispose();
|
||||||
|
_prevFocusNode.dispose();
|
||||||
|
_nextFocusNode.dispose();
|
||||||
|
_sliderFocusNode.dispose();
|
||||||
|
_backFocusNode.dispose();
|
||||||
|
_audioFocusNode.dispose();
|
||||||
|
|
||||||
// Restore normal orientation
|
// Restore normal orientation
|
||||||
SystemChrome.setPreferredOrientations([
|
SystemChrome.setPreferredOrientations([
|
||||||
@@ -819,108 +825,129 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
|
|||||||
bool _handleKeyEvent(KeyEvent event) {
|
bool _handleKeyEvent(KeyEvent event) {
|
||||||
if (event is! KeyDownEvent) return false;
|
if (event is! KeyDownEvent) return false;
|
||||||
|
|
||||||
final key = event.logicalKey;
|
try {
|
||||||
|
final key = event.logicalKey;
|
||||||
|
|
||||||
// Space / Enter / Select
|
// Explicit Play
|
||||||
if (key == LogicalKeyboardKey.space ||
|
if (key == LogicalKeyboardKey.mediaPlay) {
|
||||||
key == LogicalKeyboardKey.enter ||
|
if (!_isPlaying) _player.play();
|
||||||
key == LogicalKeyboardKey.select ||
|
|
||||||
key == LogicalKeyboardKey.numpadEnter ||
|
|
||||||
key == LogicalKeyboardKey.gameButtonA) {
|
|
||||||
// If controls are hidden, toggle them + play/pause
|
|
||||||
if (!_showControls) {
|
|
||||||
_togglePlayPause();
|
|
||||||
_toggleControls();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// If controls are visible, let Focus system handle button press
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debug Toggle: Long Press Key 'D' or 0 (if mapped) - Simplified to just a secret key combo?
|
|
||||||
// Let's use a long-press logic on Select in _handleKeyEvent?
|
|
||||||
// Actually, handling long press in key down is hard.
|
|
||||||
// We'll add a 'Debug' button in the settings or just map Key '0' if remote has it.
|
|
||||||
// Debug Toggle: Long Press Key 'D' or 0 (if mapped)
|
|
||||||
if (key == LogicalKeyboardKey.digit0) {
|
|
||||||
final current = ref.read(mobileSettingsProvider).showDebugStats;
|
|
||||||
ref.read(mobileSettingsProvider.notifier).toggleShowDebugStats(!current);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Left Arrow - Show controls or navigate, seek in VOD when hidden
|
|
||||||
if (key == LogicalKeyboardKey.arrowLeft) {
|
|
||||||
if (!_showControls) {
|
|
||||||
if (widget.streamType != StreamType.live) {
|
|
||||||
final newPos = _position - const Duration(seconds: 5);
|
|
||||||
_player.seek(newPos < Duration.zero ? Duration.zero : newPos);
|
|
||||||
}
|
|
||||||
_onUserInteraction();
|
_onUserInteraction();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false; // Let Focus handle navigation
|
|
||||||
}
|
|
||||||
|
|
||||||
// Right Arrow - Show controls or navigate, seek in VOD when hidden
|
// Explicit Pause
|
||||||
if (key == LogicalKeyboardKey.arrowRight) {
|
if (key == LogicalKeyboardKey.mediaPause) {
|
||||||
if (!_showControls) {
|
if (_isPlaying) _player.pause();
|
||||||
if (widget.streamType != StreamType.live) {
|
_onUserInteraction();
|
||||||
final newPos = _position + const Duration(seconds: 5);
|
return true;
|
||||||
if (newPos < _duration) {
|
}
|
||||||
_player.seek(newPos);
|
|
||||||
|
// Space / Enter / Select / Toggle
|
||||||
|
if (key == LogicalKeyboardKey.space ||
|
||||||
|
key == LogicalKeyboardKey.enter ||
|
||||||
|
key == LogicalKeyboardKey.select ||
|
||||||
|
key == LogicalKeyboardKey.numpadEnter ||
|
||||||
|
key == LogicalKeyboardKey.gameButtonA ||
|
||||||
|
key == LogicalKeyboardKey.mediaPlayPause) {
|
||||||
|
// If controls are hidden, toggle them + play/pause
|
||||||
|
if (!_showControls) {
|
||||||
|
_togglePlayPause();
|
||||||
|
_toggleControls();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// If controls are visible, let Focus system handle button press
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug Toggle: Long Press Key 'D' or 0 (if mapped) - Simplified to just a secret key combo?
|
||||||
|
// Let's use a long-press logic on Select in _handleKeyEvent?
|
||||||
|
// Actually, handling long press in key down is hard.
|
||||||
|
// We'll add a 'Debug' button in the settings or just map Key '0' if remote has it.
|
||||||
|
// Debug Toggle: Long Press Key 'D' or 0 (if mapped)
|
||||||
|
if (key == LogicalKeyboardKey.digit0) {
|
||||||
|
final current = ref.read(mobileSettingsProvider).showDebugStats;
|
||||||
|
ref
|
||||||
|
.read(mobileSettingsProvider.notifier)
|
||||||
|
.toggleShowDebugStats(!current);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Left Arrow - Show controls or navigate, seek in VOD when hidden
|
||||||
|
if (key == LogicalKeyboardKey.arrowLeft) {
|
||||||
|
if (!_showControls) {
|
||||||
|
if (widget.streamType != StreamType.live) {
|
||||||
|
final newPos = _position - const Duration(seconds: 10);
|
||||||
|
_player.seek(newPos < Duration.zero ? Duration.zero : newPos);
|
||||||
}
|
}
|
||||||
|
_onUserInteraction();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
_onUserInteraction();
|
return false; // Let Focus handle navigation
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right Arrow - Show controls or navigate, seek in VOD when hidden
|
||||||
|
if (key == LogicalKeyboardKey.arrowRight) {
|
||||||
|
if (!_showControls) {
|
||||||
|
if (widget.streamType != StreamType.live) {
|
||||||
|
final newPos = _position + const Duration(seconds: 10);
|
||||||
|
if (newPos < _duration) {
|
||||||
|
_player.seek(newPos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_onUserInteraction();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false; // Let Focus handle navigation
|
||||||
|
}
|
||||||
|
|
||||||
|
// Up/Down Arrow - Show controls or navigate UI, NO zapping
|
||||||
|
if (key == LogicalKeyboardKey.arrowUp ||
|
||||||
|
key == LogicalKeyboardKey.arrowDown) {
|
||||||
|
if (!_showControls) {
|
||||||
|
_onUserInteraction();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false; // Let Focus handle navigation
|
||||||
|
}
|
||||||
|
|
||||||
|
// Channel Down = Previous Channel (inverted as requested)
|
||||||
|
if (key == LogicalKeyboardKey.channelDown) {
|
||||||
|
if (widget.streamType == StreamType.live && widget.channels != null) {
|
||||||
|
_playPrevious();
|
||||||
|
_onUserInteraction();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Channel Up = Next Channel (inverted as requested)
|
||||||
|
if (key == LogicalKeyboardKey.channelUp) {
|
||||||
|
if (widget.streamType == StreamType.live && widget.channels != null) {
|
||||||
|
_playNext();
|
||||||
|
_onUserInteraction();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cycle Aspect Ratio - secret key 'A' or digit 1
|
||||||
|
if (key == LogicalKeyboardKey.keyA || key == LogicalKeyboardKey.digit1) {
|
||||||
|
_cycleAspectRatio();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false; // Let Focus handle navigation
|
|
||||||
}
|
|
||||||
|
|
||||||
// Up/Down Arrow - Show controls or navigate UI, NO zapping
|
// Escape / Back - Exit player
|
||||||
if (key == LogicalKeyboardKey.arrowUp ||
|
if (key == LogicalKeyboardKey.escape ||
|
||||||
key == LogicalKeyboardKey.arrowDown) {
|
key == LogicalKeyboardKey.goBack) {
|
||||||
if (!_showControls) {
|
// If controls are visible, close them first
|
||||||
_onUserInteraction();
|
if (_showControls) {
|
||||||
|
setState(() => _showControls = false);
|
||||||
|
} else {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false; // Let Focus handle navigation
|
} catch (e) {
|
||||||
|
debugPrint('Error in _handleKeyEvent: $e');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Channel Down = Previous Channel (inverted as requested)
|
|
||||||
if (key == LogicalKeyboardKey.channelDown) {
|
|
||||||
if (widget.streamType == StreamType.live && widget.channels != null) {
|
|
||||||
_playPrevious();
|
|
||||||
_onUserInteraction();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Channel Up = Next Channel (inverted as requested)
|
|
||||||
if (key == LogicalKeyboardKey.channelUp) {
|
|
||||||
if (widget.streamType == StreamType.live && widget.channels != null) {
|
|
||||||
_playNext();
|
|
||||||
_onUserInteraction();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cycle Aspect Ratio - secret key 'A' or digit 1
|
|
||||||
if (key == LogicalKeyboardKey.keyA || key == LogicalKeyboardKey.digit1) {
|
|
||||||
_cycleAspectRatio();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Escape / Back - Exit player
|
|
||||||
if (key == LogicalKeyboardKey.escape || key == LogicalKeyboardKey.goBack) {
|
|
||||||
// If controls are visible, close them first
|
|
||||||
if (_showControls) {
|
|
||||||
setState(() => _showControls = false);
|
|
||||||
} else {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1274,7 +1301,6 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
|
|||||||
const Icon(Icons.tv, color: Colors.white38),
|
const Icon(Icons.tv, color: Colors.white38),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -1303,23 +1329,6 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Live Badge
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
||||||
margin: const EdgeInsets.only(left: 8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.red,
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
),
|
|
||||||
child: const Text(
|
|
||||||
'LIVE',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (nextPlaying.isNotEmpty)
|
if (nextPlaying.isNotEmpty)
|
||||||
@@ -1363,7 +1372,7 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
|
|||||||
focusNode: _prevFocusNode,
|
focusNode: _prevFocusNode,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final pos = await _player.stream.position.first;
|
final pos = await _player.stream.position.first;
|
||||||
_player.seek(pos - const Duration(seconds: 5));
|
_player.seek(pos - const Duration(seconds: 10));
|
||||||
_resetControlsTimer();
|
_resetControlsTimer();
|
||||||
},
|
},
|
||||||
onFocus: _resetControlsTimer,
|
onFocus: _resetControlsTimer,
|
||||||
@@ -1376,7 +1385,7 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
|
|||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final pos = await _player.stream.position.first;
|
final pos = await _player.stream.position.first;
|
||||||
_player.seek(pos - const Duration(seconds: 5));
|
_player.seek(pos - const Duration(seconds: 10));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1409,7 +1418,7 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
|
|||||||
focusNode: _nextFocusNode,
|
focusNode: _nextFocusNode,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final pos = await _player.stream.position.first;
|
final pos = await _player.stream.position.first;
|
||||||
_player.seek(pos + const Duration(seconds: 5));
|
_player.seek(pos + const Duration(seconds: 10));
|
||||||
_resetControlsTimer();
|
_resetControlsTimer();
|
||||||
},
|
},
|
||||||
onFocus: _resetControlsTimer,
|
onFocus: _resetControlsTimer,
|
||||||
@@ -1422,7 +1431,7 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
|
|||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final pos = await _player.stream.position.first;
|
final pos = await _player.stream.position.first;
|
||||||
_player.seek(pos + const Duration(seconds: 5));
|
_player.seek(pos + const Duration(seconds: 10));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1458,48 +1467,54 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Actions(
|
child: Focus(
|
||||||
actions: <Type, Action<Intent>>{
|
focusNode: _sliderFocusNode,
|
||||||
DirectionalFocusIntent:
|
descendantsAreFocusable: false,
|
||||||
CallbackAction<DirectionalFocusIntent>(
|
onKeyEvent: (node, event) {
|
||||||
onInvoke: (intent) {
|
if (event is! KeyDownEvent) {
|
||||||
if (intent.direction ==
|
return KeyEventResult.ignored;
|
||||||
TraversalDirection.up ||
|
}
|
||||||
intent.direction ==
|
|
||||||
TraversalDirection.down) {
|
if (event.logicalKey ==
|
||||||
FocusScope.of(context)
|
LogicalKeyboardKey.arrowUp) {
|
||||||
.focusInDirection(intent.direction);
|
_playPauseFocusNode.requestFocus();
|
||||||
return null;
|
return KeyEventResult.handled;
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
},
|
if (event.logicalKey ==
|
||||||
),
|
LogicalKeyboardKey.arrowLeft) {
|
||||||
|
final newPos =
|
||||||
|
_position - const Duration(seconds: 10);
|
||||||
|
_player.seek(newPos < Duration.zero
|
||||||
|
? Duration.zero
|
||||||
|
: newPos);
|
||||||
|
_resetControlsTimer();
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.logicalKey ==
|
||||||
|
LogicalKeyboardKey.arrowRight) {
|
||||||
|
final newPos =
|
||||||
|
_position + const Duration(seconds: 10);
|
||||||
|
if (newPos < _duration) _player.seek(newPos);
|
||||||
|
_resetControlsTimer();
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
|
||||||
|
return KeyEventResult.ignored;
|
||||||
},
|
},
|
||||||
child: Shortcuts(
|
child: Slider(
|
||||||
shortcuts: <LogicalKeySet, Intent>{
|
value: _position.inSeconds
|
||||||
LogicalKeySet(LogicalKeyboardKey.arrowUp):
|
.toDouble()
|
||||||
const DirectionalFocusIntent(
|
.clamp(0, _duration.inSeconds.toDouble()),
|
||||||
TraversalDirection.up,
|
min: 0.0,
|
||||||
),
|
max: _duration.inSeconds.toDouble(),
|
||||||
LogicalKeySet(LogicalKeyboardKey.arrowDown):
|
onChanged: (value) {
|
||||||
const DirectionalFocusIntent(
|
_player.seek(Duration(seconds: value.toInt()));
|
||||||
TraversalDirection.down,
|
_resetControlsTimer();
|
||||||
),
|
|
||||||
},
|
},
|
||||||
child: Slider(
|
activeColor: AppColors.primary,
|
||||||
value: _position.inSeconds
|
inactiveColor: Colors.white24,
|
||||||
.toDouble()
|
|
||||||
.clamp(0, _duration.inSeconds.toDouble()),
|
|
||||||
min: 0.0,
|
|
||||||
max: _duration.inSeconds.toDouble(),
|
|
||||||
onChanged: (value) {
|
|
||||||
_player
|
|
||||||
.seek(Duration(seconds: value.toInt()));
|
|
||||||
_resetControlsTimer();
|
|
||||||
},
|
|
||||||
activeColor: AppColors.primary,
|
|
||||||
inactiveColor: Colors.white24,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in new issue
Block a user