fix(player): 观看全集复用抓帧首屏

This commit is contained in:
Zhou Weidong committed 2026-10-26 17:20:24 +08:00
1 parent 4d3d067f77
commit 55fc2796a9
4 files changed
+136 -36

No files matched your search

@@ -0,0 +1,63 @@
// Author: fruit.com.cn
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class PlayerFrameCapture {
const PlayerFrameCapture._();
static const int budgetMilliseconds = 32;
static Future<Uint8List?> capture({
required BuildContext context,
required GlobalKey boundaryKey,
required String source,
}) async {
final boundary = boundaryKey.currentContext?.findRenderObject();
if (boundary is! RenderRepaintBoundary) {
_log('[player-entry] phase=frame-capture-unavailable source=$source');
return null;
}
final stopwatch = Stopwatch()..start();
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();
if (stopwatch.elapsedMilliseconds > budgetMilliseconds) {
_log(
'[player-entry] phase=frame-capture-over-budget source=$source '
'ms=${stopwatch.elapsedMilliseconds} budget_ms=$budgetMilliseconds',
);
}
if (data == null) {
_log('[player-entry] phase=frame-capture-empty source=$source');
return null;
}
final bytes = data.buffer.asUint8List(
data.offsetInBytes,
data.lengthInBytes,
);
_log(
'[player-entry] phase=frame-captured source=$source '
'bytes=${bytes.length} ms=${stopwatch.elapsedMilliseconds}',
);
return bytes;
} catch (error) {
_log(
'[player-entry] phase=frame-capture-failed source=$source '
'error=$error',
);
return null;
}
}
static void _log(String message) {
if (kDebugMode) debugPrint(message);
}
}
+12
View File
@@ -32,6 +32,7 @@ import '../discover/search_page.dart';
import '../drama_detail/drama_detail_view.dart';
import '../mine/mine_types.dart';
import '../mine/mine_view.dart';
import 'player_frame_capture.dart';
import 'player_view.dart';
import 'widgets/drama_comments_sheet.dart';
import 'widgets/episode_picker_sheet.dart';
@@ -64,6 +65,9 @@ class PlayerLogic extends ChangeNotifier {
final Future<void> Function() _waitForFrame;
late PageController pages;
final GlobalKey renderBoundaryKey = GlobalKey(
debugLabel: 'player-render-boundary',
);
late PlaybackQueue queue;
late List<int> visibleIndices;
late PlaybackSession session;
@@ -1226,6 +1230,12 @@ class PlayerLogic extends ChangeNotifier {
Drama drama,
int index,
) async {
final initialFrameBytes = await PlayerFrameCapture.capture(
context: context,
boundaryKey: renderBoundaryKey,
source: 'watch-all',
);
if (!context.mounted) return;
await pauseForFullPlayer();
if (!context.mounted) return;
await Navigator.of(context).push(
@@ -1235,6 +1245,8 @@ class PlayerLogic extends ChangeNotifier {
initialIndex: index,
wallet: wallet,
downloads: downloads,
revealAfterFirstFrame: initialFrameBytes != null,
initialFrameBytes: initialFrameBytes,
),
),
);
+15 -36
View File
@@ -1,11 +1,9 @@
// 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';
@@ -34,6 +32,7 @@ import '../../l10n/app_localizations.dart';
import '../home/home_feed_channel.dart';
import 'comments_auth.dart';
import 'player_binding.dart';
import 'player_frame_capture.dart';
import 'player_logic.dart';
import 'widgets/home_feed_header.dart';
import 'widgets/home_feed_overlay.dart';
@@ -146,7 +145,6 @@ 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 = {};
@@ -159,6 +157,14 @@ class _PlayerPageState extends State<PlayerPage>
void initState() {
super.initState();
_firstFrameReady = !widget.revealAfterFirstFrame;
if (kDebugMode &&
widget.revealAfterFirstFrame &&
widget.initialFrameBytes != null) {
debugPrint(
'[player-entry] phase=initial-frame-shown '
'bytes=${widget.initialFrameBytes!.length}',
);
}
final handoff = widget.handoff;
logic = PlayerBinding().create(
history: context.read<WatchHistoryRepository>(),
@@ -605,7 +611,7 @@ class _PlayerPageState extends State<PlayerPage>
fit: StackFit.expand,
children: [
RepaintBoundary(
key: _renderBoundaryKey,
key: logic.renderBoundaryKey,
child: PlayerRenderStack(
pageController: _pageController,
slots: _renderSlots(indices),
@@ -1239,7 +1245,11 @@ class _PlayerPageState extends State<PlayerPage>
seriesEpisodes.insert(0, sourceEpisode);
episodeIndex = 0;
}
final initialFrameBytes = await _captureCurrentFrame();
final initialFrameBytes = await PlayerFrameCapture.capture(
context: context,
boundaryKey: logic.renderBoundaryKey,
source: 'full-player',
);
if (!mounted || !context.mounted) return;
final sourceUserWantsPlay = logic.session.userWantsPlay;
try {
@@ -1279,37 +1289,6 @@ 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);