fix(player): 规范预热回收日志并抓帧过渡

This commit is contained in:
Zhou Weidong committed 2026-10-26 17:13:56 +08:00
1 parent bb2c729a7e
commit 4d3d067f77
6 files changed
+186 -40

No files matched your search

+89 -9
View File
@@ -1,9 +1,11 @@
// Author: fruit.com.cn
import 'dart:async';
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
@@ -91,6 +93,7 @@ class PlayerPage extends StatefulWidget {
this.downloads,
this.handoff,
this.revealAfterFirstFrame = false,
this.initialFrameBytes,
});
final Drama drama;
@@ -119,6 +122,7 @@ class PlayerPage extends StatefulWidget {
final DownloadController? downloads;
final PlayerPageHandoff? handoff;
final bool revealAfterFirstFrame;
final Uint8List? initialFrameBytes;
@override
State<PlayerPage> createState() => _PlayerPageState();
@@ -142,6 +146,7 @@ class _PlayerPageState extends State<PlayerPage>
bool _swipePrewarmCaptured = false;
bool _presentationActive = false;
late bool _firstFrameReady;
final GlobalKey _renderBoundaryKey = GlobalKey();
bool _leavingHandoff = false;
bool _preparingFullPlayer = false;
final Set<String> _precachedPosterUrls = {};
@@ -599,10 +604,13 @@ class _PlayerPageState extends State<PlayerPage>
return Stack(
fit: StackFit.expand,
children: [
PlayerRenderStack(
pageController: _pageController,
slots: _renderSlots(indices),
onCurrentVideoVisible: _handleCurrentVideoVisible,
RepaintBoundary(
key: _renderBoundaryKey,
child: PlayerRenderStack(
pageController: _pageController,
slots: _renderSlots(indices),
onCurrentVideoVisible: _handleCurrentVideoVisible,
),
),
pageLayer,
],
@@ -677,7 +685,25 @@ class _PlayerPageState extends State<PlayerPage>
logic.blockedEpisodeId != null ||
logic.session.error != null;
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;
if (widget.handoff == null) return presentedPlayer;
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() {
final entry = logic.queue.entryAt(logic.session.currentIndex);
_openMore(context, entry.episode);
@@ -1195,6 +1239,8 @@ class _PlayerPageState extends State<PlayerPage>
seriesEpisodes.insert(0, sourceEpisode);
episodeIndex = 0;
}
final initialFrameBytes = await _captureCurrentFrame();
if (!mounted || !context.mounted) return;
final sourceUserWantsPlay = logic.session.userWantsPlay;
try {
await logic.pauseForFullPlayer();
@@ -1207,17 +1253,20 @@ class _PlayerPageState extends State<PlayerPage>
setState(() => _presentationActive = true);
final route = Navigator.of(context).push<void>(
PageRouteBuilder<void>(
opaque: false,
opaque: true,
barrierDismissible: false,
transitionDuration: Duration.zero,
reverseTransitionDuration: Duration.zero,
transitionDuration: const Duration(milliseconds: 180),
reverseTransitionDuration: const Duration(milliseconds: 180),
transitionsBuilder: (_, animation, _, child) =>
FadeTransition(opacity: animation, child: child),
pageBuilder: (_, _, _) => PlayerPage(
drama: seriesDrama,
initialIndex: episodeIndex,
handoff: handoff,
wallet: logic.wallet,
downloads: logic.downloads,
revealAfterFirstFrame: true,
revealAfterFirstFrame: initialFrameBytes != null,
initialFrameBytes: initialFrameBytes,
),
),
);
@@ -1230,6 +1279,37 @@ class _PlayerPageState extends State<PlayerPage>
_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 {
if (_leavingHandoff) return;
setState(() => _leavingHandoff = true);