135 lines
3.8 KiB
Dart
135 lines
3.8 KiB
Dart
// 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<Object>? provider;
|
|
|
|
bool get isReady => bytes != null || provider != null;
|
|
}
|
|
|
|
class PlayerFrameCapture {
|
|
const PlayerFrameCapture._();
|
|
|
|
static const int budgetMilliseconds = 32;
|
|
|
|
static Future<PreparedPlayerFrame> 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<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 Future<bool> _precache(
|
|
BuildContext context,
|
|
ImageProvider<Object> 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);
|
|
}
|
|
}
|