fix(player): 预热直出并与视频统一封面布局
This commit is contained in:
1 parent
a3a79f7068
commit
48b4e67d34
9 files changed
+119
-104
No files matched your search
@@ -260,6 +260,7 @@ class PlaybackSession extends ChangeNotifier {
|
|||||||
_usableWarm(this, warmNext, index) ??
|
_usableWarm(this, warmNext, index) ??
|
||||||
_usableWarm(this, warmPrevious, index);
|
_usableWarm(this, warmPrevious, index);
|
||||||
if (warm != null) {
|
if (warm != null) {
|
||||||
|
warm.reusedWarm = true;
|
||||||
target = _active = warm;
|
target = _active = warm;
|
||||||
_loading = !warm.prepared;
|
_loading = !warm.prepared;
|
||||||
_notify();
|
_notify();
|
||||||
@@ -470,5 +471,6 @@ class _PlayerSlot {
|
|||||||
bool activated = false;
|
bool activated = false;
|
||||||
bool playing = false;
|
bool playing = false;
|
||||||
bool retiring = false;
|
bool retiring = false;
|
||||||
|
bool reusedWarm = false;
|
||||||
Object? failure;
|
Object? failure;
|
||||||
}
|
}
|
||||||
@@ -19,6 +19,16 @@ extension PlaybackSessionControls on PlaybackSession {
|
|||||||
return slot.player;
|
return slot.player;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 当前索引是否复用了已预热的播放器槽。
|
||||||
|
bool reusedWarmAt(int index) {
|
||||||
|
for (final slot in [_active, _next, _previous]) {
|
||||||
|
if (slot != null && !slot.retiring && slot.index == index) {
|
||||||
|
return slot.reusedWarm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/// 同步应用前台状态并恢复或暂停当前集。
|
/// 同步应用前台状态并恢复或暂停当前集。
|
||||||
void setAppResumed(bool value) {
|
void setAppResumed(bool value) {
|
||||||
_appResumed = value;
|
_appResumed = value;
|
||||||
|
|||||||
@@ -587,6 +587,9 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
if (selected && videoReady) _logSwipeReveal();
|
if (selected && videoReady) _logSwipeReveal();
|
||||||
final ready = videoReady || port != null;
|
final ready = videoReady || port != null;
|
||||||
final theater = logic.pip || logic.pipPreparing;
|
final theater = logic.pip || logic.pipPreparing;
|
||||||
|
final videoFit = theater ? BoxFit.contain : BoxFit.cover;
|
||||||
|
final preparedFrameAvailable =
|
||||||
|
selected && logic.session.reusedWarmAt(index);
|
||||||
final locked = logic.session.isLocked || episode.locked;
|
final locked = logic.session.isLocked || episode.locked;
|
||||||
if (selected &&
|
if (selected &&
|
||||||
videoPort != null &&
|
videoPort != null &&
|
||||||
@@ -613,9 +616,11 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
videoUrl: videoPort?.controller.dataSource ?? episode.videoUrl,
|
videoUrl: videoPort?.controller.dataSource ?? episode.videoUrl,
|
||||||
theater: theater,
|
theater: theater,
|
||||||
holdOpeningUntilFirstFrame: logic.session.userWantsPlay,
|
holdOpeningUntilFirstFrame: logic.session.userWantsPlay,
|
||||||
|
preparedFrameAvailable: preparedFrameAvailable,
|
||||||
openingFrame: playerOpeningFrame(
|
openingFrame: playerOpeningFrame(
|
||||||
episode.thumbnailsUrl,
|
episode.thumbnailsUrl,
|
||||||
posterUrl: drama.posterUrl,
|
posterUrl: drama.posterUrl,
|
||||||
|
fit: videoFit,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (selected &&
|
if (selected &&
|
||||||
|
|||||||
@@ -12,17 +12,19 @@ import '../../../core/theme/app_colors.dart';
|
|||||||
import '../../../core/widgets/remote_image.dart';
|
import '../../../core/widgets/remote_image.dart';
|
||||||
import 'scrub_preview_card.dart';
|
import 'scrub_preview_card.dart';
|
||||||
|
|
||||||
const double _posterAspectRatio = 3 / 4;
|
|
||||||
const double _posterBlurSigma = 18;
|
|
||||||
|
|
||||||
/// 封面立即可见,雪碧图首帧解码完成后再覆盖封面。
|
/// 封面立即可见,雪碧图首帧解码完成后再覆盖封面。
|
||||||
Widget? playerOpeningFrame(String? thumbnailsUrl, {String? posterUrl}) {
|
Widget? playerOpeningFrame(
|
||||||
|
String? thumbnailsUrl, {
|
||||||
|
String? posterUrl,
|
||||||
|
BoxFit fit = BoxFit.cover,
|
||||||
|
}) {
|
||||||
final url = thumbnailsUrl?.trim() ?? '';
|
final url = thumbnailsUrl?.trim() ?? '';
|
||||||
final poster = posterUrl?.trim() ?? '';
|
final poster = posterUrl?.trim() ?? '';
|
||||||
if (url.isEmpty && poster.isEmpty) return null;
|
if (url.isEmpty && poster.isEmpty) return null;
|
||||||
return PlayerOpeningFrame(
|
return PlayerOpeningFrame(
|
||||||
thumbnailsUrl: url.isEmpty ? null : url,
|
thumbnailsUrl: url.isEmpty ? null : url,
|
||||||
posterUrl: poster.isEmpty ? null : poster,
|
posterUrl: poster.isEmpty ? null : poster,
|
||||||
|
fit: fit,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,12 +34,14 @@ class PlayerOpeningFrame extends StatefulWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
this.thumbnailsUrl,
|
this.thumbnailsUrl,
|
||||||
this.posterUrl,
|
this.posterUrl,
|
||||||
|
this.fit = BoxFit.cover,
|
||||||
this.loader,
|
this.loader,
|
||||||
this.cache,
|
this.cache,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String? thumbnailsUrl;
|
final String? thumbnailsUrl;
|
||||||
final String? posterUrl;
|
final String? posterUrl;
|
||||||
|
final BoxFit fit;
|
||||||
final SpriteThumbnailAssetLoader? loader;
|
final SpriteThumbnailAssetLoader? loader;
|
||||||
final OpeningFrameCache? cache;
|
final OpeningFrameCache? cache;
|
||||||
|
|
||||||
@@ -85,39 +89,18 @@ class _PlayerOpeningFrameState extends State<PlayerOpeningFrame> {
|
|||||||
children: [
|
children: [
|
||||||
const ColoredBox(color: AppColors.mediaBlack),
|
const ColoredBox(color: AppColors.mediaBlack),
|
||||||
if (posterUrl != null)
|
if (posterUrl != null)
|
||||||
Positioned.fill(
|
RemoteImage(
|
||||||
child: ImageFiltered(
|
posterUrl,
|
||||||
key: const Key('player-opening-poster-blur'),
|
key: const Key('player-opening-poster'),
|
||||||
imageFilter: ui.ImageFilter.blur(
|
fit: widget.fit,
|
||||||
sigmaX: _posterBlurSigma,
|
fallbackColor: AppColors.mediaBlack,
|
||||||
sigmaY: _posterBlurSigma,
|
|
||||||
),
|
|
||||||
child: RemoteImage(
|
|
||||||
posterUrl,
|
|
||||||
key: const Key('player-opening-poster-backdrop'),
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
fallbackColor: AppColors.mediaBlack,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (posterUrl != null)
|
|
||||||
Center(
|
|
||||||
child: AspectRatio(
|
|
||||||
key: const Key('player-opening-poster-stage'),
|
|
||||||
aspectRatio: _posterAspectRatio,
|
|
||||||
child: RemoteImage(
|
|
||||||
posterUrl,
|
|
||||||
key: const Key('player-opening-poster'),
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
fallbackColor: AppColors.mediaBlack,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
if (frame != null)
|
if (frame != null)
|
||||||
SpriteCropPreview(
|
SpriteCropPreview(
|
||||||
key: const Key('player-opening-sprite'),
|
key: const Key('player-opening-sprite'),
|
||||||
image: frame.image,
|
image: frame.image,
|
||||||
sourceRect: frame.sourceRect,
|
sourceRect: frame.sourceRect,
|
||||||
|
fit: widget.fit,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import 'package:video_player/video_player.dart';
|
|||||||
|
|
||||||
import '../../../core/theme/app_colors.dart';
|
import '../../../core/theme/app_colors.dart';
|
||||||
|
|
||||||
const Duration _videoFadeDuration = Duration(milliseconds: 200);
|
|
||||||
const double _errorHorizontal = 32;
|
const double _errorHorizontal = 32;
|
||||||
const int _errorMaxLines = 3;
|
const int _errorMaxLines = 3;
|
||||||
|
|
||||||
@@ -20,6 +19,7 @@ class PlayerVideoSurface extends StatefulWidget {
|
|||||||
this.handoff,
|
this.handoff,
|
||||||
this.openingFrame,
|
this.openingFrame,
|
||||||
this.holdOpeningUntilFirstFrame = false,
|
this.holdOpeningUntilFirstFrame = false,
|
||||||
|
this.preparedFrameAvailable = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
final VideoPlayerController? controller;
|
final VideoPlayerController? controller;
|
||||||
@@ -30,6 +30,9 @@ class PlayerVideoSurface extends StatefulWidget {
|
|||||||
|
|
||||||
/// 播放中的新片源在位置首次前进前继续显示封面,避免混入未出帧纹理。
|
/// 播放中的新片源在位置首次前进前继续显示封面,避免混入未出帧纹理。
|
||||||
final bool holdOpeningUntilFirstFrame;
|
final bool holdOpeningUntilFirstFrame;
|
||||||
|
|
||||||
|
/// 相邻集预热槽已持有可用画面,进入页面时不需要封面兜底。
|
||||||
|
final bool preparedFrameAvailable;
|
||||||
final String videoUrl;
|
final String videoUrl;
|
||||||
final bool theater;
|
final bool theater;
|
||||||
|
|
||||||
@@ -69,15 +72,27 @@ class _PlayerVideoSurfaceState extends State<PlayerVideoSurface> {
|
|||||||
Widget _frame(VideoPlayerValue? value) {
|
Widget _frame(VideoPlayerValue? value) {
|
||||||
final controller = widget.controller;
|
final controller = widget.controller;
|
||||||
final handoff = _liveHandoff(controller);
|
final handoff = _liveHandoff(controller);
|
||||||
final firstFrameReady = _trackFrameState(controller, value);
|
final firstFrameSeen = _trackFrameState(controller, value);
|
||||||
final reveal = _shouldReveal(value, handoff != null) &&
|
final firstFrameReady =
|
||||||
|
widget.preparedFrameAvailable || firstFrameSeen;
|
||||||
|
final textureReady = _shouldReveal(value, handoff != null);
|
||||||
|
final reveal = textureReady &&
|
||||||
(!widget.holdOpeningUntilFirstFrame ||
|
(!widget.holdOpeningUntilFirstFrame ||
|
||||||
|
widget.preparedFrameAvailable ||
|
||||||
firstFrameReady ||
|
firstFrameReady ||
|
||||||
_playbackStarted &&
|
_playbackStarted &&
|
||||||
value?.isPlaying == false &&
|
value?.isPlaying == false &&
|
||||||
value?.isBuffering != true);
|
value?.isBuffering != true);
|
||||||
|
final showOpening =
|
||||||
|
widget.openingFrame != null && handoff == null && !reveal;
|
||||||
_logStateChange(value);
|
_logStateChange(value);
|
||||||
_logLayerState(value, handoff, reveal, firstFrameReady);
|
_logLayerState(
|
||||||
|
value,
|
||||||
|
handoff,
|
||||||
|
reveal,
|
||||||
|
firstFrameReady,
|
||||||
|
showOpening,
|
||||||
|
);
|
||||||
return Stack(
|
return Stack(
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
children: [
|
children: [
|
||||||
@@ -85,19 +100,15 @@ class _PlayerVideoSurfaceState extends State<PlayerVideoSurface> {
|
|||||||
key: Key('player-fallback-background'),
|
key: Key('player-fallback-background'),
|
||||||
color: AppColors.mediaBlack,
|
color: AppColors.mediaBlack,
|
||||||
),
|
),
|
||||||
if (widget.openingFrame != null)
|
if (showOpening)
|
||||||
AnimatedOpacity(
|
KeyedSubtree(
|
||||||
key: const Key('player-opening-frame'),
|
key: const Key('player-opening-frame'),
|
||||||
opacity: reveal ? 0 : 1,
|
|
||||||
duration: handoff != null ? Duration.zero : _videoFadeDuration,
|
|
||||||
curve: Curves.easeOut,
|
|
||||||
child: widget.openingFrame!,
|
child: widget.openingFrame!,
|
||||||
),
|
),
|
||||||
AnimatedOpacity(
|
AnimatedOpacity(
|
||||||
key: const Key('player-video-opacity'),
|
key: const Key('player-video-opacity'),
|
||||||
opacity: reveal ? 1 : 0,
|
opacity: reveal ? 1 : 0,
|
||||||
duration: handoff != null ? Duration.zero : _videoFadeDuration,
|
duration: Duration.zero,
|
||||||
curve: Curves.easeOut,
|
|
||||||
child: reveal && controller != null
|
child: reveal && controller != null
|
||||||
? _VideoLayer(
|
? _VideoLayer(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
@@ -180,20 +191,24 @@ class _PlayerVideoSurfaceState extends State<PlayerVideoSurface> {
|
|||||||
VideoPlayerController? handoff,
|
VideoPlayerController? handoff,
|
||||||
bool reveal,
|
bool reveal,
|
||||||
bool firstFrameReady,
|
bool firstFrameReady,
|
||||||
|
bool showOpening,
|
||||||
) {
|
) {
|
||||||
if (!kDebugMode) return;
|
if (!kDebugMode) return;
|
||||||
final reason = handoff != null
|
final reason = handoff != null
|
||||||
? 'handoff'
|
? 'handoff'
|
||||||
: !_shouldReveal(value, handoff != null)
|
: !_shouldReveal(value, handoff != null)
|
||||||
? 'uninitialized'
|
? 'uninitialized'
|
||||||
|
: widget.preparedFrameAvailable
|
||||||
|
? 'warm-frame'
|
||||||
: !firstFrameReady && widget.holdOpeningUntilFirstFrame
|
: !firstFrameReady && widget.holdOpeningUntilFirstFrame
|
||||||
? 'awaiting-first-frame'
|
? 'awaiting-first-frame'
|
||||||
: 'reveal';
|
: 'reveal';
|
||||||
final signature =
|
final signature =
|
||||||
'opening=${widget.openingFrame == null ? 0 : reveal ? 0 : 1} '
|
'warm=${widget.preparedFrameAvailable ? 1 : 0} '
|
||||||
|
'frame=${firstFrameReady ? 1 : 0} '
|
||||||
|
'cover=${showOpening ? 1 : 0} '
|
||||||
'video=${reveal ? 1 : 0} handoff=${handoff == null ? 0 : 1} '
|
'video=${reveal ? 1 : 0} handoff=${handoff == null ? 0 : 1} '
|
||||||
'init=${value?.isInitialized == true ? 1 : 0} '
|
'init=${value?.isInitialized == true ? 1 : 0} '
|
||||||
'frame=${firstFrameReady ? 1 : 0} '
|
|
||||||
'playing=${value?.isPlaying == true ? 1 : 0} '
|
'playing=${value?.isPlaying == true ? 1 : 0} '
|
||||||
'buffering=${value?.isBuffering == true ? 1 : 0} '
|
'buffering=${value?.isBuffering == true ? 1 : 0} '
|
||||||
'reason=$reason';
|
'reason=$reason';
|
||||||
|
|||||||
@@ -256,26 +256,37 @@ class SpriteCropPreview extends StatelessWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.image,
|
required this.image,
|
||||||
required this.sourceRect,
|
required this.sourceRect,
|
||||||
|
this.fit = BoxFit.cover,
|
||||||
});
|
});
|
||||||
|
|
||||||
final ui.Image image;
|
final ui.Image image;
|
||||||
final ui.Rect sourceRect;
|
final ui.Rect sourceRect;
|
||||||
|
final BoxFit fit;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => CustomPaint(
|
Widget build(BuildContext context) => CustomPaint(
|
||||||
painter: _SpriteCropPainter(image: image, sourceRect: sourceRect),
|
painter: _SpriteCropPainter(
|
||||||
|
image: image,
|
||||||
|
sourceRect: sourceRect,
|
||||||
|
fit: fit,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SpriteCropPainter extends CustomPainter {
|
class _SpriteCropPainter extends CustomPainter {
|
||||||
const _SpriteCropPainter({required this.image, required this.sourceRect});
|
const _SpriteCropPainter({
|
||||||
|
required this.image,
|
||||||
|
required this.sourceRect,
|
||||||
|
required this.fit,
|
||||||
|
});
|
||||||
|
|
||||||
final ui.Image image;
|
final ui.Image image;
|
||||||
final ui.Rect sourceRect;
|
final ui.Rect sourceRect;
|
||||||
|
final BoxFit fit;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void paint(Canvas canvas, Size size) {
|
void paint(Canvas canvas, Size size) {
|
||||||
final fitted = applyBoxFit(BoxFit.cover, sourceRect.size, size);
|
final fitted = applyBoxFit(fit, sourceRect.size, size);
|
||||||
final sourceOffset = Alignment.center.inscribe(
|
final sourceOffset = Alignment.center.inscribe(
|
||||||
fitted.source,
|
fitted.source,
|
||||||
Offset.zero & sourceRect.size,
|
Offset.zero & sourceRect.size,
|
||||||
@@ -302,7 +313,9 @@ class _SpriteCropPainter extends CustomPainter {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool shouldRepaint(covariant _SpriteCropPainter oldDelegate) =>
|
bool shouldRepaint(covariant _SpriteCropPainter oldDelegate) =>
|
||||||
oldDelegate.image != image || oldDelegate.sourceRect != sourceRect;
|
oldDelegate.image != image ||
|
||||||
|
oldDelegate.sourceRect != sourceRect ||
|
||||||
|
oldDelegate.fit != fit;
|
||||||
}
|
}
|
||||||
|
|
||||||
String _format(Duration value) {
|
String _format(Duration value) {
|
||||||
|
|||||||
@@ -161,11 +161,13 @@ void main() {
|
|||||||
await session.select(2, preloadIndex: 3, previousIndex: 1);
|
await session.select(2, preloadIndex: 3, previousIndex: 1);
|
||||||
await session.preloadSettled;
|
await session.preloadSettled;
|
||||||
expect(session.active, isA<_Player>().having((p) => p.id, 'id', 'ep-2'));
|
expect(session.active, isA<_Player>().having((p) => p.id, 'id', 'ep-2'));
|
||||||
|
expect(session.reusedWarmAt(2), isTrue);
|
||||||
|
|
||||||
await session.select(1, preloadIndex: 2, previousIndex: 0);
|
await session.select(1, preloadIndex: 2, previousIndex: 0);
|
||||||
await session.preloadSettled;
|
await session.preloadSettled;
|
||||||
|
|
||||||
expect(session.active, isA<_Player>().having((p) => p.id, 'id', 'ep-1'));
|
expect(session.active, isA<_Player>().having((p) => p.id, 'id', 'ep-1'));
|
||||||
|
expect(session.reusedWarmAt(1), isTrue);
|
||||||
expect(
|
expect(
|
||||||
factory.players.firstWhere((player) => player.id == 'ep-1').playing,
|
factory.players.firstWhere((player) => player.id == 'ep-1').playing,
|
||||||
isTrue,
|
isTrue,
|
||||||
|
|||||||
@@ -83,13 +83,9 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(find.byKey(const Key('player-opening-poster')), findsOneWidget);
|
expect(find.byKey(const Key('player-opening-poster')), findsOneWidget);
|
||||||
expect(
|
|
||||||
find.byKey(const Key('player-opening-poster-backdrop')),
|
|
||||||
findsOneWidget,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('should_constrain_poster_inside_native_vertical_frame', (
|
testWidgets('should_use_surface_fit_without_extra_frame_constraint', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
@@ -100,23 +96,23 @@ void main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final stage = tester.widget<AspectRatio>(
|
|
||||||
find.byKey(const Key('player-opening-poster-stage')),
|
|
||||||
);
|
|
||||||
final poster = tester.widget<RemoteImage>(
|
final poster = tester.widget<RemoteImage>(
|
||||||
find.byKey(const Key('player-opening-poster')),
|
find.byKey(const Key('player-opening-poster')),
|
||||||
);
|
);
|
||||||
final backdrop = tester.widget<RemoteImage>(
|
|
||||||
find.byKey(const Key('player-opening-poster-backdrop')),
|
expect(poster.fit, BoxFit.cover);
|
||||||
|
expect(find.byType(AspectRatio), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should_forward_video_fit_to_opening_frame', () {
|
||||||
|
final frame = playerOpeningFrame(
|
||||||
|
null,
|
||||||
|
posterUrl: 'https://cdn.test/poster.jpg',
|
||||||
|
fit: BoxFit.contain,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(stage.aspectRatio, closeTo(3 / 4, 0.001));
|
expect(frame, isA<PlayerOpeningFrame>());
|
||||||
expect(poster.fit, BoxFit.cover);
|
expect((frame! as PlayerOpeningFrame).fit, BoxFit.contain);
|
||||||
expect(backdrop.fit, BoxFit.cover);
|
|
||||||
expect(
|
|
||||||
find.byKey(const Key('player-opening-poster-blur')),
|
|
||||||
findsOneWidget,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('should_reuse_decoded_sprite_when_parent_rebuilds', (
|
testWidgets('should_reuse_decoded_sprite_when_parent_rebuilds', (
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ void main() {
|
|||||||
|
|
||||||
expect(find.byKey(const Key('player-opening-frame')), findsOneWidget);
|
expect(find.byKey(const Key('player-opening-frame')), findsOneWidget);
|
||||||
expect(find.byKey(const Key('fake-opening')), findsOneWidget);
|
expect(find.byKey(const Key('fake-opening')), findsOneWidget);
|
||||||
expect(_openingOpacity(tester), 1);
|
|
||||||
expect(_opacity(tester), 0);
|
expect(_opacity(tester), 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -49,7 +48,7 @@ void main() {
|
|||||||
expect(find.byKey(const Key('player-opening-frame')), findsNothing);
|
expect(find.byKey(const Key('player-opening-frame')), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('should_cross_fade_video_over_opening_frame', (
|
testWidgets('should_show_cover_until_first_playback_frame', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
final controller = await _pumpSurface(
|
final controller = await _pumpSurface(
|
||||||
@@ -69,7 +68,6 @@ void main() {
|
|||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
expect(_opacity(tester), 0);
|
expect(_opacity(tester), 0);
|
||||||
expect(_openingOpacity(tester), 1);
|
|
||||||
expect(find.byKey(const Key('player-opening-frame')), findsOneWidget);
|
expect(find.byKey(const Key('player-opening-frame')), findsOneWidget);
|
||||||
|
|
||||||
controller.value = const VideoPlayerValue(
|
controller.value = const VideoPlayerValue(
|
||||||
@@ -82,25 +80,36 @@ void main() {
|
|||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
expect(_opacity(tester), 1);
|
expect(_opacity(tester), 1);
|
||||||
expect(find.byKey(const Key('player-opening-frame')), findsOneWidget);
|
expect(find.byKey(const Key('player-opening-frame')), findsNothing);
|
||||||
expect(
|
expect(
|
||||||
tester
|
tester
|
||||||
.widget<AnimatedOpacity>(
|
.widget<AnimatedOpacity>(
|
||||||
find.byKey(const Key('player-video-opacity')),
|
find.byKey(const Key('player-video-opacity')),
|
||||||
)
|
)
|
||||||
.duration,
|
.duration,
|
||||||
const Duration(milliseconds: 200),
|
Duration.zero,
|
||||||
);
|
);
|
||||||
await tester.pump(const Duration(milliseconds: 100));
|
});
|
||||||
final halfway = _fadeValue(tester);
|
|
||||||
expect(halfway, greaterThan(0));
|
testWidgets('should_skip_cover_when_prewarmed_frame_is_available', (
|
||||||
expect(halfway, lessThan(1));
|
tester,
|
||||||
final openingHalfway = _openingFadeValue(tester);
|
) async {
|
||||||
expect(openingHalfway, greaterThan(0));
|
await _pumpSurface(
|
||||||
expect(openingHalfway, lessThan(1));
|
tester,
|
||||||
await tester.pump(const Duration(milliseconds: 100));
|
openingFrame: const ColoredBox(color: Colors.red),
|
||||||
expect(_fadeValue(tester), 1);
|
holdOpeningUntilFirstFrame: true,
|
||||||
expect(_openingFadeValue(tester), 0);
|
preparedFrameAvailable: true,
|
||||||
|
value: const VideoPlayerValue(
|
||||||
|
duration: Duration(minutes: 1),
|
||||||
|
size: Size(1280, 720),
|
||||||
|
position: Duration.zero,
|
||||||
|
isInitialized: true,
|
||||||
|
isPlaying: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.byKey(const Key('player-opening-frame')), findsNothing);
|
||||||
|
expect(_opacity(tester), 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('should_show_video_error_above_retained_fallback', (
|
testWidgets('should_show_video_error_above_retained_fallback', (
|
||||||
@@ -147,6 +156,7 @@ void main() {
|
|||||||
handoff: handoff,
|
handoff: handoff,
|
||||||
videoUrl: current.dataSource,
|
videoUrl: current.dataSource,
|
||||||
theater: false,
|
theater: false,
|
||||||
|
openingFrame: const ColoredBox(color: Colors.red),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -155,6 +165,7 @@ void main() {
|
|||||||
|
|
||||||
await pump(previous);
|
await pump(previous);
|
||||||
expect(find.byKey(const Key('player-video-handoff')), findsOneWidget);
|
expect(find.byKey(const Key('player-video-handoff')), findsOneWidget);
|
||||||
|
expect(find.byKey(const Key('player-opening-frame')), findsNothing);
|
||||||
expect(_opacity(tester), 1);
|
expect(_opacity(tester), 1);
|
||||||
|
|
||||||
await pump(null);
|
await pump(null);
|
||||||
@@ -168,6 +179,7 @@ Future<VideoPlayerController> _pumpSurface(
|
|||||||
VideoPlayerValue? value,
|
VideoPlayerValue? value,
|
||||||
Widget? openingFrame,
|
Widget? openingFrame,
|
||||||
bool holdOpeningUntilFirstFrame = false,
|
bool holdOpeningUntilFirstFrame = false,
|
||||||
|
bool preparedFrameAvailable = false,
|
||||||
}) async {
|
}) async {
|
||||||
final controller = VideoPlayerController.networkUrl(
|
final controller = VideoPlayerController.networkUrl(
|
||||||
Uri.parse('http://192.168.31.187:8081/files/video.mp4'),
|
Uri.parse('http://192.168.31.187:8081/files/video.mp4'),
|
||||||
@@ -183,6 +195,7 @@ Future<VideoPlayerController> _pumpSurface(
|
|||||||
theater: false,
|
theater: false,
|
||||||
openingFrame: openingFrame,
|
openingFrame: openingFrame,
|
||||||
holdOpeningUntilFirstFrame: holdOpeningUntilFirstFrame,
|
holdOpeningUntilFirstFrame: holdOpeningUntilFirstFrame,
|
||||||
|
preparedFrameAvailable: preparedFrameAvailable,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -202,27 +215,3 @@ double _opacity(WidgetTester tester) {
|
|||||||
.widget<AnimatedOpacity>(find.byKey(const Key('player-video-opacity')))
|
.widget<AnimatedOpacity>(find.byKey(const Key('player-video-opacity')))
|
||||||
.opacity;
|
.opacity;
|
||||||
}
|
}
|
||||||
|
|
||||||
double _openingOpacity(WidgetTester tester) {
|
|
||||||
return tester
|
|
||||||
.widget<AnimatedOpacity>(
|
|
||||||
find.byKey(const Key('player-opening-frame')),
|
|
||||||
)
|
|
||||||
.opacity;
|
|
||||||
}
|
|
||||||
|
|
||||||
double _fadeValue(WidgetTester tester) {
|
|
||||||
final fade = find.descendant(
|
|
||||||
of: find.byKey(const Key('player-video-opacity')),
|
|
||||||
matching: find.byType(FadeTransition),
|
|
||||||
);
|
|
||||||
return tester.widget<FadeTransition>(fade).opacity.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
double _openingFadeValue(WidgetTester tester) {
|
|
||||||
final fade = find.descendant(
|
|
||||||
of: find.byKey(const Key('player-opening-frame')),
|
|
||||||
matching: find.byType(FadeTransition),
|
|
||||||
);
|
|
||||||
return tester.widget<FadeTransition>(fade).opacity.value;
|
|
||||||
}
|
|
||||||
Reference in new issue
Block a user