From d28983694806da672e406acd8c4d46f4831f6360 Mon Sep 17 00:00:00 2001 From: Predidit <34627277+Predidit@users.noreply.github.com> Date: Thu, 4 Jun 2026 19:44:05 +0800 Subject: [PATCH] draft --- .../lib/src/player/native/player/real.dart | 220 ++++++++++-------- media_kit/lib/src/player/platform_player.dart | 8 + media_kit/lib/src/player/player.dart | 20 ++ media_kit/lib/src/player/web/player/real.dart | 12 + media_kit_test/lib/tests/08.screenshot.dart | 35 ++- 5 files changed, 195 insertions(+), 100 deletions(-) diff --git a/media_kit/lib/src/player/native/player/real.dart b/media_kit/lib/src/player/native/player/real.dart index 83aaf49d1..c94235cc6 100644 --- a/media_kit/lib/src/player/native/player/real.dart +++ b/media_kit/lib/src/player/native/player/real.dart @@ -1131,6 +1131,35 @@ class NativePlayer extends PlatformPlayer { {String? format = 'image/jpeg', bool synchronized = true, bool includeLibassSubtitles = false}) async { + return _screenshotWithOptions( + format: format, + synchronized: synchronized, + includeLibassSubtitles: includeLibassSubtitles, + safe: false, + ); + } + + /// Takes the snapshot of the current video frame & returns bytes that are + /// safe to retain or transfer to another isolate. + @override + Future safeScreenshot( + {String? format = 'image/jpeg', + bool synchronized = true, + bool includeLibassSubtitles = false}) async { + return _screenshotWithOptions( + format: format, + synchronized: synchronized, + includeLibassSubtitles: includeLibassSubtitles, + safe: true, + ); + } + + Future _screenshotWithOptions({ + required String? format, + required bool synchronized, + required bool includeLibassSubtitles, + required bool safe, + }) async { Future function() async { if (![ 'image/jpeg', @@ -1157,6 +1186,7 @@ class NativePlayer extends PlatformPlayer { NativeLibrary.path, format, includeLibassSubtitles, + safe, ), ); } @@ -2662,12 +2692,14 @@ class _ScreenshotData { final String lib; final String? format; final bool includeLibassSubtitles; + final bool safe; const _ScreenshotData( this.ctx, this.lib, this.format, this.includeLibassSubtitles, + this.safe, ); } @@ -2687,109 +2719,115 @@ Uint8List? _screenshot(_ScreenshotData data) { ]; final result = calloc(); + final pointers = >[]; + Pointer> arr = nullptr; - final pointers = args.map>((e) { - return e.toNativeUtf8(); - }).toList(); - final Pointer> arr = calloc.allocate(args.join().length); - for (int i = 0; i < args.length; i++) { - arr[i] = pointers[i]; - } - mpv.mpv_command_ret( - ctx, - arr.cast(), - result.cast(), - ); + try { + for (final arg in args) { + pointers.add(arg.toNativeUtf8()); + } + arr = calloc>(args.length + 1); + for (int i = 0; i < pointers.length; i++) { + (arr + i).value = pointers[i]; + } - Uint8List? image; + mpv.mpv_command_ret( + ctx, + arr.cast(), + result.cast(), + ); - if (result.ref.format == generated.mpv_format.MPV_FORMAT_NODE_MAP) { - int? w, h, stride; - Uint8List? bytes; + Uint8List? image; + + if (result.ref.format == generated.mpv_format.MPV_FORMAT_NODE_MAP) { + int? w, h, stride; + Uint8List? bytes; + + final map = result.ref.u.list; + for (int i = 0; i < map.ref.num; i++) { + final key = map.ref.keys[i].cast().toDartString(); + final value = map.ref.values[i]; + switch (value.format) { + case generated.mpv_format.MPV_FORMAT_INT64: + switch (key) { + case 'w': + w = value.u.int64; + break; + case 'h': + h = value.u.int64; + break; + case 'stride': + stride = value.u.int64; + break; + } + break; + case generated.mpv_format.MPV_FORMAT_BYTE_ARRAY: + switch (key) { + case 'data': + final data = value.u.ba.ref.data.cast(); + bytes = data.asTypedList(value.u.ba.ref.size); + break; + } + break; + } + } - final map = result.ref.u.list; - for (int i = 0; i < map.ref.num; i++) { - final key = map.ref.keys[i].cast().toDartString(); - final value = map.ref.values[i]; - switch (value.format) { - case generated.mpv_format.MPV_FORMAT_INT64: - switch (key) { - case 'w': - w = value.u.int64; - break; - case 'h': - h = value.u.int64; + if (w != null && h != null && stride != null && bytes != null) { + switch (format) { + case 'image/jpeg': + { + final pixels = Image( + width: w, + height: h, + numChannels: 3, + ); + for (final pixel in pixels) { + final x = pixel.x; + final y = pixel.y; + final i = (y * stride) + (x * 4); + pixel.b = bytes[i]; + pixel.g = bytes[i + 1]; + pixel.r = bytes[i + 2]; + } + image = encodeJpg(pixels); break; - case 'stride': - stride = value.u.int64; + } + case 'image/png': + { + final pixels = Image( + width: w, + height: h, + numChannels: 3, + ); + for (final pixel in pixels) { + final x = pixel.x; + final y = pixel.y; + final i = (y * stride) + (x * 4); + pixel.b = bytes[i]; + pixel.g = bytes[i + 1]; + pixel.r = bytes[i + 2]; + } + image = encodePng(pixels); break; - } - break; - case generated.mpv_format.MPV_FORMAT_BYTE_ARRAY: - switch (key) { - case 'data': - final data = value.u.ba.ref.data.cast(); - bytes = data.asTypedList(value.u.ba.ref.size); + } + case null: + { + image = data.safe ? Uint8List.fromList(bytes) : bytes; break; - } - break; + } + } } } - if (w != null && h != null && stride != null && bytes != null) { - switch (format) { - case 'image/jpeg': - { - final pixels = Image( - width: w, - height: h, - numChannels: 3, - ); - for (final pixel in pixels) { - final x = pixel.x; - final y = pixel.y; - final i = (y * stride) + (x * 4); - pixel.b = bytes[i]; - pixel.g = bytes[i + 1]; - pixel.r = bytes[i + 2]; - } - image = encodeJpg(pixels); - break; - } - case 'image/png': - { - final pixels = Image( - width: w, - height: h, - numChannels: 3, - ); - for (final pixel in pixels) { - final x = pixel.x; - final y = pixel.y; - final i = (y * stride) + (x * 4); - pixel.b = bytes[i]; - pixel.g = bytes[i + 1]; - pixel.r = bytes[i + 2]; - } - image = encodePng(pixels); - break; - } - case null: - { - image = bytes; - break; - } - } + return image; + } finally { + pointers.forEach(calloc.free); + mpv.mpv_free_node_contents(result.cast()); + if (arr != nullptr) { + calloc.free(arr); } + calloc.free(result.cast()); } - - pointers.forEach(calloc.free); - mpv.mpv_free_node_contents(result.cast()); - - calloc.free(arr); - calloc.free(result.cast()); - - return image; } // -------------------------------------------------- diff --git a/media_kit/lib/src/player/platform_player.dart b/media_kit/lib/src/player/platform_player.dart index 2ee5e7749..b20650f20 100644 --- a/media_kit/lib/src/player/platform_player.dart +++ b/media_kit/lib/src/player/platform_player.dart @@ -291,6 +291,14 @@ abstract class PlatformPlayer { ); } + Future safeScreenshot( + {String? format = 'image/jpeg', + bool includeLibassSubtitles = false}) async { + throw UnimplementedError( + '[PlatformPlayer.safeScreenshot] is not implemented', + ); + } + Future get handle { throw UnimplementedError( '[PlatformPlayer.handle] is not implemented', diff --git a/media_kit/lib/src/player/player.dart b/media_kit/lib/src/player/player.dart index aa950d29e..8924a401c 100644 --- a/media_kit/lib/src/player/player.dart +++ b/media_kit/lib/src/player/player.dart @@ -326,6 +326,26 @@ class Player { ); } + /// Takes the snapshot of the current video frame & returns image bytes that + /// are safe to retain or transfer to another isolate. + /// + /// This is useful for `format == null`, where the native backend returns raw + /// BGRA pixels. [screenshot] keeps the low-copy raw path for performance, but + /// those bytes may be backed by native memory whose lifetime ends after the + /// call. [safeScreenshot] copies raw pixels into Dart-owned memory before + /// returning them. + /// + /// Encoded formats (`image/jpeg` & `image/png`) are already safe; for them, + /// this method behaves the same as [screenshot]. + Future safeScreenshot( + {String? format = 'image/jpeg', + bool includeLibassSubtitles = false}) async { + return platform?.safeScreenshot( + format: format, + includeLibassSubtitles: includeLibassSubtitles, + ); + } + /// Internal platform specific identifier for this [Player] instance. /// /// Since, [int] is a primitive type, it can be used to pass this [Player] instance to native code without directly depending upon this library. diff --git a/media_kit/lib/src/player/web/player/real.dart b/media_kit/lib/src/player/web/player/real.dart index 7c3b31c73..b4d5d0d1e 100644 --- a/media_kit/lib/src/player/web/player/real.dart +++ b/media_kit/lib/src/player/web/player/real.dart @@ -1412,6 +1412,18 @@ class WebPlayer extends PlatformPlayer { } } + @override + Future safeScreenshot( + {String? format = 'image/jpeg', + bool synchronized = true, + bool includeLibassSubtitles = false}) { + return screenshot( + format: format, + synchronized: synchronized, + includeLibassSubtitles: includeLibassSubtitles, + ); + } + void _loadSource(Media media) { try { if (_isHLS(media.uri)) { diff --git a/media_kit_test/lib/tests/08.screenshot.dart b/media_kit_test/lib/tests/08.screenshot.dart index 0bfb01862..c23a846d0 100644 --- a/media_kit_test/lib/tests/08.screenshot.dart +++ b/media_kit_test/lib/tests/08.screenshot.dart @@ -7,7 +7,7 @@ import '../common/sources/sources.dart'; import '../common/widgets.dart'; class Screenshot extends StatefulWidget { - const Screenshot({Key? key}) : super(key: key); + const Screenshot({super.key}); @override State createState() => _ScreenshotState(); @@ -21,6 +21,7 @@ class _ScreenshotState extends State { ); Image? image; + bool threadSafe = true; @override void initState() { @@ -35,18 +36,34 @@ class _ScreenshotState extends State { super.dispose(); } + Future takeScreenshot() async { + final screenshot = await (threadSafe + ? player.safeScreenshot(format: 'image/png') + : player.screenshot(format: 'image/png')); + if (!mounted) { + return; + } + setState(() { + if (screenshot != null) { + image = Image.memory(screenshot); + } + }); + } + List get items => [ const SizedBox(height: 16.0), + SwitchListTile( + title: const Text('Thread safe'), + value: threadSafe, + onChanged: (value) { + setState(() { + threadSafe = value; + }); + }, + ), Center( child: ElevatedButton( - onPressed: () async { - final screenshot = await player.screenshot(); - if (screenshot != null) { - setState(() { - image = Image.memory(screenshot); - }); - } - }, + onPressed: takeScreenshot, child: const Text('Screenshot'), ), ),