Files
ReelShort/lib/pages/player/player_frame_capture.dart
T

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);
}
}