diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index d0902f04..eb2582c2 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -850,13 +850,36 @@ class VideoPlayerScreenState extends State with WidgetsBindin if (!mounted) return; // Live TV mode: bypass standard playback initialization - if (widget.isLive && widget.liveStreamUrl != null) { + if (widget.isLive) { try { _hasFirstFrame.value = false; await player!.requestAudioFocus(); await _setLiveStreamOptions(); - await player!.open(Media(widget.liveStreamUrl!, headers: const {'Accept-Language': 'en'}), play: true); + String streamUrl; + if (widget.liveStreamUrl != null) { + streamUrl = widget.liveStreamUrl!; + } else { + // Tune channel inside the player (shows loading spinner while tuning) + final channels = widget.liveChannels; + final channelIndex = _liveChannelIndex; + if (channels == null || channelIndex < 0 || channelIndex >= channels.length) { + throw Exception('No channel to tune'); + } + final channel = channels[channelIndex]; + final channelId = channel.identifier ?? channel.key; + final client = widget.liveClient!; + final result = await client.tuneChannel(widget.liveDvrKey!, channelId); + if (result == null) throw Exception('Failed to tune channel'); + + streamUrl = '${client.config.baseUrl}${result.streamPath}' + .withPlexToken(client.config.token); + + _liveSessionIdentifier = result.sessionIdentifier; + _liveSessionPath = result.sessionPath; + } + + await player!.open(Media(streamUrl, headers: const {'Accept-Language': 'en'}), play: true); if (mounted) { setState(() { @@ -865,6 +888,8 @@ class VideoPlayerScreenState extends State with WidgetsBindin _isPlayerInitialized = true; }); } + + _startLiveTimelineUpdates(); } catch (e) { appLogger.e('Failed to start live TV playback', error: e); if (mounted) { diff --git a/lib/utils/live_tv_player_navigation.dart b/lib/utils/live_tv_player_navigation.dart index f55d6114..d93ad1a1 100644 --- a/lib/utils/live_tv_player_navigation.dart +++ b/lib/utils/live_tv_player_navigation.dart @@ -1,16 +1,17 @@ import 'package:flutter/material.dart'; import '../models/livetv_channel.dart'; +import '../models/plex_metadata.dart'; import '../screens/video_player_screen.dart'; import '../services/plex_client.dart'; import '../utils/app_logger.dart'; -import '../utils/plex_url_helper.dart'; import '../utils/video_player_navigation.dart'; -/// Tune to a live TV channel and launch the video player. +/// Navigate to the video player for a live TV channel. /// -/// 1. Calls `tuneChannel()` to get metadata + stream path -/// 2. Navigates to the VideoPlayerScreen with `isLive: true` +/// Pushes the player screen immediately with a placeholder metadata. +/// The actual tuning (tune POST + decision GET) happens inside the player, +/// which shows a loading spinner while it works. /// /// [channels] is the full channel list for channel up/down navigation. Future navigateToLiveTv( @@ -21,43 +22,30 @@ Future navigateToLiveTv( List? channels, }) async { final channelId = channel.identifier ?? channel.key; - - final scaffoldMessenger = ScaffoldMessenger.of(context); final navigator = Navigator.of(context); - appLogger.d('Tuning to channel: ${channel.displayName} ($channelId)'); + appLogger.d('Navigating to live channel: ${channel.displayName} ($channelId)'); - final result = await client.tuneChannel(dvrKey, channelId); - - if (result == null) { - appLogger.e('Failed to tune channel $channelId'); - if (context.mounted) { - scaffoldMessenger.showSnackBar( - SnackBar(content: Text('Failed to tune to ${channel.displayName}')), - ); - } - return; - } - - final streamUrl = '${client.config.baseUrl}${result.streamPath}'.withPlexToken(client.config.token); - - if (!context.mounted) return; + final placeholder = PlexMetadata( + ratingKey: channelId, + key: channelId, + type: 'clip', + title: channel.displayName, + ); final route = PageRouteBuilder( settings: const RouteSettings(name: kVideoPlayerRouteName), pageBuilder: (context, animation, secondaryAnimation) => VideoPlayerScreen( - metadata: result.metadata, + metadata: placeholder, isLive: true, liveChannelName: channel.displayName, - liveStreamUrl: streamUrl, + liveStreamUrl: null, liveChannels: channels, liveCurrentChannelIndex: channels?.indexWhere( (ch) => (ch.identifier ?? ch.key) == channelId, ), liveDvrKey: dvrKey, liveClient: client, - liveSessionIdentifier: result.sessionIdentifier, - liveSessionPath: result.sessionPath, ), transitionDuration: Duration.zero, reverseTransitionDuration: Duration.zero,