Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

On demand processing in background isolate #21

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## [UNRELEASED]
## [0.1.1]

* Fix [#11](https://github.com/ueman/screenrecorder/issues/11)
* Custom exporters can actually be used

## [0.1.0]

Expand Down
6 changes: 3 additions & 3 deletions example/macos/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.13;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
Expand Down Expand Up @@ -424,7 +424,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.13;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
Expand Down Expand Up @@ -471,7 +471,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.13;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
Expand Down
56 changes: 0 additions & 56 deletions lib/src/exporter.dart
Original file line number Diff line number Diff line change
@@ -1,63 +1,7 @@
import 'package:flutter/material.dart';
import 'package:image/image.dart' as image;
import 'dart:ui' as ui show ImageByteFormat;
import 'package:flutter/foundation.dart';
import 'package:screen_recorder/src/frame.dart';

abstract class Exporter {
void onNewFrame(Frame frame);

Future<List<int>?> export();
}

class GifExporter implements Exporter {
final List<Frame> _frames = [];

@override
Future<List<int>?> export() async {
if (_frames.isEmpty) {
return null;
}
List<RawFrame> bytes = [];
for (final frame in _frames) {
final i = await frame.image.toByteData(format: ui.ImageByteFormat.png);
if (i != null) {
bytes.add(RawFrame(16, i));
} else {
print('Skipped frame while enconding');
}
}
final result = compute(_export, bytes);
_frames.clear();
return result;
}

@override
void onNewFrame(Frame frame) {
_frames.add(frame);
}

static Future<List<int>?> _export(List<RawFrame> frames) async {
final animation = image.Animation();
animation.backgroundColor = Colors.transparent.value;
for (final frame in frames) {
final iAsBytes = frame.image.buffer.asUint8List();
final decodedImage = image.decodePng(iAsBytes);

if (decodedImage == null) {
print('Skipped frame while enconding');
continue;
}
decodedImage.duration = frame.durationInMillis;
animation.addFrame(decodedImage);
}
return image.encodeGifAnimation(animation);
}
}

class RawFrame {
RawFrame(this.durationInMillis, this.image);

final int durationInMillis;
final ByteData image;
}
7 changes: 7 additions & 0 deletions lib/src/gif/gif_exporter.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import 'package:screen_recorder/screen_recorder.dart';

import 'io_gif_exporter.dart' if (dart.library.html) 'web_gif_exporter.dart';

abstract class GifExporter implements Exporter {
factory GifExporter() => gifExporter();
}
109 changes: 109 additions & 0 deletions lib/src/gif/io_gif_exporter.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import 'dart:async';
import 'dart:isolate';

import 'package:flutter/material.dart';
import 'package:image/image.dart' as image;
import 'dart:ui' as ui show ImageByteFormat;
import 'package:flutter/foundation.dart';
import 'package:screen_recorder/src/frame.dart';
import 'package:screen_recorder/src/gif/gif_exporter.dart';
import 'package:stream_channel/isolate_channel.dart';

GifExporter gifExporter() => IoGifExporter();

class IoGifExporter implements GifExporter {
IoGifExporter() {
_controller.stream.listen((event) async {
if (event is _InitIsolateMessage) {
await _initIsolate();
}
if (event is Frame) {
final i = await event.image.toByteData(format: ui.ImageByteFormat.png);
if (i != null) {
channel!.sink.add(RawFrame(16, i));
} else {
print('Skipped frame while enconding');
}
}
});

_controller.add(_InitIsolateMessage());
}

StreamController _controller = StreamController();

ReceivePort receivePort = ReceivePort();

IsolateChannel? channel;

Isolate? _isolate;

Future<void> _initIsolate() async {
channel = new IsolateChannel.connectReceive(receivePort);

_isolate = await Isolate.spawn<SendPort>(
_isolateEntryPoint,
receivePort.sendPort,
debugName: 'GifExporterIsolate',
);
}

@override
Future<List<int>?> export() async {
channel!.sink.add(_ExportMessage());
return await channel!.stream.first;
}

@override
void onNewFrame(Frame frame) {
_controller.add(frame);
}
}

class RawFrame {
RawFrame(this.durationInMillis, this.image);

final int durationInMillis;
final ByteData image;
}

void _isolateEntryPoint(SendPort sendPort) {
final exporter = _InternalExporter();
IsolateChannel channel = new IsolateChannel.connectSend(sendPort);
channel.stream.listen((message) {
if (message is RawFrame) {
exporter.add(message);
}
if (message is _ExportMessage) {
final gif = exporter.export();
channel.sink.add(gif);
}
});
}

class _InternalExporter {
_InternalExporter() {
animation = image.Animation();
animation.backgroundColor = Colors.transparent.value;
}

late image.Animation animation;

void add(RawFrame rawFrame) {
final iAsBytes = rawFrame.image.buffer.asUint8List();
final decodedImage = image.decodePng(iAsBytes);

if (decodedImage == null) {
print('Skipped frame while enconding');
return;
}
decodedImage.duration = rawFrame.durationInMillis;
animation.addFrame(decodedImage);
}

List<int>? export() => image.encodeGifAnimation(animation);
}

class _ExportMessage {}

class _InitIsolateMessage {}
60 changes: 60 additions & 0 deletions lib/src/gif/web_gif_exporter.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
import 'package:image/image.dart' as image;
import 'dart:ui' as ui show ImageByteFormat;
import 'package:flutter/foundation.dart';
import 'package:screen_recorder/src/frame.dart';
import 'package:screen_recorder/src/gif/gif_exporter.dart';

GifExporter gifExporter() => WebGifExporter();

class WebGifExporter implements GifExporter {
final List<Frame> _frames = [];

@override
Future<List<int>?> export() async {
if (_frames.isEmpty) {
return null;
}
List<RawFrame> bytes = [];
for (final frame in _frames) {
final i = await frame.image.toByteData(format: ui.ImageByteFormat.png);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part still will freeze the screen on Web if you have many frames @ueman

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I know. For now it's just an improvement for io platforms. For web it's just the old implementation. (This PR is a few commits behind)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is that this operation(toByteData) we dont have any way to include it on a Web Worker, thats why my other proposal of processing the frames on the way. I dont like much my approach but honestly i dont find any other workaround :S

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So then, for me to get context about the PR, where is the improvement exactly? Capturing or just exporting?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This significantly improves the gif exporting on io platforms. Each frame is directly processed for a gif in a background isolate which means when the user actually requests the gif export, it's almost completely converted to a gif. So for a user this seems like a significant performance improvement.

But as said, it just works on IO platforms and only for gifs as dart:ui APIs can't be used in a background isolate.
But I'm guessing you're more interested in individual frames than a gif 😅

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok see what you mean now.

But yeah, this will still be a pain in my side haha Wondering what middle point we can have. For any reason i dont understand why the operation toByteData is freezing just on Web not in iPhone.

if (i != null) {
bytes.add(RawFrame(16, i));
} else {
print('Skipped frame while enconding');
}
}
final result = compute(_export, bytes);
_frames.clear();
return result;
}

@override
void onNewFrame(Frame frame) {
_frames.add(frame);
}

static Future<List<int>?> _export(List<RawFrame> frames) async {
final animation = image.Animation();
animation.backgroundColor = Colors.transparent.value;
for (final frame in frames) {
final iAsBytes = frame.image.buffer.asUint8List();
final decodedImage = image.decodePng(iAsBytes);

if (decodedImage == null) {
print('Skipped frame while enconding');
continue;
}
decodedImage.duration = frame.durationInMillis;
animation.addFrame(decodedImage);
}
return image.encodeGifAnimation(animation);
}
}

class RawFrame {
RawFrame(this.durationInMillis, this.image);

final int durationInMillis;
final ByteData image;
}
1 change: 1 addition & 0 deletions lib/src/screen_recorder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'dart:ui' as ui show Image;

import 'package:screen_recorder/src/exporter.dart';
import 'package:screen_recorder/src/frame.dart';
import 'package:screen_recorder/src/gif/gif_exporter.dart';

class ScreenRecorderController {
ScreenRecorderController({
Expand Down
3 changes: 2 additions & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: screen_recorder
description: Record your Flutter widgets and export the recordings as a GIF
version: 0.1.0
version: 0.1.1
repository: https://github.com/ueman/screenrecorder
issue_tracker: https://github.com/ueman/screenrecorder/issues
funding:
Expand All @@ -15,6 +15,7 @@ dependencies:
flutter:
sdk: flutter
image: ^3.0.2
stream_channel: ^2.1.1

dev_dependencies:
flutter_test:
Expand Down