feat: переделал lottie -> rlottie, с первым прогревом кадров в фоне, как в тг, чтоб лагов не было
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
export 'rlottie_engine_stub.dart'
|
||||
if (dart.library.io) 'rlottie_engine.dart';
|
||||
@@ -0,0 +1,184 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../../utils/logger.dart';
|
||||
|
||||
class DiskClip {
|
||||
DiskClip({
|
||||
required this.px,
|
||||
required this.frameCount,
|
||||
required this.frameRate,
|
||||
required this.durationMs,
|
||||
required this.frames,
|
||||
});
|
||||
|
||||
final int px;
|
||||
final int frameCount;
|
||||
final double frameRate;
|
||||
final int durationMs;
|
||||
final List<Uint8List> frames;
|
||||
}
|
||||
|
||||
class RlottieDiskCache {
|
||||
RlottieDiskCache._();
|
||||
static final RlottieDiskCache instance = RlottieDiskCache._();
|
||||
|
||||
static const _magic = 0x4b524c46;
|
||||
static const _version = 1;
|
||||
static const int _maxBytes = 256 * 1024 * 1024;
|
||||
|
||||
Directory? _dir;
|
||||
Future<Directory>? _dirFuture;
|
||||
|
||||
Future<Directory> _directory() {
|
||||
return _dirFuture ??= () async {
|
||||
final base = await getApplicationSupportDirectory();
|
||||
final dir = Directory('${base.path}/rlottie_frames');
|
||||
if (!await dir.exists()) await dir.create(recursive: true);
|
||||
_dir = dir;
|
||||
return dir;
|
||||
}();
|
||||
}
|
||||
|
||||
String _key(String url, int px) {
|
||||
final digest = sha1.convert(url.codeUnits).toString().substring(0, 20);
|
||||
return '${digest}_$px.krlf';
|
||||
}
|
||||
|
||||
Future<File> _file(String url, int px) async {
|
||||
final dir = await _directory();
|
||||
return File('${dir.path}/${_key(url, px)}');
|
||||
}
|
||||
|
||||
Future<DiskClip?> load(String url, int px) async {
|
||||
try {
|
||||
final file = await _file(url, px);
|
||||
if (!await file.exists()) return null;
|
||||
final bytes = await file.readAsBytes();
|
||||
final clip = await _decode(bytes);
|
||||
if (clip != null) {
|
||||
unawaited(file.setLastModified(DateTime.now()).catchError((_) {}));
|
||||
}
|
||||
return clip;
|
||||
} catch (e) {
|
||||
logger.w('RlottieDiskCache.load failed: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> store({
|
||||
required String url,
|
||||
required int px,
|
||||
required int frameCount,
|
||||
required double frameRate,
|
||||
required int durationMs,
|
||||
required List<Uint8List> frames,
|
||||
}) async {
|
||||
if (frames.isEmpty || frames.length != frameCount) return;
|
||||
try {
|
||||
final bytes = await _encode(px, frameCount, frameRate, durationMs, frames);
|
||||
final file = await _file(url, px);
|
||||
await file.writeAsBytes(bytes, flush: false);
|
||||
unawaited(_evict());
|
||||
} catch (e) {
|
||||
logger.w('RlottieDiskCache.store failed: $e');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<Uint8List> _encode(
|
||||
int px,
|
||||
int frameCount,
|
||||
double frameRate,
|
||||
int durationMs,
|
||||
List<Uint8List> frames,
|
||||
) {
|
||||
return Isolate.run(() {
|
||||
final frameBytes = px * px * 4;
|
||||
final payload = Uint8List(frameBytes * frameCount);
|
||||
for (var i = 0; i < frameCount; i++) {
|
||||
payload.setRange(i * frameBytes, (i + 1) * frameBytes, frames[i]);
|
||||
}
|
||||
final compressed = gzip.encode(payload);
|
||||
final header = ByteData(28);
|
||||
header.setUint32(0, _magic);
|
||||
header.setUint8(4, _version);
|
||||
header.setUint32(8, px);
|
||||
header.setUint32(12, frameCount);
|
||||
header.setFloat64(16, frameRate);
|
||||
header.setUint32(24, durationMs);
|
||||
final out = BytesBuilder();
|
||||
out.add(header.buffer.asUint8List());
|
||||
out.add(compressed);
|
||||
return out.toBytes();
|
||||
});
|
||||
}
|
||||
|
||||
static Future<DiskClip?> _decode(Uint8List bytes) {
|
||||
return Isolate.run(() {
|
||||
if (bytes.length < 28) return null;
|
||||
final header = ByteData.sublistView(bytes, 0, 28);
|
||||
if (header.getUint32(0) != _magic) return null;
|
||||
if (header.getUint8(4) != _version) return null;
|
||||
final px = header.getUint32(8);
|
||||
final frameCount = header.getUint32(12);
|
||||
final frameRate = header.getFloat64(16);
|
||||
final durationMs = header.getUint32(24);
|
||||
final frameBytes = px * px * 4;
|
||||
final payload = Uint8List.fromList(gzip.decode(bytes.sublist(28)));
|
||||
if (payload.length != frameBytes * frameCount) return null;
|
||||
final frames = <Uint8List>[];
|
||||
for (var i = 0; i < frameCount; i++) {
|
||||
frames.add(Uint8List.sublistView(
|
||||
payload, i * frameBytes, (i + 1) * frameBytes));
|
||||
}
|
||||
return DiskClip(
|
||||
px: px,
|
||||
frameCount: frameCount,
|
||||
frameRate: frameRate,
|
||||
durationMs: durationMs,
|
||||
frames: frames,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _evict() async {
|
||||
try {
|
||||
final dir = _dir ?? await _directory();
|
||||
final files = await dir
|
||||
.list()
|
||||
.where((e) => e is File && e.path.endsWith('.krlf'))
|
||||
.cast<File>()
|
||||
.toList();
|
||||
var total = 0;
|
||||
final stats = <(File, FileStat)>[];
|
||||
for (final f in files) {
|
||||
final st = await f.stat();
|
||||
total += st.size;
|
||||
stats.add((f, st));
|
||||
}
|
||||
if (total <= _maxBytes) return;
|
||||
stats.sort((a, b) => a.$2.modified.compareTo(b.$2.modified));
|
||||
for (final (file, st) in stats) {
|
||||
if (total <= _maxBytes) break;
|
||||
total -= st.size;
|
||||
await file.delete().catchError((_) => file);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.w('RlottieDiskCache.evict failed: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> clear() async {
|
||||
try {
|
||||
final dir = await _directory();
|
||||
if (await dir.exists()) await dir.delete(recursive: true);
|
||||
_dir = null;
|
||||
_dirFuture = null;
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
import 'dart:async';
|
||||
import 'dart:isolate';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
|
||||
import '../../utils/logger.dart';
|
||||
import 'rlottie_disk_cache.dart';
|
||||
import 'rlottie_ffi.dart';
|
||||
import 'rlottie_worker.dart';
|
||||
|
||||
class RlottieClip {
|
||||
RlottieClip({required this.key, required this.px});
|
||||
|
||||
final String key;
|
||||
final int px;
|
||||
|
||||
int frameCount = 0;
|
||||
int durationMs = 1000;
|
||||
double frameRate = 60;
|
||||
|
||||
List<ui.Image?> _images = const [];
|
||||
ui.Image? _lastImage;
|
||||
|
||||
final ValueNotifier<int> ready = ValueNotifier(0);
|
||||
|
||||
bool complete = false;
|
||||
int bytes = 0;
|
||||
int lastUsed = 0;
|
||||
int active = 0;
|
||||
|
||||
bool get playable => frameCount > 0;
|
||||
|
||||
ui.Image? frameAt(int index) {
|
||||
if (index < 0 || index >= _images.length) return _lastImage;
|
||||
return _images[index] ?? _lastImage;
|
||||
}
|
||||
|
||||
void _allocate(int count) {
|
||||
_images = List<ui.Image?>.filled(count, null);
|
||||
}
|
||||
|
||||
void _setFrame(int index, ui.Image image) {
|
||||
if (index < 0 || index >= _images.length) {
|
||||
image.dispose();
|
||||
return;
|
||||
}
|
||||
_images[index] = image;
|
||||
_lastImage = image;
|
||||
bytes += px * px * 4;
|
||||
var r = ready.value;
|
||||
while (r < _images.length && _images[r] != null) {
|
||||
r++;
|
||||
}
|
||||
if (r != ready.value) ready.value = r;
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
for (final img in _images) {
|
||||
img?.dispose();
|
||||
}
|
||||
_images = const [];
|
||||
_lastImage = null;
|
||||
bytes = 0;
|
||||
ready.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class _Job {
|
||||
_Job(this.clip, this.url, this.completer);
|
||||
final RlottieClip clip;
|
||||
final String url;
|
||||
final Completer<RlottieClip?> completer;
|
||||
List<Uint8List?> rawFrames = const [];
|
||||
}
|
||||
|
||||
class RlottieEngine {
|
||||
RlottieEngine._();
|
||||
static final RlottieEngine instance = RlottieEngine._();
|
||||
|
||||
static String? debugLibraryPath;
|
||||
|
||||
static const int _maxBytes = 384 * 1024 * 1024;
|
||||
static const int _modelCacheBytes = 20 * 1024 * 1024;
|
||||
|
||||
final Map<String, RlottieClip> _clips = {};
|
||||
final Map<String, Future<RlottieClip?>> _loading = {};
|
||||
final Map<int, _Job> _jobs = {};
|
||||
|
||||
int _totalBytes = 0;
|
||||
int _clock = 0;
|
||||
int _nextJobId = 1;
|
||||
|
||||
bool? _available;
|
||||
Future<SendPort>? _workerFuture;
|
||||
|
||||
int _tick() => ++_clock;
|
||||
String _keyFor(String url, int px) => '$url@$px';
|
||||
|
||||
bool get available {
|
||||
return _available ??= () {
|
||||
final bindings = RlottieBindings.open(path: debugLibraryPath);
|
||||
if (bindings == null) return false;
|
||||
bindings.configureModelCache(_modelCacheBytes);
|
||||
return true;
|
||||
}();
|
||||
}
|
||||
|
||||
Future<SendPort> _worker() {
|
||||
return _workerFuture ??= () async {
|
||||
final receive = ReceivePort();
|
||||
await Isolate.spawn(rlottieWorkerMain, receive.sendPort);
|
||||
final broadcast = receive.asBroadcastStream();
|
||||
final port = await broadcast.first as SendPort;
|
||||
broadcast.listen(_onWorkerMessage);
|
||||
return port;
|
||||
}();
|
||||
}
|
||||
|
||||
void _onWorkerMessage(dynamic message) {
|
||||
if (message is ClipMeta) {
|
||||
_onMeta(message);
|
||||
} else if (message is RenderedFrame) {
|
||||
_onFrame(message);
|
||||
} else if (message is RenderDone) {
|
||||
_onDone(message);
|
||||
} else if (message is RenderError) {
|
||||
_onError(message);
|
||||
}
|
||||
}
|
||||
|
||||
void _onMeta(ClipMeta meta) {
|
||||
final job = _jobs[meta.jobId];
|
||||
if (job == null) return;
|
||||
final clip = job.clip
|
||||
..frameCount = meta.totalFrame
|
||||
..frameRate = meta.frameRate
|
||||
..durationMs = meta.durationMs;
|
||||
clip._allocate(meta.totalFrame);
|
||||
job.rawFrames = List<Uint8List?>.filled(meta.totalFrame, null);
|
||||
if (!job.completer.isCompleted) {
|
||||
job.completer.complete(clip);
|
||||
}
|
||||
}
|
||||
|
||||
void _onFrame(RenderedFrame frame) {
|
||||
final job = _jobs[frame.jobId];
|
||||
if (job == null) return;
|
||||
final bytes = frame.data.materialize().asUint8List();
|
||||
if (frame.index < job.rawFrames.length) {
|
||||
job.rawFrames[frame.index] = bytes;
|
||||
}
|
||||
ui.decodeImageFromPixels(
|
||||
bytes,
|
||||
frame.px,
|
||||
frame.px,
|
||||
ui.PixelFormat.rgba8888,
|
||||
(image) {
|
||||
job.clip._setFrame(frame.index, image);
|
||||
_totalBytes += frame.px * frame.px * 4;
|
||||
_evictIfNeeded();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _onDone(RenderDone done) {
|
||||
final job = _jobs.remove(done.jobId);
|
||||
if (job == null) return;
|
||||
final clip = job.clip..complete = true;
|
||||
final raw = job.rawFrames;
|
||||
if (raw.length == clip.frameCount && !raw.contains(null)) {
|
||||
unawaited(RlottieDiskCache.instance.store(
|
||||
url: job.url,
|
||||
px: clip.px,
|
||||
frameCount: clip.frameCount,
|
||||
frameRate: clip.frameRate,
|
||||
durationMs: clip.durationMs,
|
||||
frames: raw.cast<Uint8List>(),
|
||||
));
|
||||
}
|
||||
job.rawFrames = const [];
|
||||
}
|
||||
|
||||
void _onError(RenderError error) {
|
||||
final job = _jobs.remove(error.jobId);
|
||||
if (job == null) return;
|
||||
logger.w('rlottie render failed (${job.url}): ${error.message}');
|
||||
if (!job.completer.isCompleted) job.completer.complete(null);
|
||||
if (job.clip.frameCount == 0) {
|
||||
_clips.remove(job.clip.key);
|
||||
job.clip.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
Future<RlottieClip?> acquire(String url, int px, {String? inlineJson}) async {
|
||||
if (!available) return null;
|
||||
final key = _keyFor(url, px);
|
||||
|
||||
final cached = _clips[key];
|
||||
if (cached != null) {
|
||||
cached.lastUsed = _tick();
|
||||
cached.active++;
|
||||
return cached;
|
||||
}
|
||||
final pending = _loading[key];
|
||||
if (pending != null) {
|
||||
final clip = await pending;
|
||||
if (clip != null) {
|
||||
clip.lastUsed = _tick();
|
||||
clip.active++;
|
||||
}
|
||||
return clip;
|
||||
}
|
||||
|
||||
final future = _load(url, px, key, inlineJson);
|
||||
_loading[key] = future;
|
||||
final clip = await future;
|
||||
_loading.remove(key);
|
||||
if (clip != null) {
|
||||
clip.lastUsed = _tick();
|
||||
clip.active++;
|
||||
}
|
||||
return clip;
|
||||
}
|
||||
|
||||
Future<RlottieClip?> _load(
|
||||
String url, int px, String key, String? inlineJson) async {
|
||||
if (inlineJson == null) {
|
||||
final disk = await RlottieDiskCache.instance.load(url, px);
|
||||
if (disk != null) {
|
||||
final clip = await _clipFromDisk(key, px, disk);
|
||||
_clips[key] = clip;
|
||||
return clip;
|
||||
}
|
||||
}
|
||||
|
||||
final json = inlineJson ?? await _fetchJson(url);
|
||||
if (json == null) return null;
|
||||
|
||||
final clip = RlottieClip(key: key, px: px);
|
||||
_clips[key] = clip;
|
||||
final jobId = _nextJobId++;
|
||||
final completer = Completer<RlottieClip?>();
|
||||
_jobs[jobId] = _Job(clip, url, completer);
|
||||
|
||||
final port = await _worker();
|
||||
port.send(RenderJob(
|
||||
jobId: jobId,
|
||||
json: json,
|
||||
cacheKey: url,
|
||||
px: px,
|
||||
libPath: debugLibraryPath,
|
||||
));
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
Future<RlottieClip> _clipFromDisk(String key, int px, DiskClip disk) async {
|
||||
final clip = RlottieClip(key: key, px: px)
|
||||
..frameCount = disk.frameCount
|
||||
..frameRate = disk.frameRate
|
||||
..durationMs = disk.durationMs
|
||||
..complete = true;
|
||||
clip._allocate(disk.frameCount);
|
||||
for (var i = 0; i < disk.frameCount; i++) {
|
||||
final image = await _decode(disk.frames[i], px);
|
||||
clip._setFrame(i, image);
|
||||
_totalBytes += px * px * 4;
|
||||
}
|
||||
_evictIfNeeded();
|
||||
return clip;
|
||||
}
|
||||
|
||||
Future<ui.Image> _decode(Uint8List rgba, int px) {
|
||||
final completer = Completer<ui.Image>();
|
||||
ui.decodeImageFromPixels(
|
||||
rgba, px, px, ui.PixelFormat.rgba8888, completer.complete);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
Future<String?> _fetchJson(String url) async {
|
||||
try {
|
||||
final file = await DefaultCacheManager().getSingleFile(url);
|
||||
return await file.readAsString();
|
||||
} catch (e) {
|
||||
logger.w('rlottie fetch failed ($url): $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> prewarm(String url, int px) async {
|
||||
if (!available) return;
|
||||
final clip = await acquire(url, px);
|
||||
if (clip != null) release(clip);
|
||||
}
|
||||
|
||||
void release(RlottieClip clip) {
|
||||
if (clip.active > 0) clip.active--;
|
||||
clip.lastUsed = _tick();
|
||||
_evictIfNeeded();
|
||||
}
|
||||
|
||||
void _evictIfNeeded() {
|
||||
if (_totalBytes <= _maxBytes) return;
|
||||
final candidates = _clips.values.where((c) => c.active <= 0).toList()
|
||||
..sort((a, b) => a.lastUsed.compareTo(b.lastUsed));
|
||||
for (final clip in candidates) {
|
||||
if (_totalBytes <= _maxBytes) break;
|
||||
_totalBytes -= clip.bytes;
|
||||
_clips.remove(clip.key);
|
||||
clip.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class RlottieClip {
|
||||
RlottieClip({this.px = 0});
|
||||
|
||||
final int px;
|
||||
int frameCount = 0;
|
||||
int durationMs = 1000;
|
||||
double frameRate = 60;
|
||||
final ValueNotifier<int> ready = ValueNotifier(0);
|
||||
|
||||
ui.Image? frameAt(int index) => null;
|
||||
}
|
||||
|
||||
class RlottieEngine {
|
||||
RlottieEngine._();
|
||||
static final RlottieEngine instance = RlottieEngine._();
|
||||
|
||||
static String? debugLibraryPath;
|
||||
|
||||
bool get available => false;
|
||||
|
||||
Future<RlottieClip?> acquire(String url, int px, {String? inlineJson}) async =>
|
||||
null;
|
||||
|
||||
Future<void> prewarm(String url, int px) async {}
|
||||
|
||||
void release(RlottieClip clip) {}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
|
||||
typedef _InitNative = Void Function();
|
||||
typedef _VoidFn = void Function();
|
||||
|
||||
typedef _FromDataNative = Pointer<Void> Function(
|
||||
Pointer<Utf8>, Pointer<Utf8>, Pointer<Utf8>);
|
||||
|
||||
typedef _SizeGetterNative = Size Function(Pointer<Void>);
|
||||
typedef _SizeGetter = int Function(Pointer<Void>);
|
||||
|
||||
typedef _DoubleGetterNative = Double Function(Pointer<Void>);
|
||||
typedef _DoubleGetter = double Function(Pointer<Void>);
|
||||
|
||||
typedef _RenderNative = Void Function(
|
||||
Pointer<Void>, Size, Pointer<Uint32>, Size, Size, Size);
|
||||
typedef _Render = void Function(
|
||||
Pointer<Void>, int, Pointer<Uint32>, int, int, int);
|
||||
|
||||
typedef _DestroyNative = Void Function(Pointer<Void>);
|
||||
typedef _Destroy = void Function(Pointer<Void>);
|
||||
|
||||
typedef _CacheSizeNative = Void Function(Size);
|
||||
typedef _CacheSize = void Function(int);
|
||||
|
||||
class RlottieBindings {
|
||||
RlottieBindings._(this._lib) {
|
||||
_init = _lib.lookupFunction<_InitNative, _VoidFn>('lottie_init');
|
||||
_shutdown = _lib.lookupFunction<_InitNative, _VoidFn>('lottie_shutdown');
|
||||
_fromData = _lib.lookupFunction<_FromDataNative, _FromDataNative>(
|
||||
'lottie_animation_from_data');
|
||||
_totalFrame = _lib.lookupFunction<_SizeGetterNative, _SizeGetter>(
|
||||
'lottie_animation_get_totalframe');
|
||||
_frameRate = _lib.lookupFunction<_DoubleGetterNative, _DoubleGetter>(
|
||||
'lottie_animation_get_framerate');
|
||||
_duration = _lib.lookupFunction<_DoubleGetterNative, _DoubleGetter>(
|
||||
'lottie_animation_get_duration');
|
||||
_render =
|
||||
_lib.lookupFunction<_RenderNative, _Render>('lottie_animation_render');
|
||||
_destroy = _lib
|
||||
.lookupFunction<_DestroyNative, _Destroy>('lottie_animation_destroy');
|
||||
_cacheSize = _lib.lookupFunction<_CacheSizeNative, _CacheSize>(
|
||||
'lottie_configure_model_cache_size');
|
||||
_init();
|
||||
}
|
||||
|
||||
final DynamicLibrary _lib;
|
||||
late final _VoidFn _init;
|
||||
late final _VoidFn _shutdown;
|
||||
late final _FromDataNative _fromData;
|
||||
late final _SizeGetter _totalFrame;
|
||||
late final _DoubleGetter _frameRate;
|
||||
late final _DoubleGetter _duration;
|
||||
late final _Render _render;
|
||||
late final _Destroy _destroy;
|
||||
late final _CacheSize _cacheSize;
|
||||
|
||||
static RlottieBindings? open({String? path}) {
|
||||
try {
|
||||
final lib = _openLibrary(path);
|
||||
return lib == null ? null : RlottieBindings._(lib);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static DynamicLibrary? _openLibrary(String? path) {
|
||||
if (path != null) return DynamicLibrary.open(path);
|
||||
if (Platform.isMacOS || Platform.isIOS) return DynamicLibrary.process();
|
||||
try {
|
||||
return DynamicLibrary.open(rlottieLibraryName);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Pointer<Void>? loadFromData(String data, String key) {
|
||||
final dataC = data.toNativeUtf8();
|
||||
final keyC = key.toNativeUtf8();
|
||||
final resC = ''.toNativeUtf8();
|
||||
try {
|
||||
final anim = _fromData(dataC, keyC, resC);
|
||||
return anim == nullptr ? null : anim;
|
||||
} finally {
|
||||
calloc.free(dataC);
|
||||
calloc.free(keyC);
|
||||
calloc.free(resC);
|
||||
}
|
||||
}
|
||||
|
||||
int totalFrame(Pointer<Void> anim) => _totalFrame(anim);
|
||||
double frameRate(Pointer<Void> anim) => _frameRate(anim);
|
||||
double duration(Pointer<Void> anim) => _duration(anim);
|
||||
|
||||
void render(Pointer<Void> anim, int frameNo, Pointer<Uint32> buffer, int px) {
|
||||
_render(anim, frameNo, buffer, px, px, px * 4);
|
||||
}
|
||||
|
||||
void destroy(Pointer<Void> anim) => _destroy(anim);
|
||||
|
||||
void configureModelCache(int bytes) => _cacheSize(bytes);
|
||||
|
||||
void shutdown() => _shutdown();
|
||||
}
|
||||
|
||||
String get rlottieLibraryName {
|
||||
if (Platform.isWindows) return 'rlottie.dll';
|
||||
return 'librlottie.so';
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import 'dart:ffi';
|
||||
import 'dart:isolate';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
|
||||
import 'rlottie_ffi.dart';
|
||||
|
||||
class RenderJob {
|
||||
const RenderJob({
|
||||
required this.jobId,
|
||||
required this.json,
|
||||
required this.cacheKey,
|
||||
required this.px,
|
||||
this.libPath,
|
||||
});
|
||||
|
||||
final int jobId;
|
||||
final String json;
|
||||
final String cacheKey;
|
||||
final int px;
|
||||
final String? libPath;
|
||||
}
|
||||
|
||||
class ClipMeta {
|
||||
const ClipMeta({
|
||||
required this.jobId,
|
||||
required this.totalFrame,
|
||||
required this.frameRate,
|
||||
required this.durationMs,
|
||||
});
|
||||
|
||||
final int jobId;
|
||||
final int totalFrame;
|
||||
final double frameRate;
|
||||
final int durationMs;
|
||||
}
|
||||
|
||||
class RenderedFrame {
|
||||
const RenderedFrame({
|
||||
required this.jobId,
|
||||
required this.index,
|
||||
required this.data,
|
||||
required this.px,
|
||||
});
|
||||
|
||||
final int jobId;
|
||||
final int index;
|
||||
final TransferableTypedData data;
|
||||
final int px;
|
||||
}
|
||||
|
||||
class RenderDone {
|
||||
const RenderDone(this.jobId);
|
||||
final int jobId;
|
||||
}
|
||||
|
||||
class RenderError {
|
||||
const RenderError(this.jobId, this.message);
|
||||
final int jobId;
|
||||
final String message;
|
||||
}
|
||||
|
||||
class CancelJob {
|
||||
const CancelJob(this.jobId);
|
||||
final int jobId;
|
||||
}
|
||||
|
||||
void rlottieWorkerMain(SendPort toMain) {
|
||||
final port = ReceivePort();
|
||||
toMain.send(port.sendPort);
|
||||
|
||||
final cancelled = <int>{};
|
||||
RlottieBindings? bindings;
|
||||
String? boundLibPath;
|
||||
|
||||
port.listen((message) {
|
||||
if (message is CancelJob) {
|
||||
cancelled.add(message.jobId);
|
||||
return;
|
||||
}
|
||||
if (message is! RenderJob) return;
|
||||
|
||||
final job = message;
|
||||
cancelled.remove(job.jobId);
|
||||
|
||||
if (bindings == null || boundLibPath != job.libPath) {
|
||||
bindings = RlottieBindings.open(path: job.libPath);
|
||||
boundLibPath = job.libPath;
|
||||
}
|
||||
final rl = bindings;
|
||||
if (rl == null) {
|
||||
toMain.send(RenderError(job.jobId, 'rlottie unavailable'));
|
||||
return;
|
||||
}
|
||||
|
||||
final anim = rl.loadFromData(job.json, job.cacheKey);
|
||||
if (anim == null) {
|
||||
toMain.send(RenderError(job.jobId, 'parse failed'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final total = rl.totalFrame(anim);
|
||||
final fps = rl.frameRate(anim);
|
||||
final durationMs = fps <= 0 ? 1000 : (total / fps * 1000).round();
|
||||
toMain.send(ClipMeta(
|
||||
jobId: job.jobId,
|
||||
totalFrame: total,
|
||||
frameRate: fps,
|
||||
durationMs: durationMs,
|
||||
));
|
||||
|
||||
final px = job.px;
|
||||
final buffer = calloc<Uint32>(px * px);
|
||||
try {
|
||||
for (var i = 0; i < total; i++) {
|
||||
if (cancelled.contains(job.jobId)) break;
|
||||
rl.render(anim, i, buffer, px);
|
||||
final rgba = _bgraToRgba(buffer, px * px);
|
||||
toMain.send(RenderedFrame(
|
||||
jobId: job.jobId,
|
||||
index: i,
|
||||
data: TransferableTypedData.fromList([rgba]),
|
||||
px: px,
|
||||
));
|
||||
}
|
||||
} finally {
|
||||
calloc.free(buffer);
|
||||
}
|
||||
toMain.send(RenderDone(job.jobId));
|
||||
} catch (e) {
|
||||
toMain.send(RenderError(job.jobId, e.toString()));
|
||||
} finally {
|
||||
rl.destroy(anim);
|
||||
cancelled.remove(job.jobId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Uint8List _bgraToRgba(Pointer<Uint32> buffer, int pixels) {
|
||||
final src = buffer.asTypedList(pixels);
|
||||
final out = Uint8List(pixels * 4);
|
||||
for (var i = 0; i < pixels; i++) {
|
||||
final v = src[i];
|
||||
final o = i * 4;
|
||||
out[o] = (v >> 16) & 0xff;
|
||||
out[o + 1] = (v >> 8) & 0xff;
|
||||
out[o + 2] = v & 0xff;
|
||||
out[o + 3] = (v >> 24) & 0xff;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
Reference in New Issue
Block a user