fix(tv): retain live TV session while backgrounded
This commit is contained in:
@@ -182,14 +182,13 @@ extension _VideoPlayerLifecycleMethods on VideoPlayerScreenState {
|
||||
/// Arm the grace timer that releases the native AV pipeline if the app
|
||||
/// stays backgrounded (Android TV only). Returns whether it was armed.
|
||||
bool _armTvBackgroundPlayerSuspendTimer() {
|
||||
if (!Platform.isAndroid || !PlatformDetector.isTV()) return false;
|
||||
if (_playerSuspendedForTvBackground) return false;
|
||||
if (widget.isLive) {
|
||||
// Restore re-tunes via _switchLiveChannel(0); don't suspend a live
|
||||
// session the zap flow can't rebuild.
|
||||
final channels = widget.live?.channels;
|
||||
if (channels == null || channels.isEmpty) return false;
|
||||
if (_live.channelIndex < 0 || _live.channelIndex >= channels.length) return false;
|
||||
if (!shouldSuspendPlayerForTvBackground(
|
||||
isAndroid: Platform.isAndroid,
|
||||
isTv: PlatformDetector.isTV(),
|
||||
isLive: widget.isLive,
|
||||
alreadySuspended: _playerSuspendedForTvBackground,
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
_tvBackgroundPlayerSuspendTimer?.cancel();
|
||||
_tvBackgroundPlayerSuspendTimer = Timer(VideoPlayerScreenState._tvBackgroundPlayerSuspendGrace, () {
|
||||
@@ -215,6 +214,9 @@ extension _VideoPlayerLifecycleMethods on VideoPlayerScreenState {
|
||||
Future<void> _suspendPlayerForTvBackground() async {
|
||||
final currentPlayer = player;
|
||||
if (!mounted || currentPlayer == null || !_isPlayerInitialized) return;
|
||||
// A live stream's tuned session is also its time-shift buffer. Stopping
|
||||
// it would force a re-tune at the live edge and discard pause state.
|
||||
if (widget.isLive) return;
|
||||
if (_playerSuspendedForTvBackground || _shouldSkipForPip) return;
|
||||
final lifecycleState = WidgetsBinding.instance.lifecycleState;
|
||||
if (lifecycleState == AppLifecycleState.resumed || lifecycleState == AppLifecycleState.inactive) return;
|
||||
@@ -248,9 +250,9 @@ extension _VideoPlayerLifecycleMethods on VideoPlayerScreenState {
|
||||
/// a fresh playback decision, since the suspended stream URL may have
|
||||
/// expired server-side — and comes back paused; the caller's
|
||||
/// [_restoreMediaControlsAfterResume] then resumes it (with
|
||||
/// rewind-on-resume) exactly like a plain background pause. Live re-tunes
|
||||
/// the current channel through the zap flow, which starts playing at the
|
||||
/// live edge.
|
||||
/// rewind-on-resume) exactly like a plain background pause. Live sessions
|
||||
/// never enter this flow because their tuned session and capture-buffer
|
||||
/// position must remain intact across backgrounding.
|
||||
Future<void> _restorePlayerAfterTvBackgroundSuspend() async {
|
||||
_playerSuspendedForTvBackground = false;
|
||||
final resumePosition = _tvBackgroundSuspendPosition;
|
||||
@@ -265,12 +267,6 @@ extension _VideoPlayerLifecycleMethods on VideoPlayerScreenState {
|
||||
final currentPlayer = player;
|
||||
if (!mounted || currentPlayer == null || !_isPlayerInitialized) return;
|
||||
|
||||
if (widget.isLive) {
|
||||
_recordLifecycleState('resumed', action: 'tv_background_suspend_retune');
|
||||
await _switchLiveChannel(0);
|
||||
return;
|
||||
}
|
||||
|
||||
_recordLifecycleState('resumed', action: 'tv_background_suspend_reload');
|
||||
final reloaded = await _reloadMediaInPlace(
|
||||
metadata: _currentMetadata,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/// Whether backgrounding may release the native video pipeline after its
|
||||
/// grace period.
|
||||
///
|
||||
/// Live TV deliberately stays paused with its player open: the tuned session
|
||||
/// owns the capture buffer, so stopping it would lose both the time-shift
|
||||
/// position and the user's paused/playing intent when playback is rebuilt.
|
||||
bool shouldSuspendPlayerForTvBackground({
|
||||
required bool isAndroid,
|
||||
required bool isTv,
|
||||
required bool isLive,
|
||||
required bool alreadySuspended,
|
||||
}) {
|
||||
return isAndroid && isTv && !isLive && !alreadySuspended;
|
||||
}
|
||||
@@ -81,6 +81,7 @@ import 'video_player/completion_latch.dart';
|
||||
import 'video_player/frame_rate_matcher.dart';
|
||||
import 'video_player/live_tv_session_args.dart';
|
||||
import 'video_player/live_tv_session_state.dart';
|
||||
import 'video_player/tv_background_suspend_policy.dart';
|
||||
import 'video_player/widgets/player_prompt_overlays.dart';
|
||||
import '../widgets/overlay_sheet.dart';
|
||||
import '../widgets/video_controls/player_chrome_controller.dart';
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/screens/video_player/tv_background_suspend_policy.dart';
|
||||
|
||||
void main() {
|
||||
test('Android TV VOD releases the player after the background grace period', () {
|
||||
expect(
|
||||
shouldSuspendPlayerForTvBackground(isAndroid: true, isTv: true, isLive: false, alreadySuspended: false),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('live TV retains its tuned session and time-shift state', () {
|
||||
expect(
|
||||
shouldSuspendPlayerForTvBackground(isAndroid: true, isTv: true, isLive: true, alreadySuspended: false),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('non-Android and non-TV players do not use the TV suspend flow', () {
|
||||
expect(
|
||||
shouldSuspendPlayerForTvBackground(isAndroid: false, isTv: true, isLive: false, alreadySuspended: false),
|
||||
isFalse,
|
||||
);
|
||||
expect(
|
||||
shouldSuspendPlayerForTvBackground(isAndroid: true, isTv: false, isLive: false, alreadySuspended: false),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('an already suspended player is not scheduled again', () {
|
||||
expect(
|
||||
shouldSuspendPlayerForTvBackground(isAndroid: true, isTv: true, isLive: false, alreadySuspended: true),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user