Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
220 changes: 129 additions & 91 deletions media_kit/lib/src/player/native/player/real.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<Uint8List?> safeScreenshot(
{String? format = 'image/jpeg',
bool synchronized = true,
bool includeLibassSubtitles = false}) async {
return _screenshotWithOptions(
format: format,
synchronized: synchronized,
includeLibassSubtitles: includeLibassSubtitles,
safe: true,
);
}

Future<Uint8List?> _screenshotWithOptions({
required String? format,
required bool synchronized,
required bool includeLibassSubtitles,
required bool safe,
}) async {
Future<Uint8List?> function() async {
if (![
'image/jpeg',
Expand All @@ -1157,6 +1186,7 @@ class NativePlayer extends PlatformPlayer {
NativeLibrary.path,
format,
includeLibassSubtitles,
safe,
),
);
}
Expand Down Expand Up @@ -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,
);
}

Expand All @@ -2687,109 +2719,115 @@ Uint8List? _screenshot(_ScreenshotData data) {
];

final result = calloc<generated.mpv_node>();
final pointers = <Pointer<Utf8>>[];
Pointer<Pointer<Utf8>> arr = nullptr;

final pointers = args.map<Pointer<Utf8>>((e) {
return e.toNativeUtf8();
}).toList();
final Pointer<Pointer<Utf8>> 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<Pointer<Utf8>>(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<Utf8>().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<Uint8>();
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<Utf8>().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<Uint8>();
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;
}

// --------------------------------------------------
8 changes: 8 additions & 0 deletions media_kit/lib/src/player/platform_player.dart
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,14 @@ abstract class PlatformPlayer {
);
}

Future<Uint8List?> safeScreenshot(
{String? format = 'image/jpeg',
bool includeLibassSubtitles = false}) async {
throw UnimplementedError(
'[PlatformPlayer.safeScreenshot] is not implemented',
);
}

Future<int> get handle {
throw UnimplementedError(
'[PlatformPlayer.handle] is not implemented',
Expand Down
20 changes: 20 additions & 0 deletions media_kit/lib/src/player/player.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<Uint8List?> 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.
Expand Down
12 changes: 12 additions & 0 deletions media_kit/lib/src/player/web/player/real.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1412,6 +1412,18 @@ class WebPlayer extends PlatformPlayer {
}
}

@override
Future<Uint8List?> 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)) {
Expand Down
35 changes: 26 additions & 9 deletions media_kit_test/lib/tests/08.screenshot.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<Screenshot> createState() => _ScreenshotState();
Expand All @@ -21,6 +21,7 @@ class _ScreenshotState extends State<Screenshot> {
);

Image? image;
bool threadSafe = true;

@override
void initState() {
Expand All @@ -35,18 +36,34 @@ class _ScreenshotState extends State<Screenshot> {
super.dispose();
}

Future<void> 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<Widget> 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'),
),
),
Expand Down
Loading