From 915f75661f60e0ab399ede262cfb366eeaa63f19 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Sun, 21 Dec 2025 10:58:07 +0100 Subject: [PATCH] feat: Add Xtream API service with DNS resolution and caching, and new IPTV player screens. --- .../iptv/services/xtream_service_mobile.dart | 27 +++--- .../iptv/screens/lite_player_screen.dart | 67 +++++++++------ .../iptv/screens/native_player_screen.dart | 82 ++++++++++++------- 3 files changed, 110 insertions(+), 66 deletions(-) diff --git a/lib/features/iptv/services/xtream_service_mobile.dart b/lib/features/iptv/services/xtream_service_mobile.dart index 98f3290..8ebb648 100644 --- a/lib/features/iptv/services/xtream_service_mobile.dart +++ b/lib/features/iptv/services/xtream_service_mobile.dart @@ -74,7 +74,8 @@ class XtreamServiceMobile { print('XtreamServiceMobile: Will use IP $_resolvedIp for ${uri.host}'); } else { print( - 'XtreamServiceMobile: DNS resolution failed, will use hostname directly',); + 'XtreamServiceMobile: DNS resolution failed, will use hostname directly', + ); } } } @@ -125,12 +126,12 @@ class XtreamServiceMobile { PlaylistConfig? get currentPlaylist => _currentPlaylist; - /// Generate direct stream URL for live TV (HLS format for mobile compatibility) + /// Generate direct stream URL for live TV (MPEG-TS format for maximum compatibility) String getLiveStreamUrl(String streamId) { if (_currentPlaylist == null) throw Exception('No playlist configured'); - // Use .m3u8 format for HLS streaming (best mobile compatibility) - return '$_effectiveDnsBase/live/${_currentPlaylist!.username}/${_currentPlaylist!.password}/$streamId.m3u8'; + // Use .ts format (MPEG-TS) for better compatibility with more providers + return '$_effectiveDnsBase/live/${_currentPlaylist!.username}/${_currentPlaylist!.password}/$streamId.ts'; } /// Generate stream URL for VOD (movies) @@ -327,8 +328,10 @@ class XtreamServiceMobile { } /// Get movies with pagination support (uses in-memory cache for performance) - Future> getMoviesPaginated( - {int offset = 0, int limit = 100,}) async { + Future> getMoviesPaginated({ + int offset = 0, + int limit = 100, + }) async { if (_currentPlaylist == null) throw Exception('No playlist configured'); try { @@ -422,8 +425,10 @@ class XtreamServiceMobile { } /// Get series with pagination support (uses in-memory cache for performance) - Future> getSeriesPaginated( - {int offset = 0, int limit = 100,}) async { + Future> getSeriesPaginated({ + int offset = 0, + int limit = 100, + }) async { if (_currentPlaylist == null) throw Exception('No playlist configured'); try { @@ -584,8 +589,10 @@ class XtreamServiceMobile { /// Get short EPG as ShortEPG object (for EPGWidget) /// Caches results for 12 hours by default - Future getShortEPG(String streamId, - {bool forceRefresh = false,}) async { + Future getShortEPG( + String streamId, { + bool forceRefresh = false, + }) async { if (_currentPlaylist == null) throw Exception('No playlist configured'); try { diff --git a/lib/mobile/features/iptv/screens/lite_player_screen.dart b/lib/mobile/features/iptv/screens/lite_player_screen.dart index 92af70c..2f712b9 100644 --- a/lib/mobile/features/iptv/screens/lite_player_screen.dart +++ b/lib/mobile/features/iptv/screens/lite_player_screen.dart @@ -769,33 +769,50 @@ class _LitePlayerScreenState extends ConsumerState style: const TextStyle(color: Colors.white), ), Expanded( - child: Shortcuts( - shortcuts: { - LogicalKeySet(LogicalKeyboardKey.arrowUp): - const DirectionalFocusIntent(TraversalDirection.up), - LogicalKeySet(LogicalKeyboardKey.arrowDown): - const DirectionalFocusIntent( - TraversalDirection.down), + child: Actions( + actions: >{ + DirectionalFocusIntent: + CallbackAction( + onInvoke: (intent) { + if (intent.direction == TraversalDirection.up || + intent.direction == TraversalDirection.down) { + FocusScope.of(context) + .focusInDirection(intent.direction); + return null; + } + return null; + }, + ), }, - 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(); + child: Shortcuts( + shortcuts: { + 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(); + }, + ), ), ), ), diff --git a/lib/mobile/features/iptv/screens/native_player_screen.dart b/lib/mobile/features/iptv/screens/native_player_screen.dart index f5b2c54..95f5984 100644 --- a/lib/mobile/features/iptv/screens/native_player_screen.dart +++ b/lib/mobile/features/iptv/screens/native_player_screen.dart @@ -1343,39 +1343,59 @@ class _NativePlayerScreenState extends ConsumerState ), ), Expanded( - child: Shortcuts( - shortcuts: { - LogicalKeySet(LogicalKeyboardKey.arrowUp): - const DirectionalFocusIntent( - TraversalDirection.up), - LogicalKeySet(LogicalKeyboardKey.arrowDown): - const DirectionalFocusIntent( - TraversalDirection.down), + child: Actions( + actions: >{ + // Override directional focus intents for Up/Down + DirectionalFocusIntent: + CallbackAction( + onInvoke: (intent) { + if (intent.direction == + TraversalDirection.up || + intent.direction == + TraversalDirection.down) { + // Manually move focus in the requested direction + FocusScope.of(context) + .focusInDirection(intent.direction); + return null; + } + // For left/right, don't handle (let Slider do seeking) + return null; + }, + ), }, - 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(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(), + 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()); + }); + }, + ), ), ), ),