fix(player): 规范预热回收日志并抓帧过渡
This commit is contained in:
1 parent
bb2c729a7e
commit
4d3d067f77
6 files changed
+186
-40
No files matched your search
@@ -59,12 +59,16 @@ Future<void> _parkNeighbors(
|
|||||||
_PlayerSlot? warmNext,
|
_PlayerSlot? warmNext,
|
||||||
_PlayerSlot? warmPrevious,
|
_PlayerSlot? warmPrevious,
|
||||||
) async {
|
) async {
|
||||||
await _placeNeighbor(session, old);
|
await _placeNeighbor(session, old, roleHint: 'old-current');
|
||||||
await _placeNeighbor(session, warmNext);
|
await _placeNeighbor(session, warmNext, roleHint: 'next');
|
||||||
await _placeNeighbor(session, warmPrevious);
|
await _placeNeighbor(session, warmPrevious, roleHint: 'previous');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _placeNeighbor(PlaybackSession session, _PlayerSlot? slot) async {
|
Future<void> _placeNeighbor(
|
||||||
|
PlaybackSession session,
|
||||||
|
_PlayerSlot? slot, {
|
||||||
|
required String roleHint,
|
||||||
|
}) async {
|
||||||
if (slot == null || slot == session._active || slot.retiring) return;
|
if (slot == null || slot == session._active || slot.retiring) return;
|
||||||
if (slot.owner != session.owner) {
|
if (slot.owner != session.owner) {
|
||||||
session._logOwnerRejection('park', slot);
|
session._logOwnerRejection('park', slot);
|
||||||
@@ -78,7 +82,7 @@ Future<void> _placeNeighbor(PlaybackSession session, _PlayerSlot? slot) async {
|
|||||||
session._previous = slot;
|
session._previous = slot;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await session._retire(slot);
|
await session._retire(slot, trigger: 'park-neighbor', roleHint: roleHint);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 当前集已经出画后,按手势方向静默准备上一集和下一集。
|
/// 当前集已经出画后,按手势方向静默准备上一集和下一集。
|
||||||
@@ -118,7 +122,11 @@ Future<void> _warmOne(
|
|||||||
await session._enqueue(() async {
|
await session._enqueue(() async {
|
||||||
if (!_stillHeld(session, candidate, next: next)) return;
|
if (!_stillHeld(session, candidate, next: next)) return;
|
||||||
_clearNeighbor(session, next: next);
|
_clearNeighbor(session, next: next);
|
||||||
await session._retire(candidate);
|
await session._retire(
|
||||||
|
candidate,
|
||||||
|
trigger: 'warm-failure',
|
||||||
|
roleHint: next ? 'next' : 'previous',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +143,11 @@ Future<_PlayerSlot?> _allocateNeighbor(
|
|||||||
final current = next ? session._next : session._previous;
|
final current = next ? session._next : session._previous;
|
||||||
if (!allowed || current?.index != index) {
|
if (!allowed || current?.index != index) {
|
||||||
_clearNeighbor(session, next: next);
|
_clearNeighbor(session, next: next);
|
||||||
await session._retire(current);
|
await session._retire(
|
||||||
|
current,
|
||||||
|
trigger: 'warm-slot-invalid',
|
||||||
|
roleHint: next ? 'next' : 'previous',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (!_canAllocate(session, request, allowed: allowed, next: next)) {
|
if (!_canAllocate(session, request, allowed: allowed, next: next)) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -138,8 +138,12 @@ class PlaybackSession extends ChangeNotifier {
|
|||||||
_notify();
|
_notify();
|
||||||
unawaited(
|
unawaited(
|
||||||
_enqueue(() async {
|
_enqueue(() async {
|
||||||
await _retire(staleNext);
|
await _retire(staleNext, trigger: 'remap-stale-next', roleHint: 'next');
|
||||||
await _retire(stalePrevious);
|
await _retire(
|
||||||
|
stalePrevious,
|
||||||
|
trigger: 'remap-stale-previous',
|
||||||
|
roleHint: 'previous',
|
||||||
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
if (_visible && !_overlayOpen) _warmNeighbors(this);
|
if (_visible && !_overlayOpen) _warmNeighbors(this);
|
||||||
@@ -190,20 +194,23 @@ class PlaybackSession extends ChangeNotifier {
|
|||||||
return slot;
|
return slot;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _retire(_PlayerSlot? slot) async {
|
Future<void> _retire(
|
||||||
|
_PlayerSlot? slot, {
|
||||||
|
required String trigger,
|
||||||
|
String? roleHint,
|
||||||
|
}) async {
|
||||||
if (slot == null) return;
|
if (slot == null) return;
|
||||||
if (slot.owner != owner) {
|
if (slot.owner != owner) {
|
||||||
_logOwnerRejection('retire', slot);
|
_logOwnerRejection('retire', slot);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!_live.contains(slot)) return;
|
if (!_live.contains(slot)) return;
|
||||||
if (kDebugMode) {
|
final role = _slotRole(slot, roleHint: roleHint);
|
||||||
debugPrint(
|
final video = slot.player is VideoPlayerPort
|
||||||
'[player-owner] phase=retire owner=${slot.owner.name} '
|
? slot.player as VideoPlayerPort
|
||||||
'drama=${slot.dramaId ?? '-'} episodeIndex=${slot.index} '
|
: null;
|
||||||
'controller=${_playerIdentity(slot.player)}',
|
final value = video?.controller.value;
|
||||||
);
|
final slotPlaying = slot.playing;
|
||||||
}
|
|
||||||
slot.retiring = true;
|
slot.retiring = true;
|
||||||
final cleared =
|
final cleared =
|
||||||
_active == slot ||
|
_active == slot ||
|
||||||
@@ -222,6 +229,19 @@ class PlaybackSession extends ChangeNotifier {
|
|||||||
} finally {
|
} finally {
|
||||||
await slot.player.dispose();
|
await slot.player.dispose();
|
||||||
_live.remove(slot);
|
_live.remove(slot);
|
||||||
|
if (kDebugMode) {
|
||||||
|
debugPrint(
|
||||||
|
'[player-slot] slot_released role=$role owner=${slot.owner.name} '
|
||||||
|
'selectOwner=$owner drama=${slot.dramaId ?? '-'} '
|
||||||
|
'episodeIndex=${slot.index} '
|
||||||
|
'controller=${_playerIdentity(slot.player)} '
|
||||||
|
'slotPlaying=${slotPlaying ? 1 : 0} '
|
||||||
|
'isPlaying=${value?.isPlaying == true ? 1 : 0} '
|
||||||
|
'position=${value?.position.inMilliseconds ?? '-'} '
|
||||||
|
'currentIndex=$_currentIndex generation=$_generation '
|
||||||
|
'trigger=$trigger',
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,7 +288,7 @@ class PlaybackSession extends ChangeNotifier {
|
|||||||
_PlayerSlot? target;
|
_PlayerSlot? target;
|
||||||
await _enqueue(() async {
|
await _enqueue(() async {
|
||||||
if (_closed || request != _generation) return;
|
if (_closed || request != _generation) return;
|
||||||
await _retire(_handoff);
|
await _retire(_handoff, trigger: 'select-handoff');
|
||||||
final old = _active;
|
final old = _active;
|
||||||
if (old != null &&
|
if (old != null &&
|
||||||
old.index == index &&
|
old.index == index &&
|
||||||
@@ -339,7 +359,9 @@ class PlaybackSession extends ChangeNotifier {
|
|||||||
await _enqueue(() => _start(slot, request));
|
await _enqueue(() => _start(slot, request));
|
||||||
await Future<void>.delayed(_qualityHandoffHold);
|
await Future<void>.delayed(_qualityHandoffHold);
|
||||||
if (previous != null && _handoff == previous && _active == slot) {
|
if (previous != null && _handoff == previous && _active == slot) {
|
||||||
await _enqueue(() => _retire(previous));
|
await _enqueue(
|
||||||
|
() => _retire(previous, trigger: 'quality-handoff-complete'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (!_closed && request == _generation) _warmNeighbors(this);
|
if (!_closed && request == _generation) _warmNeighbors(this);
|
||||||
return _active == slot && slot.failure == null;
|
return _active == slot && slot.failure == null;
|
||||||
@@ -351,8 +373,8 @@ class PlaybackSession extends ChangeNotifier {
|
|||||||
if (_closed || request != _generation || _active == null) return;
|
if (_closed || request != _generation || _active == null) return;
|
||||||
final warm = _next;
|
final warm = _next;
|
||||||
_next = null;
|
_next = null;
|
||||||
await _retire(_handoff);
|
await _retire(_handoff, trigger: 'replacement-handoff');
|
||||||
await _retire(warm);
|
await _retire(warm, trigger: 'replacement-next');
|
||||||
if (_closed || request != _generation || _active == null) return;
|
if (_closed || request != _generation || _active == null) return;
|
||||||
try {
|
try {
|
||||||
created = _create(index);
|
created = _create(index);
|
||||||
@@ -379,7 +401,9 @@ class PlaybackSession extends ChangeNotifier {
|
|||||||
|
|
||||||
Future<void> _dropReplacement(_PlayerSlot slot) {
|
Future<void> _dropReplacement(_PlayerSlot slot) {
|
||||||
return _enqueue(() async {
|
return _enqueue(() async {
|
||||||
if (_live.contains(slot)) await _retire(slot);
|
if (_live.contains(slot)) {
|
||||||
|
await _retire(slot, trigger: 'replacement-drop');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -474,7 +498,7 @@ class PlaybackSession extends ChangeNotifier {
|
|||||||
_loading = false;
|
_loading = false;
|
||||||
_notify();
|
_notify();
|
||||||
for (final slot in _live.toList()) {
|
for (final slot in _live.toList()) {
|
||||||
await _retire(slot);
|
await _retire(slot, trigger: 'session-release-all');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -483,6 +507,14 @@ class PlaybackSession extends ChangeNotifier {
|
|||||||
if (!_closed) notifyListeners();
|
if (!_closed) notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _slotRole(_PlayerSlot slot, {String? roleHint}) {
|
||||||
|
if (identical(slot, _active)) return 'current';
|
||||||
|
if (identical(slot, _previous)) return 'previous';
|
||||||
|
if (identical(slot, _next)) return 'next';
|
||||||
|
if (identical(slot, _handoff)) return 'handoff';
|
||||||
|
return roleHint ?? 'unassigned';
|
||||||
|
}
|
||||||
|
|
||||||
void _logOwnerRejection(String phase, _PlayerSlot slot) {
|
void _logOwnerRejection(String phase, _PlayerSlot slot) {
|
||||||
if (!kDebugMode) return;
|
if (!kDebugMode) return;
|
||||||
debugPrint(
|
debugPrint(
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:video_player/video_player.dart';
|
import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
import '../../config/app_config.dart';
|
import '../../config/app_config.dart';
|
||||||
@@ -69,13 +68,6 @@ class VideoPlayerPort implements PlayerPort {
|
|||||||
Future<void> dispose() async {
|
Future<void> dispose() async {
|
||||||
if (_disposed) return;
|
if (_disposed) return;
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
if (kDebugMode) {
|
|
||||||
debugPrint(
|
|
||||||
'[player-controller] phase=dispose '
|
|
||||||
'controller=${identityHashCode(controller)} '
|
|
||||||
'source=${controller.dataSource}\n${StackTrace.current}',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
await _initialization;
|
await _initialization;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
// Author: fruit.com.cn
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
@@ -91,6 +93,7 @@ class PlayerPage extends StatefulWidget {
|
|||||||
this.downloads,
|
this.downloads,
|
||||||
this.handoff,
|
this.handoff,
|
||||||
this.revealAfterFirstFrame = false,
|
this.revealAfterFirstFrame = false,
|
||||||
|
this.initialFrameBytes,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Drama drama;
|
final Drama drama;
|
||||||
@@ -119,6 +122,7 @@ class PlayerPage extends StatefulWidget {
|
|||||||
final DownloadController? downloads;
|
final DownloadController? downloads;
|
||||||
final PlayerPageHandoff? handoff;
|
final PlayerPageHandoff? handoff;
|
||||||
final bool revealAfterFirstFrame;
|
final bool revealAfterFirstFrame;
|
||||||
|
final Uint8List? initialFrameBytes;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PlayerPage> createState() => _PlayerPageState();
|
State<PlayerPage> createState() => _PlayerPageState();
|
||||||
@@ -142,6 +146,7 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
bool _swipePrewarmCaptured = false;
|
bool _swipePrewarmCaptured = false;
|
||||||
bool _presentationActive = false;
|
bool _presentationActive = false;
|
||||||
late bool _firstFrameReady;
|
late bool _firstFrameReady;
|
||||||
|
final GlobalKey _renderBoundaryKey = GlobalKey();
|
||||||
bool _leavingHandoff = false;
|
bool _leavingHandoff = false;
|
||||||
bool _preparingFullPlayer = false;
|
bool _preparingFullPlayer = false;
|
||||||
final Set<String> _precachedPosterUrls = {};
|
final Set<String> _precachedPosterUrls = {};
|
||||||
@@ -599,10 +604,13 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
return Stack(
|
return Stack(
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
children: [
|
children: [
|
||||||
PlayerRenderStack(
|
RepaintBoundary(
|
||||||
pageController: _pageController,
|
key: _renderBoundaryKey,
|
||||||
slots: _renderSlots(indices),
|
child: PlayerRenderStack(
|
||||||
onCurrentVideoVisible: _handleCurrentVideoVisible,
|
pageController: _pageController,
|
||||||
|
slots: _renderSlots(indices),
|
||||||
|
onCurrentVideoVisible: _handleCurrentVideoVisible,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
pageLayer,
|
pageLayer,
|
||||||
],
|
],
|
||||||
@@ -677,7 +685,25 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
logic.blockedEpisodeId != null ||
|
logic.blockedEpisodeId != null ||
|
||||||
logic.session.error != null;
|
logic.session.error != null;
|
||||||
final presentedPlayer = widget.revealAfterFirstFrame
|
final presentedPlayer = widget.revealAfterFirstFrame
|
||||||
? Opacity(opacity: revealContent ? 1 : 0, child: player)
|
? Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
Positioned.fill(
|
||||||
|
child: revealContent
|
||||||
|
? const SizedBox.expand()
|
||||||
|
: widget.initialFrameBytes == null
|
||||||
|
? _buildInitialFrameFallback()
|
||||||
|
: Image.memory(
|
||||||
|
widget.initialFrameBytes!,
|
||||||
|
key: const Key('player-entry-frame'),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
gaplessPlayback: true,
|
||||||
|
errorBuilder: (_, _, _) => _buildInitialFrameFallback(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Opacity(opacity: revealContent ? 1 : 0, child: player),
|
||||||
|
],
|
||||||
|
)
|
||||||
: player;
|
: player;
|
||||||
if (widget.handoff == null) return presentedPlayer;
|
if (widget.handoff == null) return presentedPlayer;
|
||||||
return PopScope<void>(
|
return PopScope<void>(
|
||||||
@@ -700,6 +726,24 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildInitialFrameFallback() {
|
||||||
|
final index = widget.initialIndex;
|
||||||
|
if (index < 0 || index >= widget.drama.episodes.length) {
|
||||||
|
return const ColoredBox(
|
||||||
|
key: Key('player-entry-black'),
|
||||||
|
color: AppColors.mediaBlack,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final episode = widget.drama.episodes[index];
|
||||||
|
return playerOpeningFrame(
|
||||||
|
episode.thumbnailsUrl,
|
||||||
|
posterUrl: episode.posterUrl,
|
||||||
|
spriteSource: _spriteAxisService,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
) ??
|
||||||
|
const ColoredBox(color: AppColors.mediaBlack);
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
@@ -1195,6 +1239,8 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
seriesEpisodes.insert(0, sourceEpisode);
|
seriesEpisodes.insert(0, sourceEpisode);
|
||||||
episodeIndex = 0;
|
episodeIndex = 0;
|
||||||
}
|
}
|
||||||
|
final initialFrameBytes = await _captureCurrentFrame();
|
||||||
|
if (!mounted || !context.mounted) return;
|
||||||
final sourceUserWantsPlay = logic.session.userWantsPlay;
|
final sourceUserWantsPlay = logic.session.userWantsPlay;
|
||||||
try {
|
try {
|
||||||
await logic.pauseForFullPlayer();
|
await logic.pauseForFullPlayer();
|
||||||
@@ -1207,17 +1253,20 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
setState(() => _presentationActive = true);
|
setState(() => _presentationActive = true);
|
||||||
final route = Navigator.of(context).push<void>(
|
final route = Navigator.of(context).push<void>(
|
||||||
PageRouteBuilder<void>(
|
PageRouteBuilder<void>(
|
||||||
opaque: false,
|
opaque: true,
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
transitionDuration: Duration.zero,
|
transitionDuration: const Duration(milliseconds: 180),
|
||||||
reverseTransitionDuration: Duration.zero,
|
reverseTransitionDuration: const Duration(milliseconds: 180),
|
||||||
|
transitionsBuilder: (_, animation, _, child) =>
|
||||||
|
FadeTransition(opacity: animation, child: child),
|
||||||
pageBuilder: (_, _, _) => PlayerPage(
|
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,
|
revealAfterFirstFrame: initialFrameBytes != null,
|
||||||
|
initialFrameBytes: initialFrameBytes,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -1230,6 +1279,37 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
_logFullPlayerRoute('restored');
|
_logFullPlayerRoute('restored');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Uint8List?> _captureCurrentFrame() async {
|
||||||
|
final boundary = _renderBoundaryKey.currentContext?.findRenderObject();
|
||||||
|
if (boundary is! RenderRepaintBoundary) return null;
|
||||||
|
final startedAt = DateTime.now();
|
||||||
|
try {
|
||||||
|
final pixelRatio = MediaQuery.devicePixelRatioOf(
|
||||||
|
context,
|
||||||
|
).clamp(1.0, 2.0).toDouble();
|
||||||
|
final image = await boundary.toImage(pixelRatio: pixelRatio);
|
||||||
|
final data = await image.toByteData(format: ui.ImageByteFormat.png);
|
||||||
|
image.dispose();
|
||||||
|
Uint8List? bytes;
|
||||||
|
if (data != null) {
|
||||||
|
bytes = data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
|
||||||
|
}
|
||||||
|
if (kDebugMode) {
|
||||||
|
debugPrint(
|
||||||
|
'[player-entry] phase=frame-captured '
|
||||||
|
'bytes=${bytes?.length ?? 0} '
|
||||||
|
'ms=${DateTime.now().difference(startedAt).inMilliseconds}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
} catch (error) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
debugPrint('[player-entry] phase=frame-capture-failed error=$error');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _leaveHandoff(BuildContext context) async {
|
Future<void> _leaveHandoff(BuildContext context) async {
|
||||||
if (_leavingHandoff) return;
|
if (_leavingHandoff) return;
|
||||||
setState(() => _leavingHandoff = true);
|
setState(() => _leavingHandoff = true);
|
||||||
|
|||||||
@@ -129,11 +129,15 @@ void main() {
|
|||||||
expect(sourceLiveBefore, greaterThan(1));
|
expect(sourceLiveBefore, greaterThan(1));
|
||||||
|
|
||||||
await tester.tap(find.byKey(const Key('home-feed-full-series')));
|
await tester.tap(find.byKey(const Key('home-feed-full-series')));
|
||||||
|
await _waitForFullPlayerEntry(tester);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
final fullPlayer = _fullPlayerFinder();
|
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 fullPlayerWidget = tester.widget<PlayerPage>(fullPlayer);
|
||||||
|
expect(fullPlayerWidget.initialFrameBytes, isNotNull);
|
||||||
|
expect(fullPlayerWidget.revealAfterFirstFrame, isTrue);
|
||||||
final fullPlayerPort = fullLogic.session.active! as VideoPlayerPort;
|
final fullPlayerPort = fullLogic.session.active! as VideoPlayerPort;
|
||||||
expect(fullPlayerPort.controller, isNot(same(feedPlayer.controller)));
|
expect(fullPlayerPort.controller, isNot(same(feedPlayer.controller)));
|
||||||
expect(feedPlayer.disposed, isFalse);
|
expect(feedPlayer.disposed, isFalse);
|
||||||
@@ -221,6 +225,7 @@ void main() {
|
|||||||
expect(find.byKey(const Key('home-feed-center-play')), findsOneWidget);
|
expect(find.byKey(const Key('home-feed-center-play')), findsOneWidget);
|
||||||
|
|
||||||
await tester.tap(find.byKey(const Key('home-feed-full-series')));
|
await tester.tap(find.byKey(const Key('home-feed-full-series')));
|
||||||
|
await _waitForFullPlayerEntry(tester);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(_fullPlayerFinder(), findsOneWidget);
|
expect(_fullPlayerFinder(), findsOneWidget);
|
||||||
@@ -263,6 +268,7 @@ void main() {
|
|||||||
expect(feedIndex, 1);
|
expect(feedIndex, 1);
|
||||||
|
|
||||||
await tester.tap(find.byKey(const Key('home-feed-full-series')));
|
await tester.tap(find.byKey(const Key('home-feed-full-series')));
|
||||||
|
await _waitForFullPlayerEntry(tester);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
final fullLogic = _pageLogic(tester, _fullPlayerFinder());
|
final fullLogic = _pageLogic(tester, _fullPlayerFinder());
|
||||||
@@ -1658,6 +1664,17 @@ Finder _fullPlayerFinder() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _waitForFullPlayerEntry(WidgetTester tester) async {
|
||||||
|
for (var attempt = 0; attempt < 120; attempt++) {
|
||||||
|
await tester.runAsync(
|
||||||
|
() => Future<void>.delayed(const Duration(milliseconds: 10)),
|
||||||
|
);
|
||||||
|
await tester.pump(const Duration(milliseconds: 16));
|
||||||
|
if (_fullPlayerFinder().evaluate().isNotEmpty) return;
|
||||||
|
}
|
||||||
|
fail('full player route was not pushed after frame capture');
|
||||||
|
}
|
||||||
|
|
||||||
class _RouteVideoFactory {
|
class _RouteVideoFactory {
|
||||||
final List<_RouteVideoPlayer> created = [];
|
final List<_RouteVideoPlayer> created = [];
|
||||||
final List<Episode> episodes = [];
|
final List<Episode> episodes = [];
|
||||||
|
|||||||
@@ -470,6 +470,7 @@ void main() {
|
|||||||
await _pumpRecommend(tester);
|
await _pumpRecommend(tester);
|
||||||
|
|
||||||
await tester.tap(find.byKey(const Key('home-feed-series-name')));
|
await tester.tap(find.byKey(const Key('home-feed-series-name')));
|
||||||
|
await _waitForFullPlayer(tester);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.byType(PlayerPage).last, findsOneWidget);
|
expect(find.byType(PlayerPage).last, findsOneWidget);
|
||||||
expect(
|
expect(
|
||||||
@@ -484,6 +485,7 @@ void main() {
|
|||||||
expect(find.byKey(const Key('recommend-tab-recommend')), findsOneWidget);
|
expect(find.byKey(const Key('recommend-tab-recommend')), findsOneWidget);
|
||||||
|
|
||||||
await tester.tap(find.byKey(const Key('home-feed-full-series')));
|
await tester.tap(find.byKey(const Key('home-feed-full-series')));
|
||||||
|
await _waitForFullPlayer(tester);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.byType(PlayerPage).last, findsOneWidget);
|
expect(find.byType(PlayerPage).last, findsOneWidget);
|
||||||
expect(
|
expect(
|
||||||
@@ -550,6 +552,17 @@ void main() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _waitForFullPlayer(WidgetTester tester) async {
|
||||||
|
for (var attempt = 0; attempt < 120; attempt++) {
|
||||||
|
await tester.runAsync(
|
||||||
|
() => Future<void>.delayed(const Duration(milliseconds: 10)),
|
||||||
|
);
|
||||||
|
await tester.pump(const Duration(milliseconds: 16));
|
||||||
|
if (find.byType(PlayerPage).evaluate().length > 1) return;
|
||||||
|
}
|
||||||
|
fail('full player route was not pushed after frame capture');
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _pumpRecommend(WidgetTester tester) async {
|
Future<void> _pumpRecommend(WidgetTester tester) async {
|
||||||
tester.view.physicalSize = const Size(390, 844);
|
tester.view.physicalSize = const Size(390, 844);
|
||||||
tester.view.devicePixelRatio = 1;
|
tester.view.devicePixelRatio = 1;
|
||||||
|
|||||||
Reference in new issue
Block a user