feat: переделал lottie -> rlottie, с первым прогревом кадров в фоне, как в тг, чтоб лагов не было

This commit is contained in:
Jganenokk
2026-07-11 00:02:48 +07:00
parent aec536666f
commit 8ab74739b5
34 changed files with 1285 additions and 182 deletions
+2
View File
@@ -25,6 +25,8 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Setup Java
uses: actions/setup-java@v4
+2
View File
@@ -25,6 +25,8 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Setup Java
uses: actions/setup-java@v4
+2
View File
@@ -28,6 +28,8 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Setup Flutter
uses: subosito/flutter-action@v2
+2
View File
@@ -28,6 +28,8 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Setup Flutter
uses: subosito/flutter-action@v2
+2
View File
@@ -28,6 +28,8 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Setup Flutter
uses: subosito/flutter-action@v2
+2
View File
@@ -28,6 +28,8 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Setup Flutter
uses: subosito/flutter-action@v2
+2
View File
@@ -12,6 +12,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
submodules: recursive
- name: Setup Java
uses: actions/setup-java@v4
+8
View File
@@ -11,6 +11,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
submodules: recursive
- name: Setup Java
uses: actions/setup-java@v4
@@ -69,6 +71,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
submodules: recursive
- name: Set up Flutter
uses: subosito/flutter-action@v2
@@ -95,6 +99,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
submodules: recursive
- name: Set up Flutter
uses: subosito/flutter-action@v2
@@ -113,6 +119,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
submodules: recursive
- name: Set up Flutter
uses: subosito/flutter-action@v2
+10
View File
@@ -26,6 +26,8 @@ jobs:
flavor: [komet, oneme]
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Setup Java
uses: actions/setup-java@v4
@@ -101,6 +103,8 @@ jobs:
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
@@ -127,6 +131,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
@@ -156,6 +162,8 @@ jobs:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
@@ -184,6 +192,8 @@ jobs:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
+12
View File
@@ -21,6 +21,8 @@ jobs:
flavor: [komet, oneme]
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Setup Java
uses: actions/setup-java@v4
@@ -96,6 +98,8 @@ jobs:
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
@@ -122,6 +126,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
@@ -151,6 +157,8 @@ jobs:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
@@ -177,6 +185,8 @@ jobs:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
@@ -238,6 +248,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Download all artifacts
uses: actions/download-artifact@v4
+3
View File
@@ -0,0 +1,3 @@
[submodule "third_party/rlottie"]
path = third_party/rlottie
url = https://github.com/Samsung/rlottie.git
+12
View File
@@ -37,6 +37,18 @@ android {
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
externalNativeBuild {
cmake {
arguments += listOf("-DBUILD_SHARED_LIBS=ON")
}
}
}
externalNativeBuild {
cmake {
path = file("src/main/cpp/CMakeLists.txt")
}
}
flavorDimensions += "distribution"
+7
View File
@@ -0,0 +1,7 @@
cmake_minimum_required(VERSION 3.10)
project(komet_rlottie LANGUAGES C CXX ASM)
add_subdirectory(
"${CMAKE_CURRENT_SOURCE_DIR}/../../../../../third_party/rlottie_build"
"${CMAKE_BINARY_DIR}/rlottie")
+1
View File
@@ -62,6 +62,7 @@ flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
pod 'rlottie', :path => '../third_party'
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
+2
View File
@@ -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 (_) {}
}
}
+314
View File
@@ -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) {}
}
+112
View File
@@ -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';
}
+153
View File
@@ -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;
}
+18 -1
View File
@@ -30,6 +30,7 @@ import '../../../backend/modules/animoji.dart';
import '../../../models/animoji.dart';
import '../../../backend/modules/complaints.dart';
import '../../../core/calls/call_controller.dart';
import '../../../core/media/rlottie/rlottie.dart';
import '../calls/call_screen.dart';
import '../../../core/protocol/opcode_map.dart';
import '../../../core/protocol/packet.dart';
@@ -460,13 +461,29 @@ class _ChatScreenState extends State<ChatScreen>
bool get _selectionMode => _selectedIds.value.isNotEmpty;
void _prewarmQuickReactions() {
if (!mounted || !RlottieEngine.instance.available) return;
final dpr =
(MediaQuery.maybeOf(context)?.devicePixelRatio ?? 2.0).clamp(1.0, 2.0);
final px = ((44.0 * dpr).clamp(96.0, 512.0) / 32).ceil() * 32;
for (final a in animojiModule.quickAnimojis) {
final url = a.lottieUrl;
if (url != null && url.isNotEmpty) {
unawaited(RlottieEngine.instance.prewarm(url, px));
}
}
}
@override
void initState() {
super.initState();
_chatController.chatId = widget.chatId;
_chatController.isMounted = () => mounted;
unawaited(PushService.clearChatNotification(widget.chatId));
unawaited(animojiModule.ensureLoaded().catchError((_) {}));
unawaited(animojiModule
.ensureLoaded()
.then((_) => _prewarmQuickReactions())
.catchError((_) {}));
WidgetsBinding.instance.addObserver(this);
chats.chatsChanged.addListener(_onChatsBump);
_messageController.addListener(_onTextChanged);
+65 -177
View File
@@ -6,6 +6,8 @@ import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:lottie/lottie.dart';
import '../../core/media/rlottie/rlottie.dart';
class LottieLoadGovernor {
LottieLoadGovernor._() {
_budgetMs = _resolveBudgetMs();
@@ -43,158 +45,6 @@ class LottieLoadGovernor {
}
}
class _LottieFrames {
final LottieDrawable drawable;
final int frameCount;
final Duration duration;
final int pxSize;
final List<ui.Image?> _images;
ui.Image? _lastImage;
int bytes = 0;
int lastUsed = 0;
int active = 0;
_LottieFrames({
required this.drawable,
required this.frameCount,
required this.duration,
required this.pxSize,
}) : _images = List<ui.Image?>.filled(frameCount, null);
ui.Image frameAt(int index) {
final existing = _images[index];
if (existing != null) {
_lastImage = existing;
return existing;
}
final last = _lastImage;
if (last != null && LottieLoadGovernor.instance.throttled.value) {
return last;
}
final progress = frameCount <= 1 ? 0.0 : index / (frameCount - 1);
final recorder = ui.PictureRecorder();
final canvas = Canvas(recorder);
drawable.setProgress(progress);
drawable.draw(
canvas,
Rect.fromLTWH(0, 0, pxSize.toDouble(), pxSize.toDouble()),
fit: BoxFit.contain,
);
final picture = recorder.endRecording();
final image = picture.toImageSync(pxSize, pxSize);
picture.dispose();
_images[index] = image;
_lastImage = image;
final added = pxSize * pxSize * 4;
bytes += added;
_LottieFrameCache.instance._onBytesAdded(added);
return image;
}
void dispose() {
for (final image in _images) {
image?.dispose();
}
_images.fillRange(0, _images.length, null);
_lastImage = null;
bytes = 0;
}
}
class _LottieFrameCache {
_LottieFrameCache._();
static final _LottieFrameCache instance = _LottieFrameCache._();
static const int _maxBytes = 384 * 1024 * 1024;
static const double _fps = 30;
final Map<String, _LottieFrames> _entries = {};
final Map<String, Future<_LottieFrames?>> _loading = {};
int _totalBytes = 0;
int _clock = 0;
int _tick() => ++_clock;
Future<_LottieFrames?> acquire(String url, int pxSize) async {
final key = '$url@$pxSize';
final cached = _entries[key];
if (cached != null) {
cached.lastUsed = _tick();
cached.active++;
return cached;
}
final pending = _loading[key];
if (pending != null) {
final entry = await pending;
if (entry != null) {
entry.lastUsed = _tick();
entry.active++;
}
return entry;
}
final future = _load(url, pxSize, key);
_loading[key] = future;
final entry = await future;
_loading.remove(key);
if (entry != null) {
entry.lastUsed = _tick();
entry.active++;
}
return entry;
}
void release(_LottieFrames frames) {
if (frames.active > 0) frames.active--;
frames.lastUsed = _tick();
_evictIfNeeded();
}
Future<_LottieFrames?> _load(String url, int pxSize, String key) async {
try {
final composition = await NetworkLottie(
url,
backgroundLoading: true,
).load();
final durationMs = composition.duration.inMilliseconds;
var frameCount = (durationMs / 1000 * _fps).round();
frameCount = frameCount.clamp(1, 120);
final entry = _LottieFrames(
drawable: LottieDrawable(composition),
frameCount: frameCount,
duration: durationMs <= 0
? const Duration(seconds: 1)
: composition.duration,
pxSize: pxSize,
);
_entries[key] = entry;
return entry;
} catch (_) {
return null;
}
}
void _onBytesAdded(int bytes) {
_totalBytes += bytes;
_evictIfNeeded();
}
void _evictIfNeeded() {
if (_totalBytes <= _maxBytes) return;
final candidates =
_entries.entries.where((e) => e.value.active <= 0).toList()
..sort((a, b) => a.value.lastUsed.compareTo(b.value.lastUsed));
for (final candidate in candidates) {
if (_totalBytes <= _maxBytes) break;
_totalBytes -= candidate.value.bytes;
candidate.value.dispose();
_entries.remove(candidate.key);
}
}
}
class LottieScrollScope extends InheritedWidget {
final ValueListenable<bool> isScrolling;
@@ -233,9 +83,12 @@ class LottiePlayer extends StatefulWidget {
class _LottiePlayerState extends State<LottiePlayer>
with SingleTickerProviderStateMixin {
static const int _leadFrames = 6;
final ValueNotifier<int> _frameIndex = ValueNotifier(0);
late final Ticker _ticker;
_LottieFrames? _frames;
late final bool _native;
RlottieClip? _clip;
ValueListenable<bool>? _scrollState;
int? _px;
bool _started = false;
@@ -248,6 +101,7 @@ class _LottiePlayerState extends State<LottiePlayer>
@override
void initState() {
super.initState();
_native = RlottieEngine.instance.available;
_ticker = createTicker(_onTick);
LottieLoadGovernor.instance.throttled.addListener(_onGateChanged);
}
@@ -268,9 +122,7 @@ class _LottiePlayerState extends State<LottiePlayer>
super.didUpdateWidget(oldWidget);
if (oldWidget.lottieUrl != widget.lottieUrl) {
_ticker.stop();
final previous = _frames;
if (previous != null) _LottieFrameCache.instance.release(previous);
_frames = null;
_releaseClip();
_started = false;
_showedFrames = false;
}
@@ -281,21 +133,29 @@ class _LottiePlayerState extends State<LottiePlayer>
LottieLoadGovernor.instance.throttled.removeListener(_onGateChanged);
_scrollState?.removeListener(_onGateChanged);
_ticker.dispose();
final frames = _frames;
if (frames != null) _LottieFrameCache.instance.release(frames);
_releaseClip();
_frameIndex.dispose();
super.dispose();
}
void _releaseClip() {
final clip = _clip;
if (clip != null) {
clip.ready.removeListener(_onReady);
RlottieEngine.instance.release(clip);
_clip = null;
}
}
void _onTick(Duration elapsed) {
final frames = _frames;
if (frames == null || frames.frameCount <= 1) return;
final periodMs = frames.duration.inMilliseconds;
final clip = _clip;
if (clip == null || clip.frameCount <= 1) return;
final periodMs = clip.durationMs;
if (periodMs <= 0) return;
final t = (elapsed.inMilliseconds % periodMs) / periodMs;
final index = (t * (frames.frameCount - 1)).round().clamp(
final index = (t * (clip.frameCount - 1)).round().clamp(
0,
frames.frameCount - 1,
clip.frameCount - 1,
);
if (index != _frameIndex.value) _frameIndex.value = index;
}
@@ -306,16 +166,29 @@ class _LottiePlayerState extends State<LottiePlayer>
if (_ticker.isActive) _ticker.stop();
return;
}
final frames = _frames;
if (frames != null) {
if (!_ticker.isActive && frames.frameCount > 1) _ticker.start();
final clip = _clip;
if (clip != null) {
_maybeStartTicker(clip);
} else if (_canLoad && !_started) {
_startLoad();
}
}
void _onReady() {
if (!mounted) return;
final clip = _clip;
if (clip != null) _maybeStartTicker(clip);
if (mounted) setState(() {});
}
void _maybeStartTicker(RlottieClip clip) {
if (_isScrolling || clip.frameCount <= 1) return;
final lead = clip.frameCount < _leadFrames ? clip.frameCount : _leadFrames;
if (clip.ready.value >= lead && !_ticker.isActive) _ticker.start();
}
void _ensure(double box) {
if (_frames != null) return;
if (_clip != null) return;
final dpr = MediaQuery.devicePixelRatioOf(context);
final raw = (box * dpr.clamp(1.0, 2.0)).clamp(96.0, 512.0);
_px = (raw / 32).ceil() * 32;
@@ -327,19 +200,21 @@ class _LottiePlayerState extends State<LottiePlayer>
final px = _px;
if (_started || px == null) return;
_started = true;
_LottieFrameCache.instance.acquire(widget.lottieUrl, px).then((frames) {
if (frames == null) return;
RlottieEngine.instance.acquire(widget.lottieUrl, px).then((clip) {
if (clip == null) return;
if (!mounted) {
_LottieFrameCache.instance.release(frames);
RlottieEngine.instance.release(clip);
return;
}
setState(() => _frames = frames);
if (!_isScrolling && frames.frameCount > 1) _ticker.start();
clip.ready.addListener(_onReady);
setState(() => _clip = clip);
_maybeStartTicker(clip);
});
}
@override
Widget build(BuildContext context) {
if (!_native) return _nativeFallback();
return LayoutBuilder(
builder: (context, constraints) {
final box =
@@ -348,15 +223,17 @@ class _LottiePlayerState extends State<LottiePlayer>
? constraints.biggest.shortestSide
: 96.0);
_ensure(box);
final frames = _frames;
if (frames == null || (_isScrolling && !_showedFrames)) {
return _fallback(box);
final clip = _clip;
if (clip == null ||
clip.ready.value == 0 ||
(_isScrolling && !_showedFrames)) {
return _staticFallback(box);
}
_showedFrames = true;
return ValueListenableBuilder<int>(
valueListenable: _frameIndex,
builder: (_, index, _) => RawImage(
image: frames.frameAt(index),
image: clip.frameAt(index),
width: box,
height: box,
fit: BoxFit.contain,
@@ -366,7 +243,18 @@ class _LottiePlayerState extends State<LottiePlayer>
);
}
Widget _fallback(double box) {
Widget _nativeFallback() {
return Lottie.network(
widget.lottieUrl,
width: widget.size,
height: widget.size,
fit: BoxFit.contain,
frameRate: FrameRate.max,
errorBuilder: (context, _, _) => _staticFallback(widget.size ?? 96.0),
);
}
Widget _staticFallback(double box) {
final url = widget.fallbackUrl ?? '';
final blank = SizedBox(width: box, height: box);
if (url.isEmpty) return blank;
+7
View File
@@ -84,6 +84,9 @@ if(TARGET flutter_webrtc_plugin)
target_compile_options(flutter_webrtc_plugin PRIVATE -include cstdint)
endif()
set(RLOTTIE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/rlottie_build")
add_subdirectory("${RLOTTIE_DIR}" "${CMAKE_BINARY_DIR}/rlottie")
# === Installation ===
# By default, "installing" just makes a relocatable bundle in the build
@@ -116,6 +119,10 @@ foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
COMPONENT Runtime)
endforeach(bundled_library)
install(FILES "$<TARGET_FILE:rlottie>"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
# Copy the native assets provided by the build.dart from all packages.
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
+1
View File
@@ -29,6 +29,7 @@ flutter_macos_podfile_setup
target 'Runner' do
use_frameworks!
pod 'rlottie', :path => '../third_party'
flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
+4 -4
View File
@@ -274,7 +274,7 @@ packages:
source: hosted
version: "1.3.3"
ffi:
dependency: transitive
dependency: "direct main"
description:
name: ffi
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
@@ -359,7 +359,7 @@ packages:
source: sdk
version: "0.0.0"
flutter_cache_manager:
dependency: transitive
dependency: "direct main"
description:
name: flutter_cache_manager
sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
@@ -1062,7 +1062,7 @@ packages:
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
dependency: "direct dev"
description:
name: path_provider_platform_interface
sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda"
@@ -1102,7 +1102,7 @@ packages:
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
dependency: "direct dev"
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
+4
View File
@@ -40,6 +40,7 @@ dependencies:
libcompress: ^1.0.0
msgpack_dart: ^1.0.1
crypto: ^3.0.7
ffi: ^2.1.0
logger: ^2.6.2
device_info_plus: 12.3.0
flutter_timezone: ^5.0.1
@@ -59,6 +60,7 @@ dependencies:
package_info_plus: ^9.0.1
mobile_scanner: ^7.2.0
cached_network_image: ^3.4.1
flutter_cache_manager: ^3.4.1
lottie: ^3.3.1
path_provider: ^2.1.4
share_plus: ^10.1.4
@@ -93,6 +95,8 @@ dev_dependencies:
# rules and activating additional ones.
flutter_lints: ^6.0.0
flutter_launcher_icons: ^0.14.4
path_provider_platform_interface: ^2.1.0
plugin_platform_interface: ^2.1.0
flutter_launcher_icons:
image_path: "assets/komet_icon.png"
+60
View File
@@ -0,0 +1,60 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:komet/core/media/rlottie/rlottie_disk_cache.dart';
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
class _FakePathProvider extends PathProviderPlatform
with MockPlatformInterfaceMixin {
_FakePathProvider(this.dir);
final String dir;
@override
Future<String?> getApplicationSupportPath() async => dir;
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
test('disk cache round-trips rendered frames', () async {
final tmp = Directory.systemTemp.createTempSync('rlottie_cache_test');
PathProviderPlatform.instance = _FakePathProvider(tmp.path);
const px = 32;
const frameCount = 5;
final frames = List.generate(frameCount, (f) {
final buf = Uint8List(px * px * 4);
for (var i = 0; i < buf.length; i++) {
buf[i] = (f * 37 + i) & 0xff;
}
return buf;
});
const url = 'https://example.com/anim.json';
await RlottieDiskCache.instance.store(
url: url,
px: px,
frameCount: frameCount,
frameRate: 30,
durationMs: 166,
frames: frames,
);
final loaded = await RlottieDiskCache.instance.load(url, px);
expect(loaded, isNotNull);
expect(loaded!.px, px);
expect(loaded.frameCount, frameCount);
expect(loaded.frameRate, 30);
expect(loaded.durationMs, 166);
for (var f = 0; f < frameCount; f++) {
expect(loaded.frames[f], orderedEquals(frames[f]),
reason: 'frame $f bytes preserved');
}
expect(await RlottieDiskCache.instance.load('https://other/x.json', px),
isNull);
tmp.deleteSync(recursive: true);
});
}
+66
View File
@@ -0,0 +1,66 @@
import 'dart:io';
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:komet/core/media/rlottie/rlottie_engine.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
test('rlottie engine renders a real clip into ui.Image frames', () async {
final libPath = Platform.environment['RLOTTIE_LIB'];
if (libPath == null || !File(libPath).existsSync()) {
markTestSkipped('set RLOTTIE_LIB to librlottie.so to run');
return;
}
RlottieEngine.debugLibraryPath = libPath;
expect(RlottieEngine.instance.available, isTrue,
reason: 'library should open');
final json = File('assets/lottie/ic_settings.json').readAsStringSync();
const px = 256;
final clip = await RlottieEngine.instance
.acquire('poc://ic_settings', px, inlineJson: json);
expect(clip, isNotNull);
expect(clip!.frameCount, greaterThan(1));
expect(clip.durationMs, greaterThan(0));
final target = clip.frameCount;
final deadline = DateTime.now().add(const Duration(seconds: 20));
while (clip.ready.value < target && DateTime.now().isBefore(deadline)) {
await Future<void>.delayed(const Duration(milliseconds: 20));
}
expect(clip.ready.value, target, reason: 'all frames decoded');
final mid = clip.frameAt(target ~/ 2);
expect(mid, isNotNull);
expect(mid!.width, px);
expect(mid.height, px);
final data = await mid.toByteData(format: ui.ImageByteFormat.rawRgba);
final bytes = data!.buffer.asUint8List();
var opaque = 0;
var transparent = 0;
for (var i = 3; i < bytes.length; i += 4) {
if (bytes[i] == 0) {
transparent++;
} else if (bytes[i] > 250) {
opaque++;
}
}
expect(opaque, greaterThan(0), reason: 'gear pixels present');
expect(transparent, greaterThan(0), reason: 'transparent background present');
final png = await mid.toByteData(format: ui.ImageByteFormat.png);
final out = Platform.environment['RLOTTIE_PNG_OUT'];
if (out != null) {
File(out).writeAsBytesSync(png!.buffer.asUint8List());
debugPrint('wrote decoded frame PNG: $out');
}
RlottieEngine.instance.release(clip);
});
}
Vendored Submodule
+1
Submodule third_party/rlottie added at f487eff2f8
+50
View File
@@ -0,0 +1,50 @@
Pod::Spec.new do |s|
s.name = 'rlottie'
s.version = '0.2.0'
s.summary = 'Samsung rlottie native Lottie renderer for Komet.'
s.description = 'Compiles the Samsung/rlottie submodule sources into the app so the native animation engine can be reached via dart:ffi (DynamicLibrary.process()).'
s.homepage = 'https://github.com/Samsung/rlottie'
s.license = { :type => 'MIT', :file => 'rlottie/COPYING' }
s.author = { 'Samsung Electronics' => 'opensource@samsung.com' }
s.source = { :path => '.' }
s.ios.deployment_target = '13.0'
s.osx.deployment_target = '10.15'
s.requires_arc = false
s.source_files = [
'rlottie/inc/*.h',
'rlottie_build/apple/config.h',
'rlottie/src/lottie/*.{cpp,h}',
'rlottie/src/lottie/zip/*.{cpp,h}',
'rlottie/src/lottie/rapidjson/**/*.h',
'rlottie/src/vector/*.{cpp,h}',
'rlottie/src/vector/freetype/*.{cpp,h}',
'rlottie/src/vector/pixman/pixman-arm-neon-asm.h',
'rlottie/src/vector/stb/*.{cpp,h}',
'rlottie/src/binding/c/*.cpp',
]
s.exclude_files = ['rlottie/src/vector/pixman/*.S']
s.public_header_files = 'rlottie/inc/*.h'
s.pod_target_xcconfig = {
'CLANG_CXX_LANGUAGE_STANDARD' => 'c++14',
'CLANG_CXX_LIBRARY' => 'libc++',
'GCC_ENABLE_CPP_EXCEPTIONS' => 'NO',
'GCC_ENABLE_CPP_RTTI' => 'NO',
'CLANG_WARN_DOCUMENTATION_COMMENTS' => 'NO',
'GCC_WARN_INHIBIT_ALL_WARNINGS' => 'YES',
'DEFINES_MODULE' => 'YES',
'HEADER_SEARCH_PATHS' => [
'"${PODS_TARGET_SRCROOT}/rlottie/inc"',
'"${PODS_TARGET_SRCROOT}/rlottie_build/apple"',
'"${PODS_TARGET_SRCROOT}/rlottie/src/lottie"',
'"${PODS_TARGET_SRCROOT}/rlottie/src/lottie/zip"',
'"${PODS_TARGET_SRCROOT}/rlottie/src/lottie/rapidjson"',
'"${PODS_TARGET_SRCROOT}/rlottie/src/vector"',
'"${PODS_TARGET_SRCROOT}/rlottie/src/vector/freetype"',
'"${PODS_TARGET_SRCROOT}/rlottie/src/vector/pixman"',
'"${PODS_TARGET_SRCROOT}/rlottie/src/vector/stb"',
].join(' '),
}
end
+76
View File
@@ -0,0 +1,76 @@
cmake_minimum_required( VERSION 3.10 )
project( rlottie VERSION 0.2 LANGUAGES C CXX ASM )
set(RLOTTIE "${CMAKE_CURRENT_LIST_DIR}/../rlottie")
if (NOT EXISTS "${RLOTTIE}/src/CMakeLists.txt")
message(FATAL_ERROR
"rlottie submodule is missing. Run: git submodule update --init --recursive")
endif()
if (NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release)
endif()
if (NOT DEFINED BUILD_SHARED_LIBS)
set(BUILD_SHARED_LIBS ON)
endif()
add_library( rlottie )
set_target_properties( rlottie PROPERTIES DEFINE_SYMBOL RLOTTIE_BUILD )
set(LOTTIE_MODULE OFF)
set(LOTTIE_THREAD ON)
set(LOTTIE_CACHE ON)
set(LOTTIE_MODULE_PATH "")
configure_file(${RLOTTIE}/cmake/config.h.in config.h)
target_include_directories(rlottie
PUBLIC $<BUILD_INTERFACE:${RLOTTIE}/inc>
PRIVATE "${CMAKE_CURRENT_BINARY_DIR}"
)
if(MSVC)
target_compile_options(rlottie
PRIVATE
/std:c++14
/EHs-c-
/GR-
/W3
)
else()
target_compile_options(rlottie
PRIVATE
-std=c++14
-fno-exceptions
-fno-unwind-tables
-fno-asynchronous-unwind-tables
-fno-rtti
-Wall
-fvisibility=hidden
-Wnon-virtual-dtor
-Woverloaded-virtual
-Wno-unused-parameter
)
endif()
if (WIN32 AND NOT BUILD_SHARED_LIBS)
target_compile_definitions(rlottie PUBLIC -DRLOTTIE_BUILD=0)
endif()
set( CMAKE_THREAD_PREFER_PTHREAD TRUE )
find_package( Threads )
if(WIN32)
set( OSSPEC_LIBS Shlwapi.lib )
endif()
target_link_libraries(rlottie
PUBLIC
"${CMAKE_THREAD_LIBS_INIT}"
${OSSPEC_LIBS}
)
add_subdirectory(${RLOTTIE}/src "${CMAKE_CURRENT_BINARY_DIR}/rlottie_src")
+56
View File
@@ -0,0 +1,56 @@
# rlottie integration (Komet)
`third_party/rlottie` is a **git submodule** pinned to Samsung/rlottie
`f487eff2f8086b84ae1c7faa0418abec909e874b`. This directory (`rlottie_build/`)
holds Komet's build glue that lives *outside* the submodule (we can't commit into
upstream's tree).
After cloning or pulling, initialize the submodule:
```
git submodule update --init --recursive
```
CI does this via `submodules: recursive` on every `actions/checkout` step.
## What powers what
The native animated-reaction / animoji / sticker renderer. rlottie renders each
frame to a premultiplied BGRA buffer off the UI thread; the Dart side
(`lib/core/media/rlottie/`) uploads frames to `ui.Image`, caches them in RAM and
on disk, and plays them from cache — so the first playback is as smooth as later
loops. Web has no native path and falls back to the pure-Dart `lottie` player.
## Files here
- `CMakeLists.txt` — build wrapper for the CMake platforms (Linux/Windows/Android).
Builds a single self-contained `rlottie` library from the submodule sources with
`LOTTIE_MODULE OFF` (stb compiled in), `LOTTIE_THREAD ON`, `LOTTIE_CACHE ON`.
Bypasses upstream's top-level CMakeLists (which references example/test) and
drives `../rlottie/src` directly.
- `apple/config.h` — static replacement for the CMake-generated `config.h`, used
by the CocoaPods build (which does not run CMake).
- `../rlottie.podspec` — compiles the submodule sources into the app for iOS/macOS
(pod root is `third_party/`, so it can reference both the submodule and this glue).
## Build wiring
| Platform | How | Loaded via | Verified |
|----------|-----|-----------|----------|
| Linux | `linux/CMakeLists.txt``add_subdirectory(rlottie_build)`, bundled to `lib/` | `DynamicLibrary.open('librlottie.so')` | ✅ full build + bundled .so |
| Android | `android/app/build.gradle.kts` `externalNativeBuild``android/app/src/main/cpp/CMakeLists.txt` | `DynamicLibrary.open('librlottie.so')` | ✅ NDK r28c arm64 cross-compile |
| Windows | `windows/CMakeLists.txt``add_subdirectory(rlottie_build)`, `rlottie.dll` next to exe | `DynamicLibrary.open('rlottie.dll')` | ⚠️ needs MSVC to verify |
| macOS | `macos/Podfile` `pod 'rlottie', :path => '../third_party'` | `DynamicLibrary.process()` | ⚠️ needs Xcode to verify |
| iOS | `ios/Podfile` `pod 'rlottie', :path => '../third_party'` | `DynamicLibrary.process()` | ⚠️ needs Xcode to verify |
## Gotchas for the unverified platforms
- **iOS/macOS symbols:** with `use_frameworks!` the `lottie_animation_*` symbols
live in `rlottie.framework`. If `DynamicLibrary.process()` can't find them,
switch the loader in `lib/core/media/rlottie/rlottie_ffi.dart` to
`DynamicLibrary.open('rlottie.framework/rlottie')`.
- **Windows:** rlottie builds with `/EHs-c- /GR-` and links `Shlwapi.lib` (set in
`CMakeLists.txt`).
- **32-bit ARM:** the pixman NEON `.S` asm is not wired; armv7 uses the C path.
- **Bumping rlottie:** `cd third_party/rlottie && git checkout <newsha>`, rebuild,
then re-check `apple/config.h` and the podspec source globs still match upstream.
+7
View File
@@ -0,0 +1,7 @@
#ifndef KOMET_RLOTTIE_CONFIG_H
#define KOMET_RLOTTIE_CONFIG_H
#define LOTTIE_THREAD_SUPPORT
#define LOTTIE_CACHE_SUPPORT
#endif
+7
View File
@@ -62,6 +62,9 @@ add_subdirectory("runner")
# them to the application.
include(flutter/generated_plugins.cmake)
set(RLOTTIE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/rlottie_build")
add_subdirectory("${RLOTTIE_DIR}" "${CMAKE_BINARY_DIR}/rlottie")
# === Installation ===
# Support files are copied into place next to the executable, so that it can
@@ -99,6 +102,10 @@ install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/opus/opus.dll"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
install(FILES "$<TARGET_FILE:rlottie>"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
# Copy the native assets provided by the build.dart from all packages.
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/")
install(DIRECTORY "${NATIVE_ASSETS_DIR}"