diff --git a/CHANGELOG.md b/CHANGELOG.md index aec1b46..0e0a246 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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] diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj index 736ab66..d9333e4 100644 --- a/example/macos/Runner.xcodeproj/project.pbxproj +++ b/example/macos/Runner.xcodeproj/project.pbxproj @@ -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; @@ -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; @@ -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; diff --git a/lib/src/exporter.dart b/lib/src/exporter.dart index aeff809..2a2e404 100644 --- a/lib/src/exporter.dart +++ b/lib/src/exporter.dart @@ -1,7 +1,3 @@ -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 { @@ -9,55 +5,3 @@ abstract class Exporter { Future?> export(); } - -class GifExporter implements Exporter { - final List _frames = []; - - @override - Future?> export() async { - if (_frames.isEmpty) { - return null; - } - List 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?> _export(List 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; -} diff --git a/lib/src/gif/gif_exporter.dart b/lib/src/gif/gif_exporter.dart new file mode 100644 index 0000000..30d8951 --- /dev/null +++ b/lib/src/gif/gif_exporter.dart @@ -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(); +} diff --git a/lib/src/gif/io_gif_exporter.dart b/lib/src/gif/io_gif_exporter.dart new file mode 100644 index 0000000..24408b7 --- /dev/null +++ b/lib/src/gif/io_gif_exporter.dart @@ -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 _initIsolate() async { + channel = new IsolateChannel.connectReceive(receivePort); + + _isolate = await Isolate.spawn( + _isolateEntryPoint, + receivePort.sendPort, + debugName: 'GifExporterIsolate', + ); + } + + @override + Future?> 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? export() => image.encodeGifAnimation(animation); +} + +class _ExportMessage {} + +class _InitIsolateMessage {} diff --git a/lib/src/gif/web_gif_exporter.dart b/lib/src/gif/web_gif_exporter.dart new file mode 100644 index 0000000..7d1b9ff --- /dev/null +++ b/lib/src/gif/web_gif_exporter.dart @@ -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 _frames = []; + + @override + Future?> export() async { + if (_frames.isEmpty) { + return null; + } + List 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?> _export(List 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; +} diff --git a/lib/src/screen_recorder.dart b/lib/src/screen_recorder.dart index 6b26c40..8e569d7 100644 --- a/lib/src/screen_recorder.dart +++ b/lib/src/screen_recorder.dart @@ -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({ diff --git a/pubspec.yaml b/pubspec.yaml index d7fd1bd..cb77d64 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: @@ -15,6 +15,7 @@ dependencies: flutter: sdk: flutter image: ^3.0.2 + stream_channel: ^2.1.1 dev_dependencies: flutter_test: