optimized decoding

This commit is contained in:
mertalev
2026-01-09 17:04:55 -05:00
parent 4ec0d6df9a
commit 89f9c136ac
23 changed files with 818 additions and 260 deletions
@@ -2,15 +2,11 @@ import 'dart:async';
import 'dart:ffi';
import 'dart:ui' as ui;
import 'package:cronet_http/cronet_http.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:ffi/ffi.dart';
import 'package:http/http.dart' as http;
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
part 'local_image_request.dart';
part 'thumbhash_image_request.dart';
@@ -52,12 +48,14 @@ abstract class ImageRequest {
final int actualWidth;
final int actualHeight;
final int rowBytes;
final int actualSize;
final ui.ImmutableBuffer buffer;
try {
actualWidth = info['width']!;
actualHeight = info['height']!;
actualSize = actualWidth * actualHeight * 4;
rowBytes = info['rowBytes'] ?? actualWidth * 4;
actualSize = rowBytes * actualHeight;
buffer = await ImmutableBuffer.fromUint8List(pointer.asTypedList(actualSize));
} finally {
malloc.free(pointer);
@@ -72,6 +70,7 @@ abstract class ImageRequest {
buffer,
width: actualWidth,
height: actualHeight,
rowBytes: rowBytes,
pixelFormat: ui.PixelFormat.rgba8888,
);
final codec = await descriptor.instantiateCodec();
@@ -16,7 +16,7 @@ class LocalImageRequest extends ImageRequest {
return null;
}
final Map<String, int> info = await thumbnailApi.requestImage(
final Map<String, int> info = await localImageApi.requestImage(
localId,
requestId: requestId,
width: width,
@@ -30,6 +30,6 @@ class LocalImageRequest extends ImageRequest {
@override
Future<void> _onCancelled() {
return thumbnailApi.cancelImageRequest(requestId);
return localImageApi.cancelRequest(requestId);
}
}
@@ -1,16 +1,8 @@
part of 'image_request.dart';
class RemoteImageRequest extends ImageRequest {
static final _client = const NetworkRepository().getHttpClient(
'thumbnails',
diskCapacity: kThumbnailDiskCacheSize,
memoryCapacity: 0,
maxConnections: 16,
cacheMode: CacheMode.disk,
);
final String uri;
final Map<String, String> headers;
final abortTrigger = Completer<void>();
RemoteImageRequest({required this.uri, required this.headers});
@@ -20,106 +12,14 @@ class RemoteImageRequest extends ImageRequest {
return null;
}
try {
final buffer = await _downloadImage();
if (buffer == null) {
return null;
}
final Map<String, int> info = await remoteImageApi.requestImage(uri, headers: headers, requestId: requestId);
return await _decodeBuffer(buffer, decode, scale);
} catch (e) {
if (_isCancelled) {
return null;
}
rethrow;
}
}
Future<ImmutableBuffer?> _downloadImage() async {
if (_isCancelled) {
return null;
}
final req = http.AbortableRequest('GET', Uri.parse(uri), abortTrigger: abortTrigger.future);
req.headers.addAll(headers);
final res = await _client.send(req);
if (_isCancelled) {
_onCancelled();
return null;
}
if (res.statusCode != 200) {
throw Exception('Failed to download $uri: ${res.statusCode}');
}
final stream = res.stream.map((chunk) {
if (_isCancelled) {
throw StateError('Cancelled request');
}
return chunk;
});
try {
final Uint8List bytes = await _downloadBytes(stream, res.contentLength ?? -1);
if (_isCancelled) {
return null;
}
return await ImmutableBuffer.fromUint8List(bytes);
} catch (e) {
if (_isCancelled) {
return null;
}
rethrow;
}
}
Future<Uint8List> _downloadBytes(Stream<List<int>> stream, int length) async {
final Uint8List bytes;
int offset = 0;
if (length > 0) {
// Known content length - use pre-allocated buffer
bytes = Uint8List(length);
await stream.listen((chunk) {
bytes.setAll(offset, chunk);
offset += chunk.length;
}, cancelOnError: true).asFuture();
} else {
// Unknown content length - collect chunks dynamically
final chunks = <List<int>>[];
int totalLength = 0;
await stream.listen((chunk) {
chunks.add(chunk);
totalLength += chunk.length;
}, cancelOnError: true).asFuture();
bytes = Uint8List(totalLength);
for (final chunk in chunks) {
bytes.setAll(offset, chunk);
offset += chunk.length;
}
}
return bytes;
}
Future<ImageInfo?> _decodeBuffer(ImmutableBuffer buffer, ImageDecoderCallback decode, scale) async {
if (_isCancelled) {
buffer.dispose();
return null;
}
final codec = await decode(buffer);
if (_isCancelled) {
buffer.dispose();
codec.dispose();
return null;
}
final frame = await codec.getNextFrame();
return ImageInfo(image: frame.image, scale: scale);
final frame = await _fromPlatformImage(info);
return frame == null ? null : ImageInfo(image: frame.image, scale: scale);
}
@override
void _onCancelled() {
abortTrigger.complete();
Future<void> _onCancelled() {
return localImageApi.cancelRequest(requestId);
}
}
@@ -11,7 +11,7 @@ class ThumbhashImageRequest extends ImageRequest {
return null;
}
final Map<String, int> info = await thumbnailApi.getThumbhash(thumbhash);
final Map<String, int> info = await localImageApi.getThumbhash(thumbhash);
final frame = await _fromPlatformImage(info);
return frame == null ? null : ImageInfo(image: frame.image, scale: scale);
}