mirror of
https://github.com/immich-app/immich.git
synced 2026-03-27 04:11:15 +03:00
memory optimization
This commit is contained in:
@@ -8,3 +8,5 @@ project(native_buffer LANGUAGES C)
|
|||||||
add_library(native_buffer SHARED
|
add_library(native_buffer SHARED
|
||||||
src/main/cpp/native_buffer.c
|
src/main/cpp/native_buffer.c
|
||||||
)
|
)
|
||||||
|
|
||||||
|
target_link_libraries(native_buffer jnigraphics)
|
||||||
|
|||||||
@@ -1,40 +1,39 @@
|
|||||||
#include <jni.h>
|
#include <jni.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
#include <android/bitmap.h>
|
||||||
|
|
||||||
JNIEXPORT jlong JNICALL
|
JNIEXPORT jlong JNICALL
|
||||||
Java_app_alextran_immich_images_ThumbnailsImpl_00024Companion_allocateNative(
|
Java_app_alextran_immich_images_LocalImagesImpl_allocateNative(
|
||||||
JNIEnv *env, jclass clazz, jint size) {
|
|
||||||
void *ptr = malloc(size);
|
|
||||||
return (jlong) ptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
JNIEXPORT jlong JNICALL
|
|
||||||
Java_app_alextran_immich_images_ThumbnailsImpl_allocateNative(
|
|
||||||
JNIEnv *env, jclass clazz, jint size) {
|
JNIEnv *env, jclass clazz, jint size) {
|
||||||
void *ptr = malloc(size);
|
void *ptr = malloc(size);
|
||||||
return (jlong) ptr;
|
return (jlong) ptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
JNIEXPORT void JNICALL
|
JNIEXPORT void JNICALL
|
||||||
Java_app_alextran_immich_images_ThumbnailsImpl_00024Companion_freeNative(
|
Java_app_alextran_immich_images_LocalImagesImpl_freeNative(
|
||||||
JNIEnv *env, jclass clazz, jlong address) {
|
JNIEnv *env, jclass clazz, jlong address) {
|
||||||
free((void *) address);
|
free((void *) address);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jobject JNICALL
|
||||||
|
Java_app_alextran_immich_images_LocalImagesImpl_wrapAsBuffer(
|
||||||
|
JNIEnv *env, jclass clazz, jlong address, jint capacity) {
|
||||||
|
return (*env)->NewDirectByteBuffer(env, (void *) address, capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jlong JNICALL
|
||||||
|
Java_app_alextran_immich_images_RemoteImagesImpl_lockBitmapPixels(
|
||||||
|
JNIEnv *env, jclass clazz, jobject bitmap) {
|
||||||
|
void *pixels = NULL;
|
||||||
|
int result = AndroidBitmap_lockPixels(env, bitmap, &pixels);
|
||||||
|
if (result != ANDROID_BITMAP_RESULT_SUCCESS) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return (jlong) pixels;
|
||||||
|
}
|
||||||
|
|
||||||
JNIEXPORT void JNICALL
|
JNIEXPORT void JNICALL
|
||||||
Java_app_alextran_immich_images_ThumbnailsImpl_freeNative(
|
Java_app_alextran_immich_images_RemoteImagesImpl_unlockBitmapPixels(
|
||||||
JNIEnv *env, jclass clazz, jlong address) {
|
JNIEnv *env, jclass clazz, jobject bitmap) {
|
||||||
free((void *) address);
|
AndroidBitmap_unlockPixels(env, bitmap);
|
||||||
}
|
|
||||||
|
|
||||||
JNIEXPORT jobject JNICALL
|
|
||||||
Java_app_alextran_immich_images_ThumbnailsImpl_00024Companion_wrapAsBuffer(
|
|
||||||
JNIEnv *env, jclass clazz, jlong address, jint capacity) {
|
|
||||||
return (*env)->NewDirectByteBuffer(env, (void *) address, capacity);
|
|
||||||
}
|
|
||||||
|
|
||||||
JNIEXPORT jobject JNICALL
|
|
||||||
Java_app_alextran_immich_images_ThumbnailsImpl_wrapAsBuffer(
|
|
||||||
JNIEnv *env, jclass clazz, jlong address, jint capacity) {
|
|
||||||
return (*env)->NewDirectByteBuffer(env, (void *) address, capacity);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,6 @@ import android.provider.MediaStore.Images
|
|||||||
import android.provider.MediaStore.Video
|
import android.provider.MediaStore.Video
|
||||||
import android.util.Size
|
import android.util.Size
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import app.alextran.immich.images.LocalImagesImpl.Companion.allocateNative
|
|
||||||
import app.alextran.immich.images.LocalImagesImpl.Companion.freeNative
|
|
||||||
import app.alextran.immich.images.LocalImagesImpl.Companion.wrapAsBuffer
|
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import kotlin.math.*
|
import kotlin.math.*
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
@@ -47,9 +44,9 @@ inline fun ImageDecoder.Source.decodeBitmap(target: Size = Size(0, 0)): Bitmap {
|
|||||||
|
|
||||||
fun Bitmap.toNativeBuffer(): Map<String, Long> {
|
fun Bitmap.toNativeBuffer(): Map<String, Long> {
|
||||||
val size = width * height * 4
|
val size = width * height * 4
|
||||||
val pointer = allocateNative(size)
|
val pointer = LocalImagesImpl.allocateNative(size)
|
||||||
try {
|
try {
|
||||||
val buffer = wrapAsBuffer(pointer, size)
|
val buffer = LocalImagesImpl.wrapAsBuffer(pointer, size)
|
||||||
copyPixelsToBuffer(buffer)
|
copyPixelsToBuffer(buffer)
|
||||||
recycle()
|
recycle()
|
||||||
return mapOf(
|
return mapOf(
|
||||||
@@ -58,7 +55,7 @@ fun Bitmap.toNativeBuffer(): Map<String, Long> {
|
|||||||
"height" to height.toLong()
|
"height" to height.toLong()
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
freeNative(pointer)
|
LocalImagesImpl.freeNative(pointer)
|
||||||
recycle()
|
recycle()
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ private open class RemoteImagesPigeonCodec : StandardMessageCodec() {
|
|||||||
interface RemoteImageApi {
|
interface RemoteImageApi {
|
||||||
fun requestImage(url: String, headers: Map<String, String>, requestId: Long, callback: (Result<Map<String, Long>>) -> Unit)
|
fun requestImage(url: String, headers: Map<String, String>, requestId: Long, callback: (Result<Map<String, Long>>) -> Unit)
|
||||||
fun cancelRequest(requestId: Long)
|
fun cancelRequest(requestId: Long)
|
||||||
|
fun releaseImage(requestId: Long)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/** The codec used by RemoteImageApi. */
|
/** The codec used by RemoteImageApi. */
|
||||||
@@ -99,6 +100,24 @@ interface RemoteImageApi {
|
|||||||
channel.setMessageHandler(null)
|
channel.setMessageHandler(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.RemoteImageApi.releaseImage$separatedMessageChannelSuffix", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { message, reply ->
|
||||||
|
val args = message as List<Any?>
|
||||||
|
val requestIdArg = args[0] as Long
|
||||||
|
val wrapped: List<Any?> = try {
|
||||||
|
api.releaseImage(requestIdArg)
|
||||||
|
listOf(null)
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
RemoteImagesPigeonUtils.wrapError(exception)
|
||||||
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import android.graphics.ColorSpace
|
|||||||
import android.graphics.ImageDecoder
|
import android.graphics.ImageDecoder
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.CancellationSignal
|
import android.os.CancellationSignal
|
||||||
import android.util.Size
|
|
||||||
import app.alextran.immich.core.SSLConfig
|
import app.alextran.immich.core.SSLConfig
|
||||||
import okhttp3.Call
|
import okhttp3.Call
|
||||||
import okhttp3.Callback
|
import okhttp3.Callback
|
||||||
@@ -31,9 +30,9 @@ data class RemoteRequest(
|
|||||||
|
|
||||||
class RemoteImagesImpl(context: Context) : RemoteImageApi {
|
class RemoteImagesImpl(context: Context) : RemoteImageApi {
|
||||||
private val requestMap = ConcurrentHashMap<Long, RemoteRequest>()
|
private val requestMap = ConcurrentHashMap<Long, RemoteRequest>()
|
||||||
|
private val lockedBitmaps = ConcurrentHashMap<Long, Bitmap>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
System.loadLibrary("native_buffer")
|
|
||||||
cacheDir = context.cacheDir
|
cacheDir = context.cacheDir
|
||||||
client = buildClient()
|
client = buildClient()
|
||||||
}
|
}
|
||||||
@@ -45,17 +44,23 @@ class RemoteImagesImpl(context: Context) : RemoteImageApi {
|
|||||||
private const val CACHE_SIZE_BYTES = 1024L * 1024 * 1024
|
private const val CACHE_SIZE_BYTES = 1024L * 1024 * 1024
|
||||||
|
|
||||||
val CANCELLED = Result.success<Map<String, Long>>(emptyMap())
|
val CANCELLED = Result.success<Map<String, Long>>(emptyMap())
|
||||||
private val decodePool = Executors.newFixedThreadPool(
|
private val decodePool =
|
||||||
(Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(2)
|
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() / 2 + 1)
|
||||||
)
|
|
||||||
|
|
||||||
private var cacheDir: File? = null
|
private var cacheDir: File? = null
|
||||||
private var client: OkHttpClient? = null
|
private var client: OkHttpClient? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
System.loadLibrary("native_buffer")
|
||||||
SSLConfig.addListener(::invalidateClient)
|
SSLConfig.addListener(::invalidateClient)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
external fun lockBitmapPixels(bitmap: Bitmap): Long
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
external fun unlockBitmapPixels(bitmap: Bitmap)
|
||||||
|
|
||||||
private fun invalidateClient() {
|
private fun invalidateClient() {
|
||||||
client?.let {
|
client?.let {
|
||||||
it.dispatcher.cancelAll()
|
it.dispatcher.cancelAll()
|
||||||
@@ -123,8 +128,20 @@ class RemoteImagesImpl(context: Context) : RemoteImageApi {
|
|||||||
signal.throwIfCanceled()
|
signal.throwIfCanceled()
|
||||||
val bitmap = decodeImage(bytes)
|
val bitmap = decodeImage(bytes)
|
||||||
signal.throwIfCanceled()
|
signal.throwIfCanceled()
|
||||||
val res = bitmap.toNativeBuffer()
|
|
||||||
callback(Result.success(res))
|
val pointer = lockBitmapPixels(bitmap)
|
||||||
|
if (pointer == 0L) {
|
||||||
|
bitmap.recycle()
|
||||||
|
return@execute callback(Result.failure(RuntimeException("Failed to lock bitmap pixels")))
|
||||||
|
}
|
||||||
|
|
||||||
|
lockedBitmaps[requestId] = bitmap
|
||||||
|
callback(Result.success(mapOf(
|
||||||
|
"pointer" to pointer,
|
||||||
|
"width" to bitmap.width.toLong(),
|
||||||
|
"height" to bitmap.height.toLong(),
|
||||||
|
"rowBytes" to bitmap.rowBytes.toLong()
|
||||||
|
)))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
val result = if (signal.isCanceled) CANCELLED else Result.failure(e)
|
val result = if (signal.isCanceled) CANCELLED else Result.failure(e)
|
||||||
callback(result)
|
callback(result)
|
||||||
@@ -138,8 +155,14 @@ class RemoteImagesImpl(context: Context) : RemoteImageApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun cancelRequest(requestId: Long) {
|
override fun cancelRequest(requestId: Long) {
|
||||||
val request = requestMap.remove(requestId) ?: return
|
requestMap.remove(requestId)?.cancellationSignal?.cancel()
|
||||||
request.cancellationSignal.cancel()
|
releaseImage(requestId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun releaseImage(requestId: Long) {
|
||||||
|
val bitmap = lockedBitmaps.remove(requestId) ?: return
|
||||||
|
unlockBitmapPixels(bitmap)
|
||||||
|
bitmap.recycle()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun decodeImage(bytes: ByteArray): Bitmap {
|
private fun decodeImage(bytes: ByteArray): Bitmap {
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ class RemoteImagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable
|
|||||||
protocol RemoteImageApi {
|
protocol RemoteImageApi {
|
||||||
func requestImage(url: String, headers: [String: String], requestId: Int64, completion: @escaping (Result<[String: Int64], Error>) -> Void)
|
func requestImage(url: String, headers: [String: String], requestId: Int64, completion: @escaping (Result<[String: Int64], Error>) -> Void)
|
||||||
func cancelRequest(requestId: Int64) throws
|
func cancelRequest(requestId: Int64) throws
|
||||||
|
func releaseImage(requestId: Int64) throws
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
||||||
@@ -114,5 +115,20 @@ class RemoteImageApiSetup {
|
|||||||
} else {
|
} else {
|
||||||
cancelRequestChannel.setMessageHandler(nil)
|
cancelRequestChannel.setMessageHandler(nil)
|
||||||
}
|
}
|
||||||
|
let releaseImageChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.RemoteImageApi.releaseImage\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
if let api = api {
|
||||||
|
releaseImageChannel.setMessageHandler { message, reply in
|
||||||
|
let args = message as! [Any?]
|
||||||
|
let requestIdArg = args[0] as! Int64
|
||||||
|
do {
|
||||||
|
try api.releaseImage(requestId: requestIdArg)
|
||||||
|
reply(wrapResult(nil))
|
||||||
|
} catch {
|
||||||
|
reply(wrapError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
releaseImageChannel.setMessageHandler(nil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import Accelerate
|
import CoreImage
|
||||||
|
import CoreVideo
|
||||||
import Flutter
|
import Flutter
|
||||||
import MobileCoreServices
|
import MobileCoreServices
|
||||||
import Photos
|
import Photos
|
||||||
@@ -20,7 +21,7 @@ class RemoteImageApiImpl: NSObject, RemoteImageApi {
|
|||||||
private static let delegate = RemoteImageApiDelegate()
|
private static let delegate = RemoteImageApiDelegate()
|
||||||
static let session = {
|
static let session = {
|
||||||
let config = URLSessionConfiguration.default
|
let config = URLSessionConfiguration.default
|
||||||
let thumbnailPath = FileManager.default.temporaryDirectory.appendingPathComponent("thumbnails", isDirectory: true)
|
let thumbnailPath = FileManager.default.temporaryDirectory.appendingPathComponent("thumbnails2", isDirectory: true)
|
||||||
try! FileManager.default.createDirectory(at: thumbnailPath, withIntermediateDirectories: true)
|
try! FileManager.default.createDirectory(at: thumbnailPath, withIntermediateDirectories: true)
|
||||||
config.urlCache = URLCache(
|
config.urlCache = URLCache(
|
||||||
memoryCapacity: 0,
|
memoryCapacity: 0,
|
||||||
@@ -47,25 +48,20 @@ class RemoteImageApiImpl: NSObject, RemoteImageApi {
|
|||||||
|
|
||||||
func cancelRequest(requestId: Int64) {
|
func cancelRequest(requestId: Int64) {
|
||||||
Self.delegate.cancel(requestId: requestId)
|
Self.delegate.cancel(requestId: requestId)
|
||||||
|
Self.delegate.releasePixelBuffer(requestId: requestId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func releaseImage(requestId: Int64) {
|
||||||
|
Self.delegate.releasePixelBuffer(requestId: requestId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RemoteImageApiDelegate: NSObject, URLSessionDataDelegate {
|
class RemoteImageApiDelegate: NSObject, URLSessionDataDelegate {
|
||||||
private static let requestQueue = DispatchQueue(label: "thumbnail.requests", qos: .userInitiated)
|
private static let requestQueue = DispatchQueue(label: "thumbnail.requests", qos: .userInitiated)
|
||||||
private static var rgbaFormat = vImage_CGImageFormat(
|
|
||||||
bitsPerComponent: 8,
|
|
||||||
bitsPerPixel: 32,
|
|
||||||
colorSpace: CGColorSpaceCreateDeviceRGB(),
|
|
||||||
bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue),
|
|
||||||
renderingIntent: .perceptual
|
|
||||||
)!
|
|
||||||
private static var requests = [Int64: RemoteImageRequest]()
|
private static var requests = [Int64: RemoteImageRequest]()
|
||||||
|
private static var lockedPixelBuffers = [Int64: CVPixelBuffer]()
|
||||||
private static let cancelledResult = Result<[String: Int64], any Error>.success([:])
|
private static let cancelledResult = Result<[String: Int64], any Error>.success([:])
|
||||||
private static let decodeOptions = [
|
private static let ciContext = CIContext(options: [.useSoftwareRenderer: false])
|
||||||
kCGImageSourceShouldCache: false,
|
|
||||||
kCGImageSourceShouldCacheImmediately: true,
|
|
||||||
kCGImageSourceCreateThumbnailWithTransform: true,
|
|
||||||
] as CFDictionary
|
|
||||||
|
|
||||||
func urlSession(
|
func urlSession(
|
||||||
_ session: URLSession, dataTask: URLSessionDataTask,
|
_ session: URLSession, dataTask: URLSessionDataTask,
|
||||||
@@ -108,11 +104,13 @@ class RemoteImageApiDelegate: NSObject, URLSessionDataDelegate {
|
|||||||
defer { remove(requestId: requestId) }
|
defer { remove(requestId: requestId) }
|
||||||
|
|
||||||
if let error = error {
|
if let error = error {
|
||||||
|
if request.isCancelled || (error as NSError).code == NSURLErrorCancelled {
|
||||||
|
return request.completion(Self.cancelledResult)
|
||||||
|
}
|
||||||
return request.completion(.failure(error))
|
return request.completion(.failure(error))
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let imageSource = CGImageSourceCreateWithData(data, nil),
|
guard let ciImage = CIImage(data: data as Data, options: [.applyOrientationProperty: true]) else {
|
||||||
let cgImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, Self.decodeOptions) else {
|
|
||||||
return request.completion(.failure(PigeonError(code: "", message: "Failed to decode image for request \(requestId)", details: nil)))
|
return request.completion(.failure(PigeonError(code: "", message: "Failed to decode image for request \(requestId)", details: nil)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,24 +118,52 @@ class RemoteImageApiDelegate: NSObject, URLSessionDataDelegate {
|
|||||||
return request.completion(Self.cancelledResult)
|
return request.completion(Self.cancelledResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
let extent = ciImage.extent
|
||||||
let buffer = try vImage_Buffer(cgImage: cgImage, format: Self.rgbaFormat)
|
let width = Int(extent.width)
|
||||||
|
let height = Int(extent.height)
|
||||||
|
|
||||||
if request.isCancelled {
|
guard width > 0 && height > 0 else {
|
||||||
buffer.free()
|
return request.completion(.failure(PigeonError(code: "", message: "Invalid image dimensions \(width)x\(height) for request \(requestId)", details: nil)))
|
||||||
return request.completion(Self.cancelledResult)
|
|
||||||
}
|
|
||||||
|
|
||||||
request.completion(
|
|
||||||
.success([
|
|
||||||
"pointer": Int64(Int(bitPattern: buffer.data)),
|
|
||||||
"width": Int64(buffer.width),
|
|
||||||
"height": Int64(buffer.height),
|
|
||||||
"rowBytes": Int64(buffer.rowBytes),
|
|
||||||
]))
|
|
||||||
} catch {
|
|
||||||
return request.completion(.failure(PigeonError(code: "", message: "Failed to convert image for request \(requestId): \(error)", details: nil)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var pixelBuffer: CVPixelBuffer?
|
||||||
|
let attrs: [String: Any] = [
|
||||||
|
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA,
|
||||||
|
kCVPixelBufferWidthKey as String: width,
|
||||||
|
kCVPixelBufferHeightKey as String: height,
|
||||||
|
kCVPixelBufferIOSurfacePropertiesKey as String: [:],
|
||||||
|
]
|
||||||
|
let status = CVPixelBufferCreate(kCFAllocatorDefault, width, height, kCVPixelFormatType_32BGRA, attrs as CFDictionary, &pixelBuffer)
|
||||||
|
|
||||||
|
guard status == kCVReturnSuccess, let pixelBuffer = pixelBuffer else {
|
||||||
|
return request.completion(.failure(PigeonError(code: "", message: "Failed to create pixel buffer for request \(requestId), status: \(status)", details: nil)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if request.isCancelled {
|
||||||
|
return request.completion(Self.cancelledResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
Self.ciContext.render(ciImage, to: pixelBuffer)
|
||||||
|
|
||||||
|
CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly)
|
||||||
|
guard let pointer = CVPixelBufferGetBaseAddress(pixelBuffer) else {
|
||||||
|
CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly)
|
||||||
|
return request.completion(.failure(PigeonError(code: "", message: "Failed to lock pixel buffer for request \(requestId)", details: nil)))
|
||||||
|
}
|
||||||
|
|
||||||
|
let rowBytes = CVPixelBufferGetBytesPerRow(pixelBuffer)
|
||||||
|
|
||||||
|
Self.requestQueue.sync {
|
||||||
|
Self.lockedPixelBuffers[requestId] = pixelBuffer
|
||||||
|
}
|
||||||
|
|
||||||
|
request.completion(
|
||||||
|
.success([
|
||||||
|
"pointer": Int64(Int(bitPattern: pointer)),
|
||||||
|
"width": Int64(width),
|
||||||
|
"height": Int64(height),
|
||||||
|
"rowBytes": Int64(rowBytes),
|
||||||
|
]))
|
||||||
}
|
}
|
||||||
|
|
||||||
func get(requestId: Int64) -> RemoteImageRequest? {
|
func get(requestId: Int64) -> RemoteImageRequest? {
|
||||||
@@ -157,4 +183,9 @@ class RemoteImageApiDelegate: NSObject, URLSessionDataDelegate {
|
|||||||
request.isCancelled = true
|
request.isCancelled = true
|
||||||
request.task?.cancel()
|
request.task?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func releasePixelBuffer(requestId: Int64) -> Void {
|
||||||
|
guard let pixelBuffer = (Self.requestQueue.sync { Self.lockedPixelBuffers.removeValue(forKey: requestId) }) else { return }
|
||||||
|
CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ abstract class ImageRequest {
|
|||||||
|
|
||||||
void _onCancelled();
|
void _onCancelled();
|
||||||
|
|
||||||
Future<ui.FrameInfo?> _fromPlatformImage(Map<String, int> info) async {
|
Future<ui.FrameInfo?> _fromPlatformImage(Map<String, int> info, ui.PixelFormat pixelFormat, bool shouldFree) async {
|
||||||
final address = info['pointer'];
|
final address = info['pointer'];
|
||||||
if (address == null) {
|
if (address == null) {
|
||||||
return null;
|
return null;
|
||||||
@@ -42,7 +42,9 @@ abstract class ImageRequest {
|
|||||||
|
|
||||||
final pointer = Pointer<Uint8>.fromAddress(address);
|
final pointer = Pointer<Uint8>.fromAddress(address);
|
||||||
if (_isCancelled) {
|
if (_isCancelled) {
|
||||||
malloc.free(pointer);
|
if (shouldFree) {
|
||||||
|
malloc.free(pointer);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +60,9 @@ abstract class ImageRequest {
|
|||||||
actualSize = rowBytes * actualHeight;
|
actualSize = rowBytes * actualHeight;
|
||||||
buffer = await ImmutableBuffer.fromUint8List(pointer.asTypedList(actualSize));
|
buffer = await ImmutableBuffer.fromUint8List(pointer.asTypedList(actualSize));
|
||||||
} finally {
|
} finally {
|
||||||
malloc.free(pointer);
|
if (shouldFree) {
|
||||||
|
malloc.free(pointer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_isCancelled) {
|
if (_isCancelled) {
|
||||||
@@ -71,7 +75,7 @@ abstract class ImageRequest {
|
|||||||
width: actualWidth,
|
width: actualWidth,
|
||||||
height: actualHeight,
|
height: actualHeight,
|
||||||
rowBytes: rowBytes,
|
rowBytes: rowBytes,
|
||||||
pixelFormat: ui.PixelFormat.rgba8888,
|
pixelFormat: pixelFormat,
|
||||||
);
|
);
|
||||||
final codec = await descriptor.instantiateCodec();
|
final codec = await descriptor.instantiateCodec();
|
||||||
if (_isCancelled) {
|
if (_isCancelled) {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class LocalImageRequest extends ImageRequest {
|
|||||||
isVideo: assetType == AssetType.video,
|
isVideo: assetType == AssetType.video,
|
||||||
);
|
);
|
||||||
|
|
||||||
final frame = await _fromPlatformImage(info);
|
final frame = await _fromPlatformImage(info, ui.PixelFormat.rgba8888, true);
|
||||||
return frame == null ? null : ImageInfo(image: frame.image, scale: scale);
|
return frame == null ? null : ImageInfo(image: frame.image, scale: scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,12 +14,16 @@ class RemoteImageRequest extends ImageRequest {
|
|||||||
|
|
||||||
final Map<String, int> info = await remoteImageApi.requestImage(uri, headers: headers, requestId: requestId);
|
final Map<String, int> info = await remoteImageApi.requestImage(uri, headers: headers, requestId: requestId);
|
||||||
|
|
||||||
final frame = await _fromPlatformImage(info);
|
try {
|
||||||
return frame == null ? null : ImageInfo(image: frame.image, scale: scale);
|
final frame = await _fromPlatformImage(info, ui.PixelFormat.bgra8888, false);
|
||||||
|
return frame == null ? null : ImageInfo(image: frame.image, scale: scale);
|
||||||
|
} finally {
|
||||||
|
unawaited(remoteImageApi.releaseImage(requestId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> _onCancelled() {
|
Future<void> _onCancelled() {
|
||||||
return localImageApi.cancelRequest(requestId);
|
return remoteImageApi.cancelRequest(requestId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class ThumbhashImageRequest extends ImageRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final Map<String, int> info = await localImageApi.getThumbhash(thumbhash);
|
final Map<String, int> info = await localImageApi.getThumbhash(thumbhash);
|
||||||
final frame = await _fromPlatformImage(info);
|
final frame = await _fromPlatformImage(info, ui.PixelFormat.rgba8888, true);
|
||||||
return frame == null ? null : ImageInfo(image: frame.image, scale: scale);
|
return frame == null ? null : ImageInfo(image: frame.image, scale: scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
23
mobile/lib/platform/remote_image_api.g.dart
generated
23
mobile/lib/platform/remote_image_api.g.dart
generated
@@ -103,4 +103,27 @@ class RemoteImageApi {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> releaseImage(int requestId) async {
|
||||||
|
final String pigeonVar_channelName =
|
||||||
|
'dev.flutter.pigeon.immich_mobile.RemoteImageApi.releaseImage$pigeonVar_messageChannelSuffix';
|
||||||
|
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
|
pigeonVar_channelName,
|
||||||
|
pigeonChannelCodec,
|
||||||
|
binaryMessenger: pigeonVar_binaryMessenger,
|
||||||
|
);
|
||||||
|
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[requestId]);
|
||||||
|
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||||
|
if (pigeonVar_replyList == null) {
|
||||||
|
throw _createConnectionError(pigeonVar_channelName);
|
||||||
|
} else if (pigeonVar_replyList.length > 1) {
|
||||||
|
throw PlatformException(
|
||||||
|
code: pigeonVar_replyList[0]! as String,
|
||||||
|
message: pigeonVar_replyList[1] as String?,
|
||||||
|
details: pigeonVar_replyList[2],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,4 +22,6 @@ abstract class RemoteImageApi {
|
|||||||
});
|
});
|
||||||
|
|
||||||
void cancelRequest(int requestId);
|
void cancelRequest(int requestId);
|
||||||
|
|
||||||
|
void releaseImage(int requestId);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user