fix(player): 隔离播放资源所有权并修复黑屏
This commit is contained in:
1 parent
54eb5a60a8
commit
bb2c729a7e
9 files changed
+223
-25
No files matched your search
@@ -15,6 +15,16 @@ class PlaybackCoordinator {
|
|||||||
if (!valid()) return false;
|
if (!valid()) return false;
|
||||||
if (_owner != session) {
|
if (_owner != session) {
|
||||||
final previous = _owner;
|
final previous = _owner;
|
||||||
|
if (previous != null && previous.owner != session.owner) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
debugPrint(
|
||||||
|
'[player-owner] phase=reject-coordinator-handoff '
|
||||||
|
'owner=${session.owner.name} '
|
||||||
|
'activeOwner=${previous.owner.name}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
await previous?._releasePlayers();
|
await previous?._releasePlayers();
|
||||||
if (previous != null && previous.livePlayerCount != 0) return false;
|
if (previous != null && previous.livePlayerCount != 0) return false;
|
||||||
_owner = null;
|
_owner = null;
|
||||||
@@ -56,6 +66,10 @@ Future<void> _parkNeighbors(
|
|||||||
|
|
||||||
Future<void> _placeNeighbor(PlaybackSession session, _PlayerSlot? slot) async {
|
Future<void> _placeNeighbor(PlaybackSession session, _PlayerSlot? slot) async {
|
||||||
if (slot == null || slot == session._active || slot.retiring) return;
|
if (slot == null || slot == session._active || slot.retiring) return;
|
||||||
|
if (slot.owner != session.owner) {
|
||||||
|
session._logOwnerRejection('park', slot);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (session._next == null && slot.index == session._nextIndex) {
|
if (session._next == null && slot.index == session._nextIndex) {
|
||||||
session._next = slot;
|
session._next = slot;
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ typedef PlayerFactory = PlayerPort Function(Episode episode);
|
|||||||
typedef PlayerPreparation =
|
typedef PlayerPreparation =
|
||||||
Future<void> Function(Episode episode, PlayerPort player);
|
Future<void> Function(Episode episode, PlayerPort player);
|
||||||
|
|
||||||
|
enum PlaybackSessionOwner { home, player }
|
||||||
|
|
||||||
/// Owns current plus previous/next frames; allocation stays serialized.
|
/// Owns current plus previous/next frames; allocation stays serialized.
|
||||||
class PlaybackSession extends ChangeNotifier {
|
class PlaybackSession extends ChangeNotifier {
|
||||||
PlaybackSession({
|
PlaybackSession({
|
||||||
@@ -27,6 +29,8 @@ class PlaybackSession extends ChangeNotifier {
|
|||||||
this.activate,
|
this.activate,
|
||||||
PlaybackCoordinator? coordinator,
|
PlaybackCoordinator? coordinator,
|
||||||
this.waitForFrame,
|
this.waitForFrame,
|
||||||
|
this.owner = PlaybackSessionOwner.player,
|
||||||
|
this.dramaIdAt,
|
||||||
}) : _coordinator = coordinator ?? PlaybackCoordinator();
|
}) : _coordinator = coordinator ?? PlaybackCoordinator();
|
||||||
|
|
||||||
final List<Episode> episodes;
|
final List<Episode> episodes;
|
||||||
@@ -39,6 +43,8 @@ class PlaybackSession extends ChangeNotifier {
|
|||||||
/// Runs only when a slot becomes current (e.g. progress listeners).
|
/// Runs only when a slot becomes current (e.g. progress listeners).
|
||||||
final PlayerPreparation? activate;
|
final PlayerPreparation? activate;
|
||||||
final Future<void> Function()? waitForFrame;
|
final Future<void> Function()? waitForFrame;
|
||||||
|
final PlaybackSessionOwner owner;
|
||||||
|
final String? Function(int index)? dramaIdAt;
|
||||||
final PlaybackCoordinator _coordinator;
|
final PlaybackCoordinator _coordinator;
|
||||||
final Set<_PlayerSlot> _live = {};
|
final Set<_PlayerSlot> _live = {};
|
||||||
_PlayerSlot? _active;
|
_PlayerSlot? _active;
|
||||||
@@ -161,7 +167,12 @@ class PlaybackSession extends ChangeNotifier {
|
|||||||
if (_live.length >= maxLivePlayers) {
|
if (_live.length >= maxLivePlayers) {
|
||||||
throw StateError('Playback resource limit reached');
|
throw StateError('Playback resource limit reached');
|
||||||
}
|
}
|
||||||
final slot = _PlayerSlot(index, factory(episodes[index]));
|
final slot = _PlayerSlot(
|
||||||
|
index,
|
||||||
|
factory(episodes[index]),
|
||||||
|
owner: owner,
|
||||||
|
dramaId: dramaIdAt?.call(index),
|
||||||
|
);
|
||||||
_live.add(slot);
|
_live.add(slot);
|
||||||
slot.ready = () async {
|
slot.ready = () async {
|
||||||
try {
|
try {
|
||||||
@@ -180,7 +191,19 @@ class PlaybackSession extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _retire(_PlayerSlot? slot) async {
|
Future<void> _retire(_PlayerSlot? slot) async {
|
||||||
if (slot == null || !_live.contains(slot)) return;
|
if (slot == null) return;
|
||||||
|
if (slot.owner != owner) {
|
||||||
|
_logOwnerRejection('retire', slot);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!_live.contains(slot)) return;
|
||||||
|
if (kDebugMode) {
|
||||||
|
debugPrint(
|
||||||
|
'[player-owner] phase=retire owner=${slot.owner.name} '
|
||||||
|
'drama=${slot.dramaId ?? '-'} episodeIndex=${slot.index} '
|
||||||
|
'controller=${_playerIdentity(slot.player)}',
|
||||||
|
);
|
||||||
|
}
|
||||||
slot.retiring = true;
|
slot.retiring = true;
|
||||||
final cleared =
|
final cleared =
|
||||||
_active == slot ||
|
_active == slot ||
|
||||||
@@ -460,6 +483,15 @@ class PlaybackSession extends ChangeNotifier {
|
|||||||
if (!_closed) notifyListeners();
|
if (!_closed) notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _logOwnerRejection(String phase, _PlayerSlot slot) {
|
||||||
|
if (!kDebugMode) return;
|
||||||
|
debugPrint(
|
||||||
|
'[player-owner] phase=reject-$phase expected=${owner.name} '
|
||||||
|
'actual=${slot.owner.name} drama=${slot.dramaId ?? '-'} '
|
||||||
|
'episodeIndex=${slot.index} controller=${_playerIdentity(slot.player)}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> close() async {
|
Future<void> close() async {
|
||||||
if (_closed) return;
|
if (_closed) return;
|
||||||
_closed = true;
|
_closed = true;
|
||||||
@@ -470,9 +502,16 @@ class PlaybackSession extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _PlayerSlot {
|
class _PlayerSlot {
|
||||||
_PlayerSlot(this.index, this.player);
|
_PlayerSlot(
|
||||||
|
this.index,
|
||||||
|
this.player, {
|
||||||
|
required this.owner,
|
||||||
|
required this.dramaId,
|
||||||
|
});
|
||||||
int index;
|
int index;
|
||||||
final PlayerPort player;
|
final PlayerPort player;
|
||||||
|
final PlaybackSessionOwner owner;
|
||||||
|
final String? dramaId;
|
||||||
late final Future<void> ready;
|
late final Future<void> ready;
|
||||||
bool prepared = false;
|
bool prepared = false;
|
||||||
bool activated = false;
|
bool activated = false;
|
||||||
@@ -481,3 +520,9 @@ class _PlayerSlot {
|
|||||||
bool reusedWarm = false;
|
bool reusedWarm = false;
|
||||||
Object? failure;
|
Object? failure;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _playerIdentity(PlayerPort player) {
|
||||||
|
return player is VideoPlayerPort
|
||||||
|
? '${identityHashCode(player.controller)}'
|
||||||
|
: '${identityHashCode(player)}';
|
||||||
|
}
|
||||||
@@ -290,6 +290,10 @@ class PlayerLogic extends ChangeNotifier {
|
|||||||
activate: activatePlayer,
|
activate: activatePlayer,
|
||||||
coordinator: coordinator,
|
coordinator: coordinator,
|
||||||
waitForFrame: _waitForFrame,
|
waitForFrame: _waitForFrame,
|
||||||
|
owner: homeMode ? PlaybackSessionOwner.home : PlaybackSessionOwner.player,
|
||||||
|
dramaIdAt: (index) => index >= 0 && index < queue.length
|
||||||
|
? queue.entryAt(index).drama.id
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1231,7 +1235,6 @@ class PlayerLogic extends ChangeNotifier {
|
|||||||
initialIndex: index,
|
initialIndex: index,
|
||||||
wallet: wallet,
|
wallet: wallet,
|
||||||
downloads: downloads,
|
downloads: downloads,
|
||||||
isolatePlaybackSession: true,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ class PlayerPage extends StatefulWidget {
|
|||||||
this.wallet,
|
this.wallet,
|
||||||
this.downloads,
|
this.downloads,
|
||||||
this.handoff,
|
this.handoff,
|
||||||
this.isolatePlaybackSession = false,
|
this.revealAfterFirstFrame = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Drama drama;
|
final Drama drama;
|
||||||
@@ -118,7 +118,7 @@ class PlayerPage extends StatefulWidget {
|
|||||||
final WalletController? wallet;
|
final WalletController? wallet;
|
||||||
final DownloadController? downloads;
|
final DownloadController? downloads;
|
||||||
final PlayerPageHandoff? handoff;
|
final PlayerPageHandoff? handoff;
|
||||||
final bool isolatePlaybackSession;
|
final bool revealAfterFirstFrame;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PlayerPage> createState() => _PlayerPageState();
|
State<PlayerPage> createState() => _PlayerPageState();
|
||||||
@@ -141,6 +141,7 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
DateTime? _swipeStartedAt;
|
DateTime? _swipeStartedAt;
|
||||||
bool _swipePrewarmCaptured = false;
|
bool _swipePrewarmCaptured = false;
|
||||||
bool _presentationActive = false;
|
bool _presentationActive = false;
|
||||||
|
late bool _firstFrameReady;
|
||||||
bool _leavingHandoff = false;
|
bool _leavingHandoff = false;
|
||||||
bool _preparingFullPlayer = false;
|
bool _preparingFullPlayer = false;
|
||||||
final Set<String> _precachedPosterUrls = {};
|
final Set<String> _precachedPosterUrls = {};
|
||||||
@@ -152,6 +153,7 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_firstFrameReady = !widget.revealAfterFirstFrame;
|
||||||
final handoff = widget.handoff;
|
final handoff = widget.handoff;
|
||||||
logic = PlayerBinding().create(
|
logic = PlayerBinding().create(
|
||||||
history: context.read<WatchHistoryRepository>(),
|
history: context.read<WatchHistoryRepository>(),
|
||||||
@@ -169,12 +171,18 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
logic.start(
|
logic.start(
|
||||||
factory: context.read<PlayerFactory>(),
|
factory: context.read<PlayerFactory>(),
|
||||||
coordinator: handoff == null && !widget.isolatePlaybackSession
|
coordinator: widget.embedded
|
||||||
? context.read<PlaybackCoordinator>()
|
? context.read<PlaybackCoordinator>()
|
||||||
: PlaybackCoordinator(),
|
: PlaybackCoordinator(),
|
||||||
initialUserWantsPlay: handoff?.sourceUserWantsPlay ?? true,
|
initialUserWantsPlay: handoff?.sourceUserWantsPlay ?? true,
|
||||||
);
|
);
|
||||||
_pageController = logic.pages;
|
_pageController = logic.pages;
|
||||||
|
if (kDebugMode && widget.revealAfterFirstFrame) {
|
||||||
|
debugPrint(
|
||||||
|
'[player-entry] phase=waiting-first-frame '
|
||||||
|
'session=${identityHashCode(logic.session)}',
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -594,6 +602,7 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
PlayerRenderStack(
|
PlayerRenderStack(
|
||||||
pageController: _pageController,
|
pageController: _pageController,
|
||||||
slots: _renderSlots(indices),
|
slots: _renderSlots(indices),
|
||||||
|
onCurrentVideoVisible: _handleCurrentVideoVisible,
|
||||||
),
|
),
|
||||||
pageLayer,
|
pageLayer,
|
||||||
],
|
],
|
||||||
@@ -660,16 +669,37 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (widget.handoff == null) return player;
|
final revealContent =
|
||||||
|
!widget.revealAfterFirstFrame ||
|
||||||
|
_firstFrameReady ||
|
||||||
|
logic.session.isLocked ||
|
||||||
|
logic.ticketError != null ||
|
||||||
|
logic.blockedEpisodeId != null ||
|
||||||
|
logic.session.error != null;
|
||||||
|
final presentedPlayer = widget.revealAfterFirstFrame
|
||||||
|
? Opacity(opacity: revealContent ? 1 : 0, child: player)
|
||||||
|
: player;
|
||||||
|
if (widget.handoff == null) return presentedPlayer;
|
||||||
return PopScope<void>(
|
return PopScope<void>(
|
||||||
canPop: _leavingHandoff,
|
canPop: _leavingHandoff,
|
||||||
onPopInvokedWithResult: (didPop, _) {
|
onPopInvokedWithResult: (didPop, _) {
|
||||||
if (!didPop) unawaited(_leaveHandoff(context));
|
if (!didPop) unawaited(_leaveHandoff(context));
|
||||||
},
|
},
|
||||||
child: player,
|
child: presentedPlayer,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleCurrentVideoVisible() {
|
||||||
|
if (!mounted || _firstFrameReady) return;
|
||||||
|
setState(() => _firstFrameReady = true);
|
||||||
|
if (kDebugMode) {
|
||||||
|
debugPrint(
|
||||||
|
'[player-entry] phase=ready '
|
||||||
|
'session=${identityHashCode(logic.session)}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _openCurrentMore() {
|
void _openCurrentMore() {
|
||||||
final entry = logic.queue.entryAt(logic.session.currentIndex);
|
final entry = logic.queue.entryAt(logic.session.currentIndex);
|
||||||
_openMore(context, entry.episode);
|
_openMore(context, entry.episode);
|
||||||
@@ -1176,13 +1206,18 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
_logFullPlayerRoute('present');
|
_logFullPlayerRoute('present');
|
||||||
setState(() => _presentationActive = true);
|
setState(() => _presentationActive = true);
|
||||||
final route = Navigator.of(context).push<void>(
|
final route = Navigator.of(context).push<void>(
|
||||||
MaterialPageRoute<void>(
|
PageRouteBuilder<void>(
|
||||||
builder: (_) => PlayerPage(
|
opaque: false,
|
||||||
|
barrierDismissible: false,
|
||||||
|
transitionDuration: Duration.zero,
|
||||||
|
reverseTransitionDuration: Duration.zero,
|
||||||
|
pageBuilder: (_, _, _) => PlayerPage(
|
||||||
drama: seriesDrama,
|
drama: seriesDrama,
|
||||||
initialIndex: episodeIndex,
|
initialIndex: episodeIndex,
|
||||||
handoff: handoff,
|
handoff: handoff,
|
||||||
wallet: logic.wallet,
|
wallet: logic.wallet,
|
||||||
downloads: logic.downloads,
|
downloads: logic.downloads,
|
||||||
|
revealAfterFirstFrame: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -52,10 +52,12 @@ class PlayerRenderStack extends StatefulWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.pageController,
|
required this.pageController,
|
||||||
required this.slots,
|
required this.slots,
|
||||||
|
this.onCurrentVideoVisible,
|
||||||
});
|
});
|
||||||
|
|
||||||
final PageController pageController;
|
final PageController pageController;
|
||||||
final Map<PlayerRenderSlotRole, PlayerRenderSlotData> slots;
|
final Map<PlayerRenderSlotRole, PlayerRenderSlotData> slots;
|
||||||
|
final VoidCallback? onCurrentVideoVisible;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PlayerRenderStack> createState() => _PlayerRenderStackState();
|
State<PlayerRenderStack> createState() => _PlayerRenderStackState();
|
||||||
@@ -68,7 +70,10 @@ class _PlayerRenderStackState extends State<PlayerRenderStack> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_lastCurrentIndex = _currentSlot.index;
|
_lastCurrentIndex = _currentSlot.index;
|
||||||
_log('init current=${_lastCurrentIndex ?? '-'}');
|
_log(
|
||||||
|
'init current=${_lastCurrentIndex ?? '-'} '
|
||||||
|
'pageController=${identityHashCode(widget.pageController)}',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -86,7 +91,10 @@ class _PlayerRenderStackState extends State<PlayerRenderStack> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_log('dispose current=${_lastCurrentIndex ?? '-'}');
|
_log(
|
||||||
|
'dispose current=${_lastCurrentIndex ?? '-'} '
|
||||||
|
'pageController=${identityHashCode(widget.pageController)}',
|
||||||
|
);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,6 +197,9 @@ class _PlayerRenderStackState extends State<PlayerRenderStack> {
|
|||||||
fallback: slot.fallback,
|
fallback: slot.fallback,
|
||||||
warmSlotReused: slot.warmSlotReused,
|
warmSlotReused: slot.warmSlotReused,
|
||||||
waitForPlaybackFrame: slot.waitForPlaybackFrame,
|
waitForPlaybackFrame: slot.waitForPlaybackFrame,
|
||||||
|
onVideoVisible: slot.role == PlayerRenderSlotRole.current
|
||||||
|
? widget.onCurrentVideoVisible
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class PlayerVideoSurface extends StatefulWidget {
|
|||||||
this.debugSlot = 'default',
|
this.debugSlot = 'default',
|
||||||
this.debugIndex,
|
this.debugIndex,
|
||||||
this.showFallbackBackground = true,
|
this.showFallbackBackground = true,
|
||||||
|
this.onVideoVisible,
|
||||||
});
|
});
|
||||||
|
|
||||||
final VideoPlayerController? controller;
|
final VideoPlayerController? controller;
|
||||||
@@ -57,6 +58,7 @@ class PlayerVideoSurface extends StatefulWidget {
|
|||||||
final bool showFallbackBackground;
|
final bool showFallbackBackground;
|
||||||
final String videoUrl;
|
final String videoUrl;
|
||||||
final bool theater;
|
final bool theater;
|
||||||
|
final VoidCallback? onVideoVisible;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PlayerVideoSurface> createState() => _PlayerVideoSurfaceState();
|
State<PlayerVideoSurface> createState() => _PlayerVideoSurfaceState();
|
||||||
@@ -73,6 +75,7 @@ class _PlayerVideoSurfaceState extends State<PlayerVideoSurface>
|
|||||||
bool _confirmationScheduled = false;
|
bool _confirmationScheduled = false;
|
||||||
bool _transitioning = false;
|
bool _transitioning = false;
|
||||||
bool _videoVisible = false;
|
bool _videoVisible = false;
|
||||||
|
bool _videoVisibleNotified = false;
|
||||||
String? _lastLayerState;
|
String? _lastLayerState;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -279,6 +282,7 @@ class _PlayerVideoSurfaceState extends State<PlayerVideoSurface>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
_logLayer('video', widget.controller?.value);
|
_logLayer('video', widget.controller?.value);
|
||||||
|
_notifyVideoVisible();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (initial) {
|
if (initial) {
|
||||||
@@ -303,6 +307,7 @@ class _PlayerVideoSurfaceState extends State<PlayerVideoSurface>
|
|||||||
_placeholderSnapshot = null;
|
_placeholderSnapshot = null;
|
||||||
});
|
});
|
||||||
_logLayer('fade-end', widget.controller?.value);
|
_logLayer('fade-end', widget.controller?.value);
|
||||||
|
_notifyVideoVisible();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _resetForControllerChange() {
|
void _resetForControllerChange() {
|
||||||
@@ -314,9 +319,20 @@ class _PlayerVideoSurfaceState extends State<PlayerVideoSurface>
|
|||||||
_confirmationScheduled = false;
|
_confirmationScheduled = false;
|
||||||
_transitioning = false;
|
_transitioning = false;
|
||||||
_videoVisible = false;
|
_videoVisible = false;
|
||||||
|
_videoVisibleNotified = false;
|
||||||
_lastLayerState = null;
|
_lastLayerState = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _notifyVideoVisible() {
|
||||||
|
if (_videoVisibleNotified) return;
|
||||||
|
_videoVisibleNotified = true;
|
||||||
|
final callback = widget.onVideoVisible;
|
||||||
|
if (callback == null) return;
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (mounted) callback();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void _logLayer(String phase, VideoPlayerValue? value) {
|
void _logLayer(String phase, VideoPlayerValue? value) {
|
||||||
if (!kDebugMode) return;
|
if (!kDebugMode) return;
|
||||||
final fallback = phase == 'video' || phase == 'fade-end'
|
final fallback = phase == 'video' || phase == 'fade-end'
|
||||||
|
|||||||
@@ -32,6 +32,68 @@ void main() {
|
|||||||
await session.close();
|
await session.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should_not_retire_home_controller_when_player_switches_episodes', () async {
|
||||||
|
final factory = FakeFactory();
|
||||||
|
final home = PlaybackSession(
|
||||||
|
episodes: episodes,
|
||||||
|
factory: factory.create,
|
||||||
|
canPlay: (episode) => episode.isFree,
|
||||||
|
owner: PlaybackSessionOwner.home,
|
||||||
|
);
|
||||||
|
await home.select(0, preloadIndex: 1);
|
||||||
|
await home.preloadSettled;
|
||||||
|
final homePlayer = home.active;
|
||||||
|
expect(homePlayer, isNotNull);
|
||||||
|
|
||||||
|
final player = PlaybackSession(
|
||||||
|
episodes: episodes,
|
||||||
|
factory: factory.create,
|
||||||
|
canPlay: (episode) => episode.isFree,
|
||||||
|
owner: PlaybackSessionOwner.player,
|
||||||
|
);
|
||||||
|
await player.select(0, preloadIndex: 1);
|
||||||
|
await player.select(1, previousIndex: 0);
|
||||||
|
await player.preloadSettled;
|
||||||
|
|
||||||
|
expect(home.active, same(homePlayer));
|
||||||
|
expect(
|
||||||
|
factory.created
|
||||||
|
.firstWhere((candidate) => identical(candidate, homePlayer))
|
||||||
|
.disposed,
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
expect(player.active, isNot(same(homePlayer)));
|
||||||
|
await player.close();
|
||||||
|
await home.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should_keep_home_controller_alive_when_player_session_closes', () async {
|
||||||
|
final factory = FakeFactory();
|
||||||
|
final home = PlaybackSession(
|
||||||
|
episodes: episodes,
|
||||||
|
factory: factory.create,
|
||||||
|
canPlay: (episode) => episode.isFree,
|
||||||
|
owner: PlaybackSessionOwner.home,
|
||||||
|
);
|
||||||
|
await home.select(0);
|
||||||
|
final homePlayer = home.active! as FakePlayer;
|
||||||
|
|
||||||
|
final player = PlaybackSession(
|
||||||
|
episodes: episodes,
|
||||||
|
factory: factory.create,
|
||||||
|
canPlay: (episode) => episode.isFree,
|
||||||
|
owner: PlaybackSessionOwner.player,
|
||||||
|
);
|
||||||
|
await player.select(0);
|
||||||
|
await player.select(1);
|
||||||
|
await player.close();
|
||||||
|
|
||||||
|
expect(homePlayer.disposed, isFalse);
|
||||||
|
expect(home.active, same(homePlayer));
|
||||||
|
expect(home.currentIndex, 0);
|
||||||
|
await home.close();
|
||||||
|
});
|
||||||
|
|
||||||
test('should_release_previous_player_immediately_when_selecting_locked', () async {
|
test('should_release_previous_player_immediately_when_selecting_locked', () async {
|
||||||
final lockedEpisodes = [
|
final lockedEpisodes = [
|
||||||
Episode(
|
Episode(
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ void main() {
|
|||||||
await tester.tap(find.byKey(const Key('home-feed-full-series')));
|
await tester.tap(find.byKey(const Key('home-feed-full-series')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
final fullPlayer = find.byType(PlayerPage);
|
final fullPlayer = _fullPlayerFinder();
|
||||||
final fullLogic = _pageLogic(tester, fullPlayer);
|
final fullLogic = _pageLogic(tester, fullPlayer);
|
||||||
expect(fullLogic.session, isNot(same(sourceLogic.session)));
|
expect(fullLogic.session, isNot(same(sourceLogic.session)));
|
||||||
final fullPlayerPort = fullLogic.session.active! as VideoPlayerPort;
|
final fullPlayerPort = fullLogic.session.active! as VideoPlayerPort;
|
||||||
@@ -223,11 +223,17 @@ void main() {
|
|||||||
await tester.tap(find.byKey(const Key('home-feed-full-series')));
|
await tester.tap(find.byKey(const Key('home-feed-full-series')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.byType(PlayerPage), findsOneWidget);
|
expect(_fullPlayerFinder(), findsOneWidget);
|
||||||
expect(find.byKey(const Key('player-play-toggle')), findsOneWidget);
|
expect(find.byKey(const Key('player-play-toggle')), findsOneWidget);
|
||||||
expect(find.byKey(const Key('home-feed-center-play')), findsNothing);
|
expect(
|
||||||
expect(find.byKey(const Key('home-tabs-bar')), findsNothing);
|
find.byKey(const Key('home-feed-center-play')).hitTestable(),
|
||||||
expect(find.byKey(const Key('recommend-top-nav')), findsNothing);
|
findsNothing,
|
||||||
|
);
|
||||||
|
expect(find.byKey(const Key('home-tabs-bar')).hitTestable(), findsNothing);
|
||||||
|
expect(
|
||||||
|
find.byKey(const Key('recommend-top-nav')).hitTestable(),
|
||||||
|
findsNothing,
|
||||||
|
);
|
||||||
|
|
||||||
await tester.tap(find.byKey(const Key('player-back')));
|
await tester.tap(find.byKey(const Key('player-back')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
@@ -259,7 +265,7 @@ void main() {
|
|||||||
await tester.tap(find.byKey(const Key('home-feed-full-series')));
|
await tester.tap(find.byKey(const Key('home-feed-full-series')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
final fullLogic = _pageLogic(tester, find.byType(PlayerPage));
|
final fullLogic = _pageLogic(tester, _fullPlayerFinder());
|
||||||
await tester.drag(
|
await tester.drag(
|
||||||
find.byKey(const Key('player-video-tap')),
|
find.byKey(const Key('player-video-tap')),
|
||||||
const Offset(0, -560),
|
const Offset(0, -560),
|
||||||
@@ -1646,6 +1652,12 @@ PlayerLogic _pageLogic(WidgetTester tester, Finder page) {
|
|||||||
return state.logic as PlayerLogic;
|
return state.logic as PlayerLogic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Finder _fullPlayerFinder() {
|
||||||
|
return find.byWidgetPredicate(
|
||||||
|
(widget) => widget is PlayerPage && widget.handoff != null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
class _RouteVideoFactory {
|
class _RouteVideoFactory {
|
||||||
final List<_RouteVideoPlayer> created = [];
|
final List<_RouteVideoPlayer> created = [];
|
||||||
final List<Episode> episodes = [];
|
final List<Episode> episodes = [];
|
||||||
|
|||||||
@@ -471,13 +471,13 @@ void main() {
|
|||||||
|
|
||||||
await tester.tap(find.byKey(const Key('home-feed-series-name')));
|
await tester.tap(find.byKey(const Key('home-feed-series-name')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.byType(PlayerPage), findsOneWidget);
|
expect(find.byType(PlayerPage).last, findsOneWidget);
|
||||||
expect(
|
expect(
|
||||||
tester.widget<PlayerPage>(find.byType(PlayerPage)).homeMode,
|
tester.widget<PlayerPage>(find.byType(PlayerPage).last).homeMode,
|
||||||
isFalse,
|
isFalse,
|
||||||
);
|
);
|
||||||
expect(find.byKey(const Key('player-back')), findsOneWidget);
|
expect(find.byKey(const Key('player-back')), findsOneWidget);
|
||||||
expect(find.byKey(const Key('home-tabs-bar')), findsNothing);
|
expect(find.byKey(const Key('home-tabs-bar')).hitTestable(), findsNothing);
|
||||||
await tester.tap(find.byKey(const Key('player-back')));
|
await tester.tap(find.byKey(const Key('player-back')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.byKey(const Key('recommend-page')), findsOneWidget);
|
expect(find.byKey(const Key('recommend-page')), findsOneWidget);
|
||||||
@@ -485,13 +485,13 @@ void main() {
|
|||||||
|
|
||||||
await tester.tap(find.byKey(const Key('home-feed-full-series')));
|
await tester.tap(find.byKey(const Key('home-feed-full-series')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.byType(PlayerPage), findsOneWidget);
|
expect(find.byType(PlayerPage).last, findsOneWidget);
|
||||||
expect(
|
expect(
|
||||||
tester.widget<PlayerPage>(find.byType(PlayerPage)).homeMode,
|
tester.widget<PlayerPage>(find.byType(PlayerPage).last).homeMode,
|
||||||
isFalse,
|
isFalse,
|
||||||
);
|
);
|
||||||
expect(find.byKey(const Key('player-back')), findsOneWidget);
|
expect(find.byKey(const Key('player-back')), findsOneWidget);
|
||||||
expect(find.byKey(const Key('home-tabs-bar')), findsNothing);
|
expect(find.byKey(const Key('home-tabs-bar')).hitTestable(), findsNothing);
|
||||||
await tester.tap(find.byKey(const Key('player-back')));
|
await tester.tap(find.byKey(const Key('player-back')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in new issue
Block a user