// Author: fruit.com.cn import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import '../../core/config/app_config.dart'; class PreparedPlayerFrame { const PreparedPlayerFrame({this.bytes, this.provider}); final Uint8List? bytes; final ImageProvider? provider; bool get isReady => bytes != null || provider != null; } class PlayerFrameCapture { const PlayerFrameCapture._(); static const int budgetMilliseconds = 32; static Future captureAndPrecache({ required BuildContext context, required GlobalKey boundaryKey, required String source, String? fallbackPosterUrl, }) async { final bytes = await capture( context: context, boundaryKey: boundaryKey, source: source, ); if (bytes != null) { if (!context.mounted) return const PreparedPlayerFrame(); final provider = MemoryImage(bytes); if (await _precache(context, provider, source: source, kind: 'capture')) { _log( '[player-entry] phase=frame-precached source=$source kind=capture', ); return PreparedPlayerFrame(bytes: bytes, provider: provider); } } final poster = fallbackPosterUrl?.trim() ?? ''; if (poster.isNotEmpty) { if (!context.mounted) return const PreparedPlayerFrame(); final provider = NetworkImage(AppConfig.mediaUrl(poster)); if (await _precache(context, provider, source: source, kind: 'poster')) { _log('[player-entry] phase=frame-precached source=$source kind=poster'); return PreparedPlayerFrame(provider: provider); } } return const PreparedPlayerFrame(); } static Future 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 Future _precache( BuildContext context, ImageProvider provider, { required String source, required String kind, }) async { var failed = false; try { await precacheImage( provider, context, onError: (_, _) { failed = true; }, ); } catch (_) { failed = true; } if (failed) { _log( '[player-entry] phase=frame-precache-failed source=$source kind=$kind', ); } return !failed; } static void _log(String message) { if (kDebugMode) debugPrint(message); } }