diff --git a/android/src/main/cpp/FrameProcessorRuntimeManager.cpp b/android/src/main/cpp/FrameProcessorRuntimeManager.cpp index e12b1d8..668cb4a 100644 --- a/android/src/main/cpp/FrameProcessorRuntimeManager.cpp +++ b/android/src/main/cpp/FrameProcessorRuntimeManager.cpp @@ -102,7 +102,6 @@ void FrameProcessorRuntimeManager::logErrorToJS(const std::string& message) { }); } - // actual JSI installer void FrameProcessorRuntimeManager::installJSIBindings() { __android_log_write(ANDROID_LOG_INFO, TAG, "Installing JSI bindings..."); diff --git a/android/src/main/cpp/JImageProxy.cpp b/android/src/main/cpp/JImageProxy.cpp index 1224a42..9e8f32e 100644 --- a/android/src/main/cpp/JImageProxy.cpp +++ b/android/src/main/cpp/JImageProxy.cpp @@ -14,31 +14,38 @@ using namespace jni; int JImageProxy::getWidth() { static const auto getWidthMethod = getClass()->getMethod("getWidth"); - return getWidthMethod(javaClassLocal()); + return getWidthMethod(self()); } int JImageProxy::getHeight() { static const auto getWidthMethod = getClass()->getMethod("getHeight"); - return getWidthMethod(javaClassLocal()); + return getWidthMethod(self()); } bool JImageProxy::getIsValid() { - return true; + static const auto utilsClass = findClassStatic("com/mrousavy/camera/frameprocessor/ImageProxyUtils"); + static const auto isImageProxyValidMethod = utilsClass->getStaticMethod("isImageProxyValid"); + return isImageProxyValidMethod(utilsClass, self()); } int JImageProxy::getPlaneCount() { static const auto getPlanesMethod = getClass()->getMethod()>("getPlanes"); - auto planes = getPlanesMethod(javaClassLocal()); + auto planes = getPlanesMethod(self()); return planes->size(); } int JImageProxy::getBytesPerRow() { static const auto getPlanesMethod = getClass()->getMethod()>("getPlanes"); - auto planes = getPlanesMethod(javaClassLocal()); + auto planes = getPlanesMethod(self()); auto firstPlane = planes->getElement(0); static const auto getRowStrideMethod = findClassLocal("android/media/Image$PlaneProxy")->getMethod("getRowStride"); return getRowStrideMethod(firstPlane.get()); } +void JImageProxy::close() { + static const auto closeMethod = getClass()->getMethod("close"); + closeMethod(self()); +} + } // namespace vision diff --git a/android/src/main/cpp/JImageProxy.h b/android/src/main/cpp/JImageProxy.h index 711188f..960c7a2 100644 --- a/android/src/main/cpp/JImageProxy.h +++ b/android/src/main/cpp/JImageProxy.h @@ -18,6 +18,8 @@ struct JImageProxy : public facebook::jni::JavaClass { bool getIsValid(); int getPlaneCount(); int getBytesPerRow(); + void close(); + }; } // namespace vision diff --git a/android/src/main/cpp/JImageProxyHostObject.cpp b/android/src/main/cpp/JImageProxyHostObject.cpp index 280f407..2e7776f 100644 --- a/android/src/main/cpp/JImageProxyHostObject.cpp +++ b/android/src/main/cpp/JImageProxyHostObject.cpp @@ -13,11 +13,11 @@ std::vector JImageProxyHostObject::getPropertyNames(jsi::Runtim std::vector result; result.push_back(jsi::PropNameID::forUtf8(rt, std::string("toString"))); result.push_back(jsi::PropNameID::forUtf8(rt, std::string("isValid"))); - result.push_back(jsi::PropNameID::forUtf8(rt, std::string("isReady"))); result.push_back(jsi::PropNameID::forUtf8(rt, std::string("width"))); result.push_back(jsi::PropNameID::forUtf8(rt, std::string("height"))); result.push_back(jsi::PropNameID::forUtf8(rt, std::string("bytesPerRow"))); result.push_back(jsi::PropNameID::forUtf8(rt, std::string("planesCount"))); + result.push_back(jsi::PropNameID::forUtf8(rt, std::string("close"))); return result; } @@ -25,7 +25,10 @@ jsi::Value JImageProxyHostObject::get(jsi::Runtime& runtime, const jsi::PropName auto name = propNameId.utf8(runtime); if (name == "toString") { - auto toString = [this] (jsi::Runtime& runtime, const jsi::Value& thisValue, const jsi::Value* arguments, size_t count) -> jsi::Value { + auto toString = [this] (jsi::Runtime& runtime, const jsi::Value&, const jsi::Value*, size_t) -> jsi::Value { + if (!this->frame) { + return jsi::String::createFromUtf8(runtime, "[closed frame]"); + } auto width = this->frame->getWidth(); auto height = this->frame->getHeight(); auto str = std::to_string(width) + " x " + std::to_string(height) + " Frame"; @@ -33,32 +36,57 @@ jsi::Value JImageProxyHostObject::get(jsi::Runtime& runtime, const jsi::PropName }; return jsi::Function::createFromHostFunction(runtime, jsi::PropNameID::forUtf8(runtime, "toString"), 0, toString); } + if (name == "close") { + auto close = [this] (jsi::Runtime& runtime, const jsi::Value&, const jsi::Value*, size_t) -> jsi::Value { + if (!this->frame) { + throw jsi::JSError(runtime, "Trying to close an already closed frame! Did you call frame.close() twice?"); + } + this->close(); + return jsi::Value::undefined(); + }; + return jsi::Function::createFromHostFunction(runtime, jsi::PropNameID::forUtf8(runtime, "close"), 0, close); + } if (name == "isValid") { - return jsi::Value(this->frame->getIsValid()); - } - if (name == "isReady") { - return jsi::Value(this->frame->getIsValid()); + return jsi::Value(this->frame && this->frame->getIsValid()); } if (name == "width") { + this->assertIsFrameStrong(runtime, name); return jsi::Value(this->frame->getWidth()); } if (name == "height") { + this->assertIsFrameStrong(runtime, name); return jsi::Value(this->frame->getHeight()); } if (name == "bytesPerRow") { + this->assertIsFrameStrong(runtime, name); return jsi::Value(this->frame->getBytesPerRow()); } if (name == "planesCount") { + this->assertIsFrameStrong(runtime, name); return jsi::Value(this->frame->getPlaneCount()); } return jsi::Value::undefined(); } +void JImageProxyHostObject::assertIsFrameStrong(jsi::Runtime& runtime, const std::string& accessedPropName) { + if (!this->frame) { + auto message = "Cannot get `" + accessedPropName + "`, frame is already closed!"; + throw jsi::JSError(runtime, message.c_str()); + } +} + JImageProxyHostObject::~JImageProxyHostObject() { - __android_log_write(ANDROID_LOG_INFO, TAG, "Destroying JImageProxyHostObject..."); + this->close(); +} + +void JImageProxyHostObject::close() { + if (this->frame) { + this->frame->close(); + this->frame.release(); + } } } // namespace vision diff --git a/android/src/main/cpp/JImageProxyHostObject.h b/android/src/main/cpp/JImageProxyHostObject.h index 13f715a..f6fa0fb 100644 --- a/android/src/main/cpp/JImageProxyHostObject.h +++ b/android/src/main/cpp/JImageProxyHostObject.h @@ -22,14 +22,17 @@ class JSI_EXPORT JImageProxyHostObject : public jsi::HostObject { public: jsi::Value get(jsi::Runtime &, const jsi::PropNameID &name) override; - std::vector getPropertyNames(jsi::Runtime &rt) override; + void close(); + public: jni::local_ref frame; private: static auto constexpr TAG = "VisionCamera"; + + void assertIsFrameStrong(jsi::Runtime& runtime, const std::string& accessedPropName); }; } // namespace vision diff --git a/android/src/main/java/com/mrousavy/camera/frameprocessor/ImageProxyUtils.java b/android/src/main/java/com/mrousavy/camera/frameprocessor/ImageProxyUtils.java new file mode 100644 index 0000000..6606c89 --- /dev/null +++ b/android/src/main/java/com/mrousavy/camera/frameprocessor/ImageProxyUtils.java @@ -0,0 +1,24 @@ +package com.mrousavy.camera.frameprocessor; + +import android.annotation.SuppressLint; +import android.media.Image; +import androidx.camera.core.ImageProxy; +import com.facebook.proguard.annotations.DoNotStrip; + +public class ImageProxyUtils { + @SuppressLint("UnsafeOptInUsageError") + @DoNotStrip + public static boolean isImageProxyValid(ImageProxy imageProxy) { + try { + Image image = imageProxy.getImage(); + if (image == null) return false; + // will throw an exception if the image is already closed + imageProxy.getImage().getCropRect(); + // no exception thrown, image must still be valid. + return true; + } catch (Exception e) { + // exception thrown, image has already been closed. + return false; + } + } +} diff --git a/example/ios/VisionCameraExample.xcodeproj/project.pbxproj b/example/ios/VisionCameraExample.xcodeproj/project.pbxproj index a58758d..cf83259 100644 --- a/example/ios/VisionCameraExample.xcodeproj/project.pbxproj +++ b/example/ios/VisionCameraExample.xcodeproj/project.pbxproj @@ -10,34 +10,34 @@ 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; - 2D25D416E10790D96A54417B /* libPods-VisionCameraExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 0FC5D1C53D06B40B4CA981B8 /* libPods-VisionCameraExample.a */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; B8DB3BD5263DE8B7004C18D7 /* BuildFile in Sources */ = {isa = PBXBuildFile; }; B8DB3BDC263DEA31004C18D7 /* ExampleFrameProcessorPlugin.m in Sources */ = {isa = PBXBuildFile; fileRef = B8DB3BD8263DEA31004C18D7 /* ExampleFrameProcessorPlugin.m */; }; B8DB3BDD263DEA31004C18D7 /* ExamplePluginSwift.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8DB3BDA263DEA31004C18D7 /* ExamplePluginSwift.swift */; }; B8DB3BDE263DEA31004C18D7 /* ExamplePluginSwift.m in Sources */ = {isa = PBXBuildFile; fileRef = B8DB3BDB263DEA31004C18D7 /* ExamplePluginSwift.m */; }; B8F0E10825E0199F00586F16 /* File.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F0E10725E0199F00586F16 /* File.swift */; }; + C0B129659921D2EA967280B2 /* libPods-VisionCameraExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3CDCFE89C25C89320B98945E /* libPods-VisionCameraExample.a */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 008F07F21AC5B25A0029DE68 /* main.jsbundle */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = main.jsbundle; sourceTree = ""; }; - 0FC5D1C53D06B40B4CA981B8 /* libPods-VisionCameraExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-VisionCameraExample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07F961A680F5B00A75B9A /* VisionCameraExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VisionCameraExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = VisionCameraExample/AppDelegate.h; sourceTree = ""; }; 13B07FB01A68108700A75B9A /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = VisionCameraExample/AppDelegate.m; sourceTree = ""; }; 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = VisionCameraExample/Images.xcassets; sourceTree = ""; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = VisionCameraExample/Info.plist; sourceTree = ""; }; 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = VisionCameraExample/main.m; sourceTree = ""; }; - 3AE81CC8871244CDFFFE8AE3 /* Pods-VisionCameraExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-VisionCameraExample.release.xcconfig"; path = "Target Support Files/Pods-VisionCameraExample/Pods-VisionCameraExample.release.xcconfig"; sourceTree = ""; }; + 3CDCFE89C25C89320B98945E /* libPods-VisionCameraExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-VisionCameraExample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = VisionCameraExample/LaunchScreen.storyboard; sourceTree = ""; }; B8DB3BD8263DEA31004C18D7 /* ExampleFrameProcessorPlugin.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ExampleFrameProcessorPlugin.m; sourceTree = ""; }; B8DB3BDA263DEA31004C18D7 /* ExamplePluginSwift.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExamplePluginSwift.swift; sourceTree = ""; }; B8DB3BDB263DEA31004C18D7 /* ExamplePluginSwift.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ExamplePluginSwift.m; sourceTree = ""; }; B8F0E10625E0199F00586F16 /* VisionCameraExample-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "VisionCameraExample-Bridging-Header.h"; sourceTree = ""; }; B8F0E10725E0199F00586F16 /* File.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = File.swift; sourceTree = ""; }; - E9E709EFF68B90B41F828506 /* Pods-VisionCameraExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-VisionCameraExample.debug.xcconfig"; path = "Target Support Files/Pods-VisionCameraExample/Pods-VisionCameraExample.debug.xcconfig"; sourceTree = ""; }; + C1D342AD8210E7627A632602 /* Pods-VisionCameraExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-VisionCameraExample.debug.xcconfig"; path = "Target Support Files/Pods-VisionCameraExample/Pods-VisionCameraExample.debug.xcconfig"; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; ED2971642150620600B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS12.0.sdk/System/Library/Frameworks/JavaScriptCore.framework; sourceTree = DEVELOPER_DIR; }; + F64CB73AE44CAC94E069BF68 /* Pods-VisionCameraExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-VisionCameraExample.release.xcconfig"; path = "Target Support Files/Pods-VisionCameraExample/Pods-VisionCameraExample.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -45,7 +45,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 2D25D416E10790D96A54417B /* libPods-VisionCameraExample.a in Frameworks */, + C0B129659921D2EA967280B2 /* libPods-VisionCameraExample.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -74,7 +74,7 @@ children = ( ED297162215061F000B7C4FE /* JavaScriptCore.framework */, ED2971642150620600B7C4FE /* JavaScriptCore.framework */, - 0FC5D1C53D06B40B4CA981B8 /* libPods-VisionCameraExample.a */, + 3CDCFE89C25C89320B98945E /* libPods-VisionCameraExample.a */, ); name = Frameworks; sourceTree = ""; @@ -82,8 +82,8 @@ 6B9684456A2045ADE5A6E47E /* Pods */ = { isa = PBXGroup; children = ( - E9E709EFF68B90B41F828506 /* Pods-VisionCameraExample.debug.xcconfig */, - 3AE81CC8871244CDFFFE8AE3 /* Pods-VisionCameraExample.release.xcconfig */, + C1D342AD8210E7627A632602 /* Pods-VisionCameraExample.debug.xcconfig */, + F64CB73AE44CAC94E069BF68 /* Pods-VisionCameraExample.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -150,7 +150,7 @@ isa = PBXNativeTarget; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "VisionCameraExample" */; buildPhases = ( - 61061D8AA46EA92199E38783 /* [CP] Check Pods Manifest.lock */, + CF06330A830C3A8E3D0B576B /* [CP] Check Pods Manifest.lock */, B81F12712604CF8800B08949 /* SwiftFormat */, B81F12702604CF6600B08949 /* SwiftLint */, FD10A7F022414F080027D42C /* Start Packager */, @@ -158,7 +158,7 @@ 13B07F8C1A680F5B00A75B9A /* Frameworks */, 13B07F8E1A680F5B00A75B9A /* Resources */, 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, - 62D9B6272AA9D5720A929C8F /* [CP] Copy Pods Resources */, + 5E303E5293C2545D8F744D87 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -228,29 +228,7 @@ shellPath = /bin/sh; shellScript = "export NODE_BINARY=node\n../node_modules/react-native/scripts/react-native-xcode.sh"; }; - 61061D8AA46EA92199E38783 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-VisionCameraExample-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 62D9B6272AA9D5720A929C8F /* [CP] Copy Pods Resources */ = { + 5E303E5293C2545D8F744D87 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -336,6 +314,28 @@ shellPath = /bin/sh; shellScript = "if which swiftformat >/dev/null; then\n swiftformat .\nelse\n echo \"warning: SwiftFormat not installed, download from https://github.com/nicklockwood/SwiftFormat\"\nfi\n"; }; + CF06330A830C3A8E3D0B576B /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-VisionCameraExample-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; FD10A7F022414F080027D42C /* Start Packager */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -377,7 +377,7 @@ /* Begin XCBuildConfiguration section */ 13B07F941A680F5B00A75B9A /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = E9E709EFF68B90B41F828506 /* Pods-VisionCameraExample.debug.xcconfig */; + baseConfigurationReference = C1D342AD8210E7627A632602 /* Pods-VisionCameraExample.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; @@ -402,7 +402,7 @@ }; 13B07F951A680F5B00A75B9A /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 3AE81CC8871244CDFFFE8AE3 /* Pods-VisionCameraExample.release.xcconfig */; + baseConfigurationReference = F64CB73AE44CAC94E069BF68 /* Pods-VisionCameraExample.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; diff --git a/example/yarn.lock b/example/yarn.lock index bc1ddda..47c1a77 100644 --- a/example/yarn.lock +++ b/example/yarn.lock @@ -1308,9 +1308,9 @@ anymatch@^3.0.3: picomatch "^2.0.4" appdirsjs@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/appdirsjs/-/appdirsjs-1.2.4.tgz#3ab582acc9fdfaaa0c1f81b3a25422ad4d95f9d4" - integrity sha512-WO5StDORR6JF/xYnXk/Fm0yu+iULaV5ULKuUw0Tu+jbgiTlSquaWBCgbpnsHLMXldf+fM3Gxn5p7vjond7He6w== + version "1.2.5" + resolved "https://registry.yarnpkg.com/appdirsjs/-/appdirsjs-1.2.5.tgz#c9888c8a0a908014533d5176ec56f1d5a8fd3700" + integrity sha512-UyaAyzj+7XLoKhbXJi4zoAw8IDXCiLNCKfQEiuCsCCTkDmiG1vpCliQn/MoUvO3DZqCN1i6gOahokcFtNSIrVA== argparse@^1.0.7: version "1.0.10" @@ -2455,16 +2455,15 @@ fast-diff@^1.1.2: integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== fast-glob@^3.1.1: - version "3.2.5" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.5.tgz#7939af2a656de79a4f1901903ee8adcaa7cb9661" - integrity sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg== + version "3.2.6" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.6.tgz#434dd9529845176ea049acc9343e8282765c6e1a" + integrity sha512-GnLuqj/pvQ7pX8/L4J84nijv6sAnlwvSDpMkJi9i7nPmPxGtRPkBSStfvDW5l6nMdX9VWe+pkKWFTgD+vF2QSQ== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.0" + glob-parent "^5.1.2" merge2 "^1.3.0" - micromatch "^4.0.2" - picomatch "^2.2.1" + micromatch "^4.0.4" fast-json-stable-stringify@^2.0.0: version "2.1.0" @@ -2691,7 +2690,7 @@ get-value@^2.0.3, get-value@^2.0.6: resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= -glob-parent@^5.1.0, glob-parent@^5.1.2: +glob-parent@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== @@ -4062,7 +4061,7 @@ micromatch@^3.1.10, micromatch@^3.1.4: snapdragon "^0.8.1" to-regex "^3.0.2" -micromatch@^4.0.2: +micromatch@^4.0.2, micromatch@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== @@ -4486,7 +4485,7 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3: +picomatch@^2.0.4, picomatch@^2.2.3: version "2.3.0" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== diff --git a/ios/Frame Processor/FrameHostObject.h b/ios/Frame Processor/FrameHostObject.h index 60d4a13..700b0eb 100644 --- a/ios/Frame Processor/FrameHostObject.h +++ b/ios/Frame Processor/FrameHostObject.h @@ -22,8 +22,11 @@ public: public: jsi::Value get(jsi::Runtime&, const jsi::PropNameID& name) override; std::vector getPropertyNames(jsi::Runtime& rt) override; - void destroyBuffer(); - + void close(); + public: Frame* frame; + +private: + void assertIsFrameStrong(jsi::Runtime& runtime, const std::string& accessedPropName); }; diff --git a/ios/Frame Processor/FrameHostObject.mm b/ios/Frame Processor/FrameHostObject.mm index 15b2e6b..f7ebbe3 100644 --- a/ios/Frame Processor/FrameHostObject.mm +++ b/ios/Frame Processor/FrameHostObject.mm @@ -14,11 +14,11 @@ std::vector FrameHostObject::getPropertyNames(jsi::Runtime& rt) std::vector result; result.push_back(jsi::PropNameID::forUtf8(rt, std::string("toString"))); result.push_back(jsi::PropNameID::forUtf8(rt, std::string("isValid"))); - result.push_back(jsi::PropNameID::forUtf8(rt, std::string("isReady"))); result.push_back(jsi::PropNameID::forUtf8(rt, std::string("width"))); result.push_back(jsi::PropNameID::forUtf8(rt, std::string("height"))); result.push_back(jsi::PropNameID::forUtf8(rt, std::string("bytesPerRow"))); result.push_back(jsi::PropNameID::forUtf8(rt, std::string("planesCount"))); + result.push_back(jsi::PropNameID::forUtf8(rt, std::string("close"))); return result; } @@ -26,7 +26,10 @@ jsi::Value FrameHostObject::get(jsi::Runtime& runtime, const jsi::PropNameID& pr auto name = propName.utf8(runtime); if (name == "toString") { - auto toString = [this] (jsi::Runtime& runtime, const jsi::Value& thisValue, const jsi::Value* arguments, size_t count) -> jsi::Value { + auto toString = [this] (jsi::Runtime& runtime, const jsi::Value&, const jsi::Value*, size_t) -> jsi::Value { + if (this->frame == nil) { + return jsi::String::createFromUtf8(runtime, "[closed frame]"); + } auto imageBuffer = CMSampleBufferGetImageBuffer(frame.buffer); auto width = CVPixelBufferGetWidth(imageBuffer); auto height = CVPixelBufferGetHeight(imageBuffer); @@ -36,31 +39,41 @@ jsi::Value FrameHostObject::get(jsi::Runtime& runtime, const jsi::PropNameID& pr }; return jsi::Function::createFromHostFunction(runtime, jsi::PropNameID::forUtf8(runtime, "toString"), 0, toString); } + if (name == "close") { + auto close = [this] (jsi::Runtime& runtime, const jsi::Value&, const jsi::Value*, size_t) -> jsi::Value { + if (this->frame == nil) { + throw jsi::JSError(runtime, "Trying to close an already closed frame! Did you call frame.close() twice?"); + } + this->close(); + return jsi::Value::undefined(); + }; + return jsi::Function::createFromHostFunction(runtime, jsi::PropNameID::forUtf8(runtime, "close"), 0, close); + } if (name == "isValid") { auto isValid = frame != nil && CMSampleBufferIsValid(frame.buffer); return jsi::Value(isValid); } - if (name == "isReady") { - auto isReady = frame != nil && CMSampleBufferDataIsReady(frame.buffer); - return jsi::Value(isReady); - } if (name == "width") { + this->assertIsFrameStrong(runtime, name); auto imageBuffer = CMSampleBufferGetImageBuffer(frame.buffer); auto width = CVPixelBufferGetWidth(imageBuffer); return jsi::Value((double) width); } if (name == "height") { + this->assertIsFrameStrong(runtime, name); auto imageBuffer = CMSampleBufferGetImageBuffer(frame.buffer); auto height = CVPixelBufferGetHeight(imageBuffer); return jsi::Value((double) height); } if (name == "bytesPerRow") { + this->assertIsFrameStrong(runtime, name); auto imageBuffer = CMSampleBufferGetImageBuffer(frame.buffer); auto bytesPerRow = CVPixelBufferGetPlaneCount(imageBuffer); return jsi::Value((double) bytesPerRow); } if (name == "planesCount") { + this->assertIsFrameStrong(runtime, name); auto imageBuffer = CMSampleBufferGetImageBuffer(frame.buffer); auto planesCount = CVPixelBufferGetPlaneCount(imageBuffer); return jsi::Value((double) planesCount); @@ -69,11 +82,21 @@ jsi::Value FrameHostObject::get(jsi::Runtime& runtime, const jsi::PropNameID& pr return jsi::Value::undefined(); } -FrameHostObject::~FrameHostObject() { - destroyBuffer(); +void FrameHostObject::assertIsFrameStrong(jsi::Runtime &runtime, const std::string &accessedPropName) { + if (frame == nil) { + auto message = "Cannot get `" + accessedPropName + "`, frame is already closed!"; + throw jsi::JSError(runtime, message.c_str()); + } } -void FrameHostObject::destroyBuffer() { - // ARC will hopefully delete it lol - this->frame = nil; +FrameHostObject::~FrameHostObject() { + close(); +} + +void FrameHostObject::close() { + if (frame != nil) { + CMSampleBufferInvalidate(frame.buffer); + // ARC will hopefully delete it lol + this->frame = nil; + } } diff --git a/ios/Frame Processor/FrameProcessorUtils.mm b/ios/Frame Processor/FrameProcessorUtils.mm index 6fe7c81..9f5c1ae 100644 --- a/ios/Frame Processor/FrameProcessorUtils.mm +++ b/ios/Frame Processor/FrameProcessorUtils.mm @@ -28,7 +28,6 @@ FrameProcessorCallback convertJSIFunctionToFrameProcessorCallback(jsi::Runtime & cb.call(runtime, jsi::Object::createFromHostObject(runtime, frameHostObject)); } catch (jsi::JSError& jsError) { auto message = jsError.getMessage(); - RCTBridge* bridge = [RCTBridge currentBridge]; if (bridge != nil) { bridge.jsCallInvoker->invokeAsync([bridge, message]() { @@ -44,6 +43,6 @@ FrameProcessorCallback convertJSIFunctionToFrameProcessorCallback(jsi::Runtime & // 1. we are sure we don't need it anymore, the frame processor worklet has finished executing. // 2. we don't know when the JS runtime garbage collects this object, it might be holding it for a few more frames // which then blocks the camera queue from pushing new frames (memory limit) - frameHostObject->destroyBuffer(); + frameHostObject->close(); }; } diff --git a/src/Frame.ts b/src/Frame.ts index 87cd3f4..bb747e1 100644 --- a/src/Frame.ts +++ b/src/Frame.ts @@ -3,13 +3,9 @@ */ export interface Frame { /** - * Whether the underlying buffer is still valid or not. The buffer will be released after the frame processor returns. + * Whether the underlying buffer is still valid or not. The buffer will be released after the frame processor returns, or `close()` is called. */ isValid: boolean; - /** - * Whether the underlying buffer is marked as "ready" or not. - */ - isReady: boolean; /** * Returns the width of the frame, in pixels. */ @@ -35,4 +31,19 @@ export interface Frame { * ``` */ toString(): string; + /** + * Closes and disposes the Frame. + * Only close frames that you have created yourself, e.g. by copying the frame you receive in a frame processor. + * + * @example + * ```ts + * const frameProcessor = useFrameProcessor((frame) => { + * const smallerCopy = resize(frame, 480, 270) + * // run AI ... + * smallerCopy.close() + * // don't close `frame`! + * }) + * ``` + */ + close(): void; } diff --git a/yarn.lock b/yarn.lock index 0705d73..bee9aba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1838,9 +1838,9 @@ anymatch@^3.0.3: picomatch "^2.0.4" appdirsjs@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/appdirsjs/-/appdirsjs-1.2.4.tgz#3ab582acc9fdfaaa0c1f81b3a25422ad4d95f9d4" - integrity sha512-WO5StDORR6JF/xYnXk/Fm0yu+iULaV5ULKuUw0Tu+jbgiTlSquaWBCgbpnsHLMXldf+fM3Gxn5p7vjond7He6w== + version "1.2.5" + resolved "https://registry.yarnpkg.com/appdirsjs/-/appdirsjs-1.2.5.tgz#c9888c8a0a908014533d5176ec56f1d5a8fd3700" + integrity sha512-UyaAyzj+7XLoKhbXJi4zoAw8IDXCiLNCKfQEiuCsCCTkDmiG1vpCliQn/MoUvO3DZqCN1i6gOahokcFtNSIrVA== argparse@^1.0.7: version "1.0.10" @@ -3495,16 +3495,15 @@ fast-diff@^1.1.2: integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== fast-glob@^3.1.1: - version "3.2.5" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.5.tgz#7939af2a656de79a4f1901903ee8adcaa7cb9661" - integrity sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg== + version "3.2.6" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.6.tgz#434dd9529845176ea049acc9343e8282765c6e1a" + integrity sha512-GnLuqj/pvQ7pX8/L4J84nijv6sAnlwvSDpMkJi9i7nPmPxGtRPkBSStfvDW5l6nMdX9VWe+pkKWFTgD+vF2QSQ== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.0" + glob-parent "^5.1.2" merge2 "^1.3.0" - micromatch "^4.0.2" - picomatch "^2.2.1" + micromatch "^4.0.4" fast-json-stable-stringify@^2.0.0: version "2.1.0" @@ -3859,7 +3858,7 @@ gitconfiglocal@^1.0.0: dependencies: ini "^1.3.2" -glob-parent@^5.1.0, glob-parent@^5.1.2: +glob-parent@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== @@ -5402,7 +5401,7 @@ micromatch@^3.1.10, micromatch@^3.1.4: snapdragon "^0.8.1" to-regex "^3.0.2" -micromatch@^4.0.2: +micromatch@^4.0.2, micromatch@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== @@ -6057,7 +6056,7 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3: +picomatch@^2.0.4, picomatch@^2.2.3: version "2.3.0" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== @@ -7581,9 +7580,9 @@ uglify-es@^3.1.9: source-map "~0.6.1" uglify-js@^3.1.4: - version "3.13.9" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.13.9.tgz#4d8d21dcd497f29cfd8e9378b9df123ad025999b" - integrity sha512-wZbyTQ1w6Y7fHdt8sJnHfSIuWeDgk6B5rCb4E/AM6QNNPbOMIZph21PW5dRB3h7Df0GszN+t7RuUH6sWK5bF0g== + version "3.13.10" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.13.10.tgz#a6bd0d28d38f592c3adb6b180ea6e07e1e540a8d" + integrity sha512-57H3ACYFXeo1IaZ1w02sfA71wI60MGco/IQFjOqK+WtKoprh7Go2/yvd2HPtoJILO2Or84ncLccI4xoHMTSbGg== ultron@1.0.x: version "1.0.2"