mirror of
https://github.com/R0m1k3/xtremobile.git
synced 2026-10-23 07:08:16 +02:00
feat: XMLTV EPG fallback, Warm Cinema theme, background release
Align the Android app with xtremflow on three fronts. EPG: add XmltvService, a streaming XMLTV parser (gunzip-aware, indexed to a -3h/+18h window, JSON disk cache with a 6h TTL). XtreamServiceMobile now falls back through the provider's xmltv.php, a user-supplied URL, then a community guide whenever get_short_epg returns nothing. Channels are matched on epg_channel_id first, then on a normalized name (accents, HD/FHD/4K suffixes, "FR:" and "|FR|" prefixes). A streamId -> Channel index populated by getLiveChannels supplies those keys, so no getShortEPG call site changed. EPG requests are also funnelled through a 3-slot semaphore, as in xtremflow: a 30-tile grid used to fire 30 parallel player_api.php calls, which rate-limited panels answered with errors, emptying the guide for the whole screen. Theme: port the Warm Cinema charter (ember/charcoal/cream, Fraunces + Karla) into app_colors, app_theme and app_decorations. The 21 Apple TV colour names already in use are kept as compatibility aliases. mobile_theme now derives from AppTheme instead of duplicating its 390 lines. Playback lifecycle: stop reacting to AppLifecycleState.inactive, which fires on transient Android interruptions such as pulling down the notification shade and was killing playback. Both engines now release on paused/hidden/detached via _releasePlayback(): stream stopped, decoder released, timers cancelled, wakelock dropped. An _isReleased latch keeps the watchdog and reconnect paths from restarting a stream from the background. Long-run stability: cancel the six player stream subscriptions on dispose, cap demuxer-max-back-bytes at 8MB (mpv defaults to 50MiB, which combined with the 150MB readahead pushed the native heap toward an Android memory kill after a few hours), rebuild on position only while the controls are visible, and dispose the Lite player's focus nodes. Startup: prewarmHost() pre-resolves DNS and opens then closes a bare TCP/TLS socket before playback. No HTTP request is issued, so no provider connection slot is consumed. Capped at 1.5s. Verified: flutter analyze clean on all touched files; 19/19 tests, including a run over a real 46MB/69802-programme dump (517 channels indexed in 1.1s, 5/5 name probes matched); flutter build apk --release succeeds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
63e77c5bc5
commit
8d74bbb320
15 files changed
+2138
-852
No files matched your search
@@ -72,6 +72,10 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
|
||||
Timer? _controlsTimer;
|
||||
int _loadId = 0;
|
||||
|
||||
/// Set once the app is backgrounded, so a queued reload cannot restart the
|
||||
/// stream behind the user.
|
||||
bool _isReleased = false;
|
||||
|
||||
// Focus
|
||||
final FocusNode _playPauseFocusNode = FocusNode();
|
||||
final FocusNode _prevFocusNode = FocusNode();
|
||||
@@ -98,17 +102,45 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (_controller == null) return;
|
||||
if (state == AppLifecycleState.paused ||
|
||||
state == AppLifecycleState.inactive ||
|
||||
state == AppLifecycleState.detached) {
|
||||
_controller?.pause();
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
// `inactive` is deliberately NOT handled: on Android it fires for transient
|
||||
// interruptions (notification shade, call banner, permission dialog).
|
||||
final isBackgrounded = state == AppLifecycleState.paused ||
|
||||
state == AppLifecycleState.hidden ||
|
||||
state == AppLifecycleState.detached;
|
||||
if (!isBackgrounded) return;
|
||||
|
||||
_releasePlayback();
|
||||
|
||||
if (mounted && ModalRoute.of(context)?.isCurrent == true) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop playback and release the decoder when the app leaves the foreground.
|
||||
///
|
||||
/// `pause()` alone is not enough: ExoPlayer keeps filling its buffer, so the
|
||||
/// stream would still be pulled (and metered) in the background.
|
||||
void _releasePlayback() {
|
||||
if (_isReleased) return;
|
||||
_isReleased = true;
|
||||
|
||||
_clockTimer?.cancel();
|
||||
_clockTimer = null;
|
||||
_epgTimer?.cancel();
|
||||
_epgTimer = null;
|
||||
_controlsTimer?.cancel();
|
||||
_controlsTimer = null;
|
||||
_stabilizationTimer?.cancel();
|
||||
_stabilizationTimer = null;
|
||||
|
||||
final controller = _controller;
|
||||
_controller = null;
|
||||
controller?.pause();
|
||||
controller?.dispose();
|
||||
|
||||
WakelockPlus.disable();
|
||||
}
|
||||
|
||||
void _startClock() {
|
||||
_clockTimer?.cancel();
|
||||
_updateTime();
|
||||
@@ -132,6 +164,13 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
_xtreamService = XtreamServiceMobile(dir.path);
|
||||
await _xtreamService!.setPlaylistAsync(widget.playlist);
|
||||
|
||||
// Warm DNS + TCP/TLS to the panel host before the first request.
|
||||
await _xtreamService!.prewarmHost().timeout(
|
||||
const Duration(milliseconds: 1500),
|
||||
onTimeout: () {},
|
||||
);
|
||||
|
||||
_loadStream();
|
||||
|
||||
if (widget.streamType == model.StreamType.live) {
|
||||
@@ -160,6 +199,8 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
|
||||
|
||||
Future<void> _loadStream() async {
|
||||
if (_xtreamService == null) return;
|
||||
// Backgrounded while a (re)load was queued — do not resurrect the stream.
|
||||
if (_isReleased || !mounted) return;
|
||||
|
||||
await _controller?.dispose();
|
||||
_controller = null;
|
||||
@@ -359,6 +400,11 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
|
||||
_controller?.pause(); // Force stop audio immediately
|
||||
_controller?.dispose();
|
||||
_xtreamService?.dispose();
|
||||
|
||||
_playPauseFocusNode.dispose();
|
||||
_prevFocusNode.dispose();
|
||||
_nextFocusNode.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user