From a0590dccb530c8203d0f70fba6c40467acf6ef2f Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Mon, 13 Feb 2023 15:22:45 +0100 Subject: [PATCH] feat: Replace Reanimated with RN Worklets (#1468) * Setup RN Worklets * Use RN Worklets on iOS * Fix console * Add `installFrameProcessorBindings()` function * Add `FrameProcessorPlugins` proxy (BREAKING CHANGE) * Clean up docs * Update FRAME_PROCESSORS.mdx * Use RN Worklets 0.2.5 * feat: Android build setup * Rewrite Android Frame Processor Part * Update CMakeLists.txt * fix: Add react-native-worklets Gradle dependency * Update Podfile.lock * fix build * gradle:7.4.1 * Init JSI Bindings in method on Android * Fix Folly flags * fix: Init `FrameProcessorRuntimeManager` later * fix: Wrap in `` * Refactor plugins * fix: Remove enableFrameProcessors * Install RN Worklets from current GH master * Update babel.config.js * Update CameraViewModule.kt * Update ImageProxyUtils.java * feat: Upgrade to Reanimated v3 * fix: Fix crash on Worklet init * Update RN Worklets to latest master * fix: Simplify FP Plugins Proxy --- .github/workflows/validate-cpp.yml | 2 +- VisionCamera.podspec | 1 + android/CMakeLists.txt | 106 +++------- android/build.gradle | 43 +++-- android/settings.gradle | 3 - .../main/cpp/FrameProcessorRuntimeManager.cpp | 165 ++++++++-------- .../main/cpp/FrameProcessorRuntimeManager.h | 20 +- android/src/main/cpp/MakeJSIRuntime.h | 30 --- .../src/main/cpp/VisionCameraScheduler.cpp | 12 +- android/src/main/cpp/VisionCameraScheduler.h | 22 ++- .../reanimated-headers/AndroidErrorHandler.h | 30 --- .../cpp/reanimated-headers/AndroidScheduler.h | 37 ---- .../java/com/mrousavy/camera/CameraView.kt | 4 +- .../com/mrousavy/camera/CameraViewModule.kt | 26 ++- .../frameprocessor/FrameProcessorPlugin.java | 2 +- .../FrameProcessorRuntimeManager.kt | 46 +++-- .../frameprocessor/ImageProxyUtils.java | 2 +- .../frameprocessor/VisionCameraScheduler.java | 4 +- docs/docs/guides/FRAME_PROCESSORS.mdx | 41 +--- .../FRAME_PROCESSORS_CREATE_OVERVIEW.mdx | 2 +- .../guides/FRAME_PROCESSOR_CREATE_FINAL.mdx | 30 +-- .../FRAME_PROCESSOR_CREATE_PLUGIN_ANDROID.mdx | 4 +- .../FRAME_PROCESSOR_CREATE_PLUGIN_IOS.mdx | 4 +- .../guides/FRAME_PROCESSOR_PLUGIN_LIST.mdx | 7 +- docs/docs/guides/SETUP.mdx | 2 +- docs/docs/guides/TODO.md | 2 +- .../camera/example/MainApplication.java | 7 - example/android/build.gradle | 2 +- example/babel.config.js | 8 +- example/ios/Podfile.lock | 15 +- example/package.json | 7 +- example/src/App.tsx | 37 ++-- example/src/frame-processors/ExamplePlugin.ts | 16 +- example/yarn.lock | 50 ++--- ios/CameraBridge.h | 6 - ios/CameraView+Orientation.swift | 6 +- ios/CameraViewManager.m | 2 + ios/CameraViewManager.swift | 22 +-- ios/Frame Processor/FrameProcessorPlugin.h | 7 +- .../FrameProcessorRuntimeManager.h | 8 +- .../FrameProcessorRuntimeManager.mm | 181 +++++++++--------- ios/Frame Processor/FrameProcessorUtils.h | 4 +- ios/Frame Processor/FrameProcessorUtils.mm | 15 +- ios/Frame Processor/VisionCameraScheduler.h | 38 ---- ios/Frame Processor/VisionCameraScheduler.mm | 39 ---- ios/React Utils/MakeJSIRuntime.h | 42 ---- ios/VisionCamera.xcodeproj/project.pbxproj | 2 - package.json | 4 +- scripts/cpplint.sh | 2 +- src/Camera.tsx | 8 + src/FrameProcessorPlugins.ts | 13 ++ src/globals.d.ts | 22 +-- src/hooks/useFrameProcessor.ts | 38 +--- src/index.ts | 1 + yarn.lock | 81 +++----- 55 files changed, 469 insertions(+), 861 deletions(-) delete mode 100644 android/src/main/cpp/MakeJSIRuntime.h delete mode 100644 android/src/main/cpp/reanimated-headers/AndroidErrorHandler.h delete mode 100644 android/src/main/cpp/reanimated-headers/AndroidScheduler.h delete mode 100644 ios/Frame Processor/VisionCameraScheduler.h delete mode 100644 ios/Frame Processor/VisionCameraScheduler.mm delete mode 100644 ios/React Utils/MakeJSIRuntime.h create mode 100644 src/FrameProcessorPlugins.ts diff --git a/.github/workflows/validate-cpp.yml b/.github/workflows/validate-cpp.yml index fd3a32f..b92ab23 100644 --- a/.github/workflows/validate-cpp.yml +++ b/.github/workflows/validate-cpp.yml @@ -24,7 +24,7 @@ jobs: with: github_token: ${{ secrets.github_token }} reporter: github-pr-review - flags: --linelength=230 --exclude "android/src/main/cpp/reanimated-headers" + flags: --linelength=230 targets: --recursive cpp android/src/main/cpp filter: "-legal/copyright\ ,-readability/todo\ diff --git a/VisionCamera.podspec b/VisionCamera.podspec index 1e468f8..3cde833 100644 --- a/VisionCamera.podspec +++ b/VisionCamera.podspec @@ -64,4 +64,5 @@ Pod::Spec.new do |s| s.dependency "React-callinvoker" s.dependency "React" s.dependency "React-Core" + s.dependency "react-native-worklets" end diff --git a/android/CMakeLists.txt b/android/CMakeLists.txt index e19bd9f..0db8aec 100644 --- a/android/CMakeLists.txt +++ b/android/CMakeLists.txt @@ -1,20 +1,23 @@ project(VisionCamera) -cmake_minimum_required(VERSION 3.4.1) +cmake_minimum_required(VERSION 3.9.0) -set (CMAKE_VERBOSE_MAKEFILE ON) -set (CMAKE_CXX_STANDARD 14) +set(CMAKE_VERBOSE_MAKEFILE ON) +set(PACKAGE_NAME "VisionCamera") +set(BUILD_DIR ${CMAKE_SOURCE_DIR}/build) +set(CMAKE_VERBOSE_MAKEFILE ON) +set(CMAKE_CXX_STANDARD 17) +# Folly include("${NODE_MODULES_DIR}/react-native/ReactAndroid/cmake-utils/folly-flags.cmake") add_compile_options(${folly_FLAGS}) - -set (PACKAGE_NAME "VisionCamera") -set (BUILD_DIR ${CMAKE_SOURCE_DIR}/build) -# Consume shared libraries and headers from prefabs -find_package(fbjni REQUIRED CONFIG) +# Third party libraries (Prefabs) find_package(ReactAndroid REQUIRED CONFIG) -# VisionCamera shared +find_package(fbjni REQUIRED CONFIG) +find_package(react-native-worklets REQUIRED CONFIG) +find_library(LOG_LIB log) +# Add react-native-vision-camera sources add_library( ${PACKAGE_NAME} SHARED @@ -29,87 +32,24 @@ add_library( src/main/cpp/java-bindings/JHashMap.cpp ) -# includes +# Header Search Paths (includes) target_include_directories( ${PACKAGE_NAME} PRIVATE - "${NODE_MODULES_DIR}/react-native/ReactAndroid/src/main/jni/react/turbomodule" + "src/main/cpp" "${NODE_MODULES_DIR}/react-native/ReactCommon" "${NODE_MODULES_DIR}/react-native/ReactCommon/callinvoker" - "${NODE_MODULES_DIR}/react-native/ReactCommon/jsi" - "${NODE_MODULES_DIR}/react-native/ReactCommon/react/renderer/graphics/platform/cxx" - "${NODE_MODULES_DIR}/react-native/ReactCommon/runtimeexecutor" - "${NODE_MODULES_DIR}/react-native/ReactCommon/yoga" - # --- Reanimated --- - # New - "${NODE_MODULES_DIR}/react-native-reanimated/Common/cpp/AnimatedSensor" - "${NODE_MODULES_DIR}/react-native-reanimated/Common/cpp/Tools" - "${NODE_MODULES_DIR}/react-native-reanimated/Common/cpp/SpecTools" - "${NODE_MODULES_DIR}/react-native-reanimated/Common/cpp/SharedItems" - "${NODE_MODULES_DIR}/react-native-reanimated/Common/cpp/Registries" - "${NODE_MODULES_DIR}/react-native-reanimated/Common/cpp/LayoutAnimations" - "${NODE_MODULES_DIR}/react-native-reanimated/Common/cpp/hidden_headers" - "src/main/cpp" + "${NODE_MODULES_DIR}/react-native/ReactAndroid/src/main/jni/react/turbomodule" # <-- CallInvokerHolder JNI wrapper ) - -# find libraries - -file (GLOB LIBRN_DIR "${BUILD_DIR}/react-native-0*/jni/${ANDROID_ABI}") - -if(${FOR_HERMES}) - string(APPEND CMAKE_CXX_FLAGS " -DFOR_HERMES=1") - - find_package(hermes-engine REQUIRED CONFIG) - - target_link_libraries( - ${PACKAGE_NAME} - "hermes-engine::libhermes" - ) - file (GLOB LIBREANIMATED_DIR "${BUILD_DIR}/react-native-reanimated-*-hermes.aar/jni/${ANDROID_ABI}") -else() - file (GLOB LIBJSC_DIR "${BUILD_DIR}/android-jsc*.aar/jni/${ANDROID_ABI}") - - set(JS_ENGINE_LIB ReactAndroid::jscexecutor) - target_link_libraries( - ${PACKAGE_NAME} - ${JS_ENGINE_LIB} - ) - - # Use Reanimated JSC - file (GLOB LIBREANIMATED_DIR "${BUILD_DIR}/react-native-reanimated-*-jsc.aar/jni/${ANDROID_ABI}") -endif() - +# Link everything together target_link_libraries( ${PACKAGE_NAME} - ReactAndroid::folly_runtime - ReactAndroid::glog - ReactAndroid::jsi - ReactAndroid::reactnativejni - fbjni::fbjni -) - -find_library( - REANIMATED_LIB - reanimated - PATHS ${LIBREANIMATED_DIR} - NO_CMAKE_FIND_ROOT_PATH -) - -find_library( - LOG_LIB - log -) - -# linking -message(WARNING "VisionCamera linking: FOR_HERMES=${FOR_HERMES}") -target_link_libraries( - ${PACKAGE_NAME} - ${LOG_LIB} - ${JSI_LIB} - ${REANIMATED_LIB} - ${REACT_NATIVE_JNI_LIB} - ${FBJNI_LIB} - ${FOLLY_LIB} - android + ${LOG_LIB} # <-- Logcat logger + android # <-- Android JNI core + ReactAndroid::jsi # <-- RN: JSI + ReactAndroid::reactnativejni # <-- RN: React Native JNI bindings + ReactAndroid::folly_runtime # <-- RN: For casting JSI <> Java objects + fbjni::fbjni # <-- fbjni + react-native-worklets::rnworklets # <-- RN Worklets ) diff --git a/android/build.gradle b/android/build.gradle index 866aef3..6be8473 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,3 +1,5 @@ +import java.nio.file.Paths + buildscript { def kotlin_version = rootProject.ext.has('kotlinVersion') ? rootProject.ext.get('kotlinVersion') : project.properties['VisionCamera_kotlinVersion'] @@ -16,7 +18,6 @@ buildscript { } } -def ENABLE_FRAME_PROCESSORS = false // TODO: fix def kotlin_version = rootProject.ext.has('kotlinVersion') ? rootProject.ext.get('kotlinVersion') : project.properties['VisionCamera_kotlinVersion'] def resolveBuildType() { @@ -49,6 +50,23 @@ def reactNativeArchitectures() { return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"] } +static def findNodeModules(baseDir) { + def basePath = baseDir.toPath().normalize() + // Node's module resolution algorithm searches up to the root directory, + // after which the base path will be null + while (basePath) { + def nodeModulesPath = Paths.get(basePath.toString(), "node_modules") + def reactNativePath = Paths.get(nodeModulesPath.toString(), "react-native") + if (nodeModulesPath.toFile().exists() && reactNativePath.toFile().exists()) { + return nodeModulesPath.toString() + } + basePath = basePath.getParent() + } + throw new GradleException("react-native-worklets: Failed to find node_modules/ path!") +} + +def nodeModules = findNodeModules(projectDir) + repositories { google() mavenCentral() @@ -77,13 +95,12 @@ android { versionName "1.0" buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() - if (ENABLE_FRAME_PROCESSORS) { - externalNativeBuild { - cmake { - cppFlags "-O2 -frtti -fexceptions -Wall -Wno-unused-variable -fstack-protector-all" - arguments "-DANDROID_STL=c++_shared" - abiFilters (*reactNativeArchitectures()) - } + externalNativeBuild { + cmake { + cppFlags "-O2 -frtti -fexceptions -Wall -Wno-unused-variable -fstack-protector-all" + arguments "-DANDROID_STL=c++_shared", + "-DNODE_MODULES_DIR=${nodeModules}" + abiFilters (*reactNativeArchitectures()) } } } @@ -93,11 +110,9 @@ android { targetCompatibility JavaVersion.VERSION_1_8 } - if (ENABLE_FRAME_PROCESSORS) { - externalNativeBuild { - cmake { - path "CMakeLists.txt" - } + externalNativeBuild { + cmake { + path "CMakeLists.txt" } } packagingOptions { @@ -128,6 +143,8 @@ dependencies { implementation "androidx.camera:camera-extensions:1.1.0-beta02" implementation "androidx.exifinterface:exifinterface:1.3.3" + + implementation project(":react-native-worklets") } // Resolves "LOCAL_SRC_FILES points to a missing file, Check that libfb.so exists or that its path is correct". diff --git a/android/settings.gradle b/android/settings.gradle index b02ca4a..56a6c3d 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,6 +1,3 @@ rootProject.name = 'VisionCamera' -include ':react-native-reanimated' -project(':react-native-reanimated').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-reanimated/android/') - include ':VisionCamera' diff --git a/android/src/main/cpp/FrameProcessorRuntimeManager.cpp b/android/src/main/cpp/FrameProcessorRuntimeManager.cpp index ebbf3ef..ebe4715 100644 --- a/android/src/main/cpp/FrameProcessorRuntimeManager.cpp +++ b/android/src/main/cpp/FrameProcessorRuntimeManager.cpp @@ -7,17 +7,11 @@ #include #include #include +#include -#include "RuntimeDecorator.h" -#include "RuntimeManager.h" -#include "reanimated-headers/AndroidScheduler.h" -#include "reanimated-headers/AndroidErrorHandler.h" - -#include "MakeJSIRuntime.h" #include "CameraView.h" #include "FrameHostObject.h" #include "JSIJNIConversion.h" -#include "VisionCameraScheduler.h" #include "java-bindings/JImageProxy.h" #include "java-bindings/JFrameProcessorPlugin.h" @@ -28,6 +22,28 @@ using TSelf = local_ref::jhybr using TJSCallInvokerHolder = jni::alias_ref; using TAndroidScheduler = jni::alias_ref; +FrameProcessorRuntimeManager::FrameProcessorRuntimeManager(jni::alias_ref jThis, + jsi::Runtime* jsRuntime, + std::shared_ptr jsCallInvoker, + std::shared_ptr scheduler) : + javaPart_(jni::make_global(jThis)), + _jsRuntime(jsRuntime) { + auto runOnJS = [jsCallInvoker](std::function&& f) { + // Run on React JS Runtime + jsCallInvoker->invokeAsync(std::move(f)); + }; + auto runOnWorklet = [scheduler](std::function&& f) { + // Run on Frame Processor Worklet Runtime + scheduler->dispatchAsync(std::move(f)); + }; + + _workletContext = std::make_shared("VisionCamera"); + _workletContext->initialize("VisionCamera", + jsRuntime, + runOnJS, + runOnWorklet); +} + // JNI binding void vision::FrameProcessorRuntimeManager::registerNatives() { registerHybrid({ @@ -35,8 +51,6 @@ void vision::FrameProcessorRuntimeManager::registerNatives() { FrameProcessorRuntimeManager::initHybrid), makeNativeMethod("installJSIBindings", FrameProcessorRuntimeManager::installJSIBindings), - makeNativeMethod("initializeRuntime", - FrameProcessorRuntimeManager::initializeRuntime), makeNativeMethod("registerPlugin", FrameProcessorRuntimeManager::registerPlugin), }); @@ -52,32 +66,11 @@ TSelf vision::FrameProcessorRuntimeManager::initHybrid( "Initializing FrameProcessorRuntimeManager..."); // cast from JNI hybrid objects to C++ instances - auto runtime = reinterpret_cast(jsRuntimePointer); + auto jsRuntime = reinterpret_cast(jsRuntimePointer); auto jsCallInvoker = jsCallInvokerHolder->cthis()->getCallInvoker(); auto scheduler = std::shared_ptr(androidScheduler->cthis()); - scheduler->setJSCallInvoker(jsCallInvoker); - return makeCxxInstance(jThis, runtime, jsCallInvoker, scheduler); -} - -void vision::FrameProcessorRuntimeManager::initializeRuntime() { - __android_log_write(ANDROID_LOG_INFO, TAG, - "Initializing Vision JS-Runtime..."); - - // create JSI runtime and decorate it - auto runtime = makeJSIRuntime(); - reanimated::RuntimeDecorator::decorateRuntime(*runtime, "FRAME_PROCESSOR"); - runtime->global().setProperty(*runtime, "_FRAME_PROCESSOR", - jsi::Value(true)); - - // create REA runtime manager - auto errorHandler = std::make_shared(scheduler_); - _runtimeManager = std::make_unique(std::move(runtime), - errorHandler, - scheduler_); - - __android_log_write(ANDROID_LOG_INFO, TAG, - "Initialized Vision JS-Runtime!"); + return makeCxxInstance(jThis, jsRuntime, jsCallInvoker, scheduler); } global_ref FrameProcessorRuntimeManager::findCameraViewById(int viewId) { @@ -87,21 +80,16 @@ global_ref FrameProcessorRuntimeManager::findCameraViewB } void FrameProcessorRuntimeManager::logErrorToJS(const std::string& message) { - if (!this->jsCallInvoker_) { + if (!_workletContext) { return; } - - this->jsCallInvoker_->invokeAsync([this, message]() { - if (this->runtime_ == nullptr) { - return; - } - - auto& runtime = *this->runtime_; - auto consoleError = runtime - .global() - .getPropertyAsObject(runtime, "console") - .getPropertyAsFunction(runtime, "error"); - consoleError.call(runtime, jsi::String::createFromUtf8(runtime, message)); + // Call console.error() on JS Thread + _workletContext->invokeOnJsThread([message](jsi::Runtime& runtime) { + auto consoleError = runtime + .global() + .getPropertyAsObject(runtime, "console") + .getPropertyAsFunction(runtime, "error"); + consoleError.call(runtime, jsi::String::createFromUtf8(runtime, message)); }); } @@ -111,42 +99,38 @@ void FrameProcessorRuntimeManager::setFrameProcessor(jsi::Runtime& runtime, __android_log_write(ANDROID_LOG_INFO, TAG, "Setting new Frame Processor..."); - if (!_runtimeManager || !_runtimeManager->runtime) { + if (!_workletContext) { throw jsi::JSError(runtime, - "setFrameProcessor(..): VisionCamera's RuntimeManager is not yet initialized!"); + "setFrameProcessor(..): VisionCamera's Worklet Context is not yet initialized!"); } // find camera view auto cameraView = findCameraViewById(viewTag); __android_log_write(ANDROID_LOG_INFO, TAG, "Found CameraView!"); - // convert jsi::Function to a ShareableValue (can be shared across runtimes) - __android_log_write(ANDROID_LOG_INFO, TAG, - "Adapting Shareable value from function (conversion to worklet)..."); - auto worklet = reanimated::ShareableValue::adapt(runtime, - frameProcessor, - _runtimeManager.get()); + // convert jsi::Function to a Worklet (can be shared across runtimes) + __android_log_write(ANDROID_LOG_INFO, TAG, "Creating Worklet..."); + auto worklet = std::make_shared(runtime, frameProcessor); + auto workletInvoker = std::make_shared(worklet); __android_log_write(ANDROID_LOG_INFO, TAG, "Successfully created worklet!"); - scheduler_->scheduleOnUI([=]() { - // cast worklet to a jsi::Function for the new runtime - auto& rt = *_runtimeManager->runtime; - auto function = std::make_shared(worklet->getValue(rt).asObject(rt).asFunction(rt)); - - // assign lambda to frame processor - cameraView->cthis()->setFrameProcessor([this, &rt, function](jni::alias_ref frame) { - try { - // create HostObject which holds the Frame (JImageProxy) - auto hostObject = std::make_shared(frame); - function->callWithThis(rt, *function, jsi::Object::createFromHostObject(rt, hostObject)); - } catch (jsi::JSError& jsError) { - auto message = "Frame Processor threw an error: " + jsError.getMessage(); - __android_log_write(ANDROID_LOG_ERROR, TAG, message.c_str()); - this->logErrorToJS(message); - } - }); - - __android_log_write(ANDROID_LOG_INFO, TAG, "Frame Processor set!"); + _workletContext->invokeOnWorkletThread([=](RNWorklet::JsiWorkletContext*, jsi::Runtime& rt) { + // Set Frame Processor as callable C++ lambda - this will then call the Worklet + cameraView->cthis()->setFrameProcessor([this, workletInvoker, &rt](jni::alias_ref frame) { + try { + // create HostObject which holds the Frame (JImageProxy) + auto frameHostObject = std::make_shared(frame); + auto argument = jsi::Object::createFromHostObject(rt, frameHostObject); + jsi::Value jsValue(std::move(argument)); + // Call the Worklet on the Worklet Runtime + workletInvoker->call(rt, jsi::Value::undefined(), &jsValue, 1); + } catch (jsi::JSError& jsError) { + // Worklet threw a JS Error, catch it and log it to JS. + auto message = "Frame Processor threw an error: " + jsError.getMessage(); + __android_log_write(ANDROID_LOG_ERROR, TAG, message.c_str()); + this->logErrorToJS(message); + } + }); }); } @@ -166,13 +150,13 @@ void FrameProcessorRuntimeManager::unsetFrameProcessor(int viewTag) { void FrameProcessorRuntimeManager::installJSIBindings() { __android_log_write(ANDROID_LOG_INFO, TAG, "Installing JSI bindings..."); - if (runtime_ == nullptr) { + if (_jsRuntime == nullptr) { __android_log_write(ANDROID_LOG_ERROR, TAG, "JS-Runtime was null, Frame Processor JSI bindings could not be installed!"); return; } - auto& jsiRuntime = *runtime_; + auto& jsiRuntime = *_jsRuntime; auto setFrameProcessor = [this](jsi::Runtime &runtime, const jsi::Value &thisValue, @@ -234,27 +218,26 @@ void FrameProcessorRuntimeManager::installJSIBindings() { } void FrameProcessorRuntimeManager::registerPlugin(alias_ref plugin) { - // _runtimeManager might never be null, but we can never be too sure. - if (!_runtimeManager || !_runtimeManager->runtime) { - throw std::runtime_error("Tried to register plugin before initializing JS runtime! Call `initializeRuntime()` first."); - } - - auto& runtime = *_runtimeManager->runtime; + auto& runtime = *_jsRuntime; // we need a strong reference on the plugin, make_global does that. auto pluginGlobal = make_global(plugin); - // name is always prefixed with two underscores (__) - auto name = "__" + pluginGlobal->getName(); + auto pluginName = pluginGlobal->getName(); - __android_log_print(ANDROID_LOG_INFO, TAG, "Installing Frame Processor Plugin \"%s\"...", name.c_str()); + __android_log_print(ANDROID_LOG_INFO, TAG, "Installing Frame Processor Plugin \"%s\"...", pluginName.c_str()); - auto callback = [pluginGlobal](jsi::Runtime& runtime, + if (!runtime.global().hasProperty(runtime, "FrameProcessorPlugins")) { + runtime.global().setProperty(runtime, "FrameProcessorPlugins", jsi::Object(runtime)); + } + jsi::Object frameProcessorPlugins = runtime.global().getPropertyAsObject(runtime, "FrameProcessorPlugins"); + + auto function = [pluginGlobal](jsi::Runtime& runtime, const jsi::Value& thisValue, const jsi::Value* arguments, size_t count) -> jsi::Value { // Unbox object and get typed HostObject auto boxedHostObject = arguments[0].asObject(runtime).asHostObject(runtime); - auto frameHostObject = static_cast(boxedHostObject.get()); + auto frameHostObject = dynamic_cast(boxedHostObject.get()); // parse params - we are offset by `1` because the frame is the first parameter. auto params = JArrayClass::newArray(count - 1); @@ -269,10 +252,14 @@ void FrameProcessorRuntimeManager::registerPlugin(alias_ref #include #include - -#include "RuntimeManager.h" -#include "reanimated-headers/AndroidScheduler.h" +#include #include "CameraView.h" #include "VisionCameraScheduler.h" @@ -32,25 +30,17 @@ class FrameProcessorRuntimeManager : public jni::HybridClass jThis, - jsi::Runtime* runtime, + jsi::Runtime* jsRuntime, std::shared_ptr jsCallInvoker, - std::shared_ptr scheduler) : - javaPart_(jni::make_global(jThis)), - runtime_(runtime), - jsCallInvoker_(jsCallInvoker), - scheduler_(scheduler) - {} + std::shared_ptr scheduler); private: friend HybridBase; jni::global_ref javaPart_; - jsi::Runtime* runtime_; - std::shared_ptr jsCallInvoker_; - std::shared_ptr _runtimeManager; - std::shared_ptr scheduler_; + jsi::Runtime* _jsRuntime; + std::shared_ptr _workletContext; jni::global_ref findCameraViewById(int viewId); - void initializeRuntime(); void installJSIBindings(); void registerPlugin(alias_ref plugin); void logErrorToJS(const std::string& message); diff --git a/android/src/main/cpp/MakeJSIRuntime.h b/android/src/main/cpp/MakeJSIRuntime.h deleted file mode 100644 index 39045bd..0000000 --- a/android/src/main/cpp/MakeJSIRuntime.h +++ /dev/null @@ -1,30 +0,0 @@ -// -// Created by Marc Rousavy on 06.07.21. -// - -#pragma once - -#include -#include - -#if FOR_HERMES - // Hermes - #include -#else - // JSC - #include -#endif - -namespace vision { - -using namespace facebook; - -static std::unique_ptr makeJSIRuntime() { - #if FOR_HERMES - return facebook::hermes::makeHermesRuntime(); - #else - return facebook::jsc::makeJSCRuntime(); - #endif -} - -} // namespace vision diff --git a/android/src/main/cpp/VisionCameraScheduler.cpp b/android/src/main/cpp/VisionCameraScheduler.cpp index 3f065a5..4369edb 100644 --- a/android/src/main/cpp/VisionCameraScheduler.cpp +++ b/android/src/main/cpp/VisionCameraScheduler.cpp @@ -14,9 +14,9 @@ TSelf VisionCameraScheduler::initHybrid(jni::alias_ref jThis) { return makeCxxInstance(jThis); } -void VisionCameraScheduler::scheduleOnUI(std::function job) { +void VisionCameraScheduler::dispatchAsync(std::function job) { // 1. add job to queue - uiJobs.push(job); + _jobs.push(job); scheduleTrigger(); } @@ -26,16 +26,18 @@ void VisionCameraScheduler::scheduleTrigger() { method(javaPart_.get()); } -void VisionCameraScheduler::triggerUI() { +void VisionCameraScheduler::trigger() { + std::unique_lock lock(_mutex); // 3. call job we enqueued in step 1. - auto job = uiJobs.pop(); + auto job = _jobs.front(); job(); + _jobs.pop(); } void VisionCameraScheduler::registerNatives() { registerHybrid({ makeNativeMethod("initHybrid", VisionCameraScheduler::initHybrid), - makeNativeMethod("triggerUI", VisionCameraScheduler::triggerUI), + makeNativeMethod("trigger", VisionCameraScheduler::trigger), }); } diff --git a/android/src/main/cpp/VisionCameraScheduler.h b/android/src/main/cpp/VisionCameraScheduler.h index a05c3c9..f306708 100644 --- a/android/src/main/cpp/VisionCameraScheduler.h +++ b/android/src/main/cpp/VisionCameraScheduler.h @@ -4,35 +4,47 @@ #pragma once -#include "Scheduler.h" #include #include #include +#include +#include namespace vision { using namespace facebook; -class VisionCameraScheduler : public reanimated::Scheduler, public jni::HybridClass { +/** + * A Scheduler that runs methods on the Frame Processor Thread (which is a Java Thread). + * In order to call something on the Java Frame Processor Thread, you have to: + * + * 1. Call `dispatchAsync(..)` with the given C++ Method. + * 2. Internally, `scheduleTrigger()` will get called, which is a Java Method. + * 3. The `scheduleTrigger()` Java Method will switch to the Frame Processor Java Thread and call `trigger()` on there + * 4. `trigger()` is a C++ function here that just calls the passed C++ Method from step 1. + */ +class VisionCameraScheduler : public jni::HybridClass { public: static auto constexpr kJavaDescriptor = "Lcom/mrousavy/camera/frameprocessor/VisionCameraScheduler;"; static jni::local_ref initHybrid(jni::alias_ref jThis); static void registerNatives(); // schedules the given job to be run on the VisionCamera FP Thread at some future point in time - void scheduleOnUI(std::function job) override; + void dispatchAsync(std::function job); private: friend HybridBase; jni::global_ref javaPart_; + std::queue> _jobs; + std::mutex _mutex; explicit VisionCameraScheduler(jni::alias_ref jThis): javaPart_(jni::make_global(jThis)) {} - // Schedules a call to `triggerUI` on the VisionCamera FP Thread + // Schedules a call to `trigger` on the VisionCamera FP Thread void scheduleTrigger(); // Calls the latest job in the job queue - void triggerUI() override; + void trigger(); }; } // namespace vision \ No newline at end of file diff --git a/android/src/main/cpp/reanimated-headers/AndroidErrorHandler.h b/android/src/main/cpp/reanimated-headers/AndroidErrorHandler.h deleted file mode 100644 index fd45154..0000000 --- a/android/src/main/cpp/reanimated-headers/AndroidErrorHandler.h +++ /dev/null @@ -1,30 +0,0 @@ -// copied from https://github.com/software-mansion/react-native-reanimated/blob/master/android/src/main/cpp/headers/AndroidErrorHandler.h - -#pragma once - -#include "ErrorHandler.h" -#include "AndroidScheduler.h" -#include "Scheduler.h" -#include -#include -#include -#include "Logger.h" - -namespace reanimated -{ - - class AndroidErrorHandler : public JavaClass, public ErrorHandler { - std::shared_ptr error; - std::shared_ptr scheduler; - void raiseSpec() override; - public: - static auto constexpr kJavaDescriptor = "Lcom/swmansion/reanimated/AndroidErrorHandler;"; - AndroidErrorHandler( - std::shared_ptr scheduler); - std::shared_ptr getScheduler() override; - std::shared_ptr getError() override; - void setError(std::string message) override; - virtual ~AndroidErrorHandler() {} - }; - -} diff --git a/android/src/main/cpp/reanimated-headers/AndroidScheduler.h b/android/src/main/cpp/reanimated-headers/AndroidScheduler.h deleted file mode 100644 index e96887c..0000000 --- a/android/src/main/cpp/reanimated-headers/AndroidScheduler.h +++ /dev/null @@ -1,37 +0,0 @@ -// copied from https://github.com/software-mansion/react-native-reanimated/blob/master/android/src/main/cpp/headers/AndroidScheduler.h - -#pragma once - -#include -#include -#include -#include -#include -#include "Scheduler.h" - -namespace reanimated { - - using namespace facebook; - - class AndroidScheduler : public jni::HybridClass { - public: - static auto constexpr kJavaDescriptor = "Lcom/swmansion/reanimated/Scheduler;"; - static jni::local_ref initHybrid(jni::alias_ref jThis); - static void registerNatives(); - - std::shared_ptr getScheduler() { return scheduler_; } - - void scheduleOnUI(); - - private: - friend HybridBase; - - void triggerUI(); - - jni::global_ref javaPart_; - std::shared_ptr scheduler_; - - explicit AndroidScheduler(jni::alias_ref jThis); - }; - -} diff --git a/android/src/main/java/com/mrousavy/camera/CameraView.kt b/android/src/main/java/com/mrousavy/camera/CameraView.kt index e666cb5..210d757 100644 --- a/android/src/main/java/com/mrousavy/camera/CameraView.kt +++ b/android/src/main/java/com/mrousavy/camera/CameraView.kt @@ -210,9 +210,7 @@ class CameraView(context: Context, private val frameProcessorThread: ExecutorSer } init { - if (FrameProcessorRuntimeManager.enableFrameProcessors) { - mHybridData = initHybrid() - } + mHybridData = initHybrid() previewView = PreviewView(context) previewView.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) diff --git a/android/src/main/java/com/mrousavy/camera/CameraViewModule.kt b/android/src/main/java/com/mrousavy/camera/CameraViewModule.kt index 7672b15..4dae53b 100644 --- a/android/src/main/java/com/mrousavy/camera/CameraViewModule.kt +++ b/android/src/main/java/com/mrousavy/camera/CameraViewModule.kt @@ -20,9 +20,6 @@ import com.facebook.react.modules.core.PermissionAwareActivity import com.facebook.react.modules.core.PermissionListener import com.facebook.react.uimanager.UIManagerHelper import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.turbomodule.core.CallInvokerHolderImpl -import com.mrousavy.camera.CameraView -import com.mrousavy.camera.ViewNotFoundError import java.util.concurrent.ExecutorService import com.mrousavy.camera.frameprocessor.FrameProcessorRuntimeManager import com.mrousavy.camera.parsers.* @@ -55,17 +52,6 @@ class CameraViewModule(reactContext: ReactApplicationContext) : ReactContextBase if (coroutineScope.isActive) { coroutineScope.cancel("CameraViewModule has been destroyed.") } - frameProcessorManager = null - } - - override fun initialize() { - super.initialize() - - if (frameProcessorManager == null) { - frameProcessorThread.execute { - frameProcessorManager = FrameProcessorRuntimeManager(reactApplicationContext, frameProcessorThread) - } - } } override fun onCatalystInstanceDestroy() { @@ -165,6 +151,18 @@ class CameraViewModule(reactContext: ReactApplicationContext) : ReactContextBase } } + @ReactMethod(isBlockingSynchronousMethod = true) + fun installFrameProcessorBindings(): Boolean { + try { + frameProcessorManager = FrameProcessorRuntimeManager(reactApplicationContext, frameProcessorThread) + frameProcessorManager!!.installBindings() + return true + } catch (e: Error) { + Log.e(TAG, "Failed to install Frame Processor JSI Bindings!", e) + return false + } + } + // TODO: This uses the Camera2 API to list all characteristics of a camera device and therefore doesn't work with Camera1. Find a way to use CameraX for this // https://issuetracker.google.com/issues/179925896 @ReactMethod diff --git a/android/src/main/java/com/mrousavy/camera/frameprocessor/FrameProcessorPlugin.java b/android/src/main/java/com/mrousavy/camera/frameprocessor/FrameProcessorPlugin.java index 54e445d..98905d5 100644 --- a/android/src/main/java/com/mrousavy/camera/frameprocessor/FrameProcessorPlugin.java +++ b/android/src/main/java/com/mrousavy/camera/frameprocessor/FrameProcessorPlugin.java @@ -48,6 +48,6 @@ public abstract class FrameProcessorPlugin { * @param plugin An instance of a plugin. */ public static void register(@NonNull FrameProcessorPlugin plugin) { - FrameProcessorRuntimeManager.Companion.getPlugins().add(plugin); + FrameProcessorRuntimeManager.Companion.addPlugin(plugin); } } diff --git a/android/src/main/java/com/mrousavy/camera/frameprocessor/FrameProcessorRuntimeManager.kt b/android/src/main/java/com/mrousavy/camera/frameprocessor/FrameProcessorRuntimeManager.kt index ebad5f6..b6abf7e 100644 --- a/android/src/main/java/com/mrousavy/camera/frameprocessor/FrameProcessorRuntimeManager.kt +++ b/android/src/main/java/com/mrousavy/camera/frameprocessor/FrameProcessorRuntimeManager.kt @@ -16,18 +16,20 @@ import java.util.concurrent.ExecutorService class FrameProcessorRuntimeManager(context: ReactApplicationContext, frameProcessorThread: ExecutorService) { companion object { const val TAG = "FrameProcessorRuntime" - val Plugins: ArrayList = ArrayList() - var enableFrameProcessors = true + private val Plugins: ArrayList = ArrayList() init { try { - System.loadLibrary("reanimated") System.loadLibrary("VisionCamera") } catch (e: UnsatisfiedLinkError) { - Log.w(TAG, "Failed to load Reanimated/VisionCamera C++ library. Frame Processors are disabled!") - enableFrameProcessors = false + Log.e(TAG, "Failed to load VisionCamera C++ library!", e) + throw e } } + + fun addPlugin(plugin: FrameProcessorPlugin) { + Plugins.add(plugin) + } } @DoNotStrip @@ -36,24 +38,11 @@ class FrameProcessorRuntimeManager(context: ReactApplicationContext, frameProces private var mScheduler: VisionCameraScheduler? = null init { - if (enableFrameProcessors) { - val holder = context.catalystInstance.jsCallInvokerHolder as CallInvokerHolderImpl - mScheduler = VisionCameraScheduler(frameProcessorThread) - mContext = WeakReference(context) - mHybridData = initHybrid(context.javaScriptContextHolder.get(), holder, mScheduler!!) - initializeRuntime() - - Log.i(TAG, "Installing Frame Processor Plugins...") - Plugins.forEach { plugin -> - registerPlugin(plugin) - } - Log.i(TAG, "Successfully installed ${Plugins.count()} Frame Processor Plugins!") - - Log.i(TAG, "Installing JSI Bindings on JS Thread...") - context.runOnJSQueueThread { - installJSIBindings() - } - } + val jsCallInvokerHolder = context.catalystInstance.jsCallInvokerHolder as CallInvokerHolderImpl + val jsRuntimeHolder = context.javaScriptContextHolder.get() + mScheduler = VisionCameraScheduler(frameProcessorThread) + mContext = WeakReference(context) + mHybridData = initHybrid(jsRuntimeHolder, jsCallInvokerHolder, mScheduler!!) } @Suppress("unused") @@ -67,13 +56,22 @@ class FrameProcessorRuntimeManager(context: ReactApplicationContext, frameProces return view ?: throw ViewNotFoundError(viewId) } + fun installBindings() { + Log.i(TAG, "Installing JSI Bindings on JS Thread...") + installJSIBindings() + Log.i(TAG, "Installing Frame Processor Plugins...") + Plugins.forEach { plugin -> + registerPlugin(plugin) + } + Log.i(TAG, "Successfully installed ${Plugins.count()} Frame Processor Plugins!") + } + // private C++ funcs private external fun initHybrid( jsContext: Long, jsCallInvokerHolder: CallInvokerHolderImpl, scheduler: VisionCameraScheduler ): HybridData - private external fun initializeRuntime() private external fun registerPlugin(plugin: FrameProcessorPlugin) private external fun installJSIBindings() } diff --git a/android/src/main/java/com/mrousavy/camera/frameprocessor/ImageProxyUtils.java b/android/src/main/java/com/mrousavy/camera/frameprocessor/ImageProxyUtils.java index e03a55a..69c02ec 100644 --- a/android/src/main/java/com/mrousavy/camera/frameprocessor/ImageProxyUtils.java +++ b/android/src/main/java/com/mrousavy/camera/frameprocessor/ImageProxyUtils.java @@ -19,7 +19,7 @@ public class ImageProxyUtils { Image image = imageProxy.getImage(); if (image == null) return false; // will throw an exception if the image is already closed - imageProxy.getImage().getCropRect(); + image.getCropRect(); // no exception thrown, image must still be valid. return true; } catch (Exception e) { diff --git a/android/src/main/java/com/mrousavy/camera/frameprocessor/VisionCameraScheduler.java b/android/src/main/java/com/mrousavy/camera/frameprocessor/VisionCameraScheduler.java index 4a96564..cdc43a3 100644 --- a/android/src/main/java/com/mrousavy/camera/frameprocessor/VisionCameraScheduler.java +++ b/android/src/main/java/com/mrousavy/camera/frameprocessor/VisionCameraScheduler.java @@ -17,11 +17,11 @@ public class VisionCameraScheduler { } private native HybridData initHybrid(); - private native void triggerUI(); + private native void trigger(); @SuppressWarnings("unused") @DoNotStrip private void scheduleTrigger() { - frameProcessorThread.submit(this::triggerUI); + frameProcessorThread.submit(this::trigger); } } diff --git a/docs/docs/guides/FRAME_PROCESSORS.mdx b/docs/docs/guides/FRAME_PROCESSORS.mdx index e31ca9c..5c65794 100644 --- a/docs/docs/guides/FRAME_PROCESSORS.mdx +++ b/docs/docs/guides/FRAME_PROCESSORS.mdx @@ -48,17 +48,13 @@ Frame processors are by far not limited to Hotdog detection, other examples incl * Creating scanners for **QR codes**, **Barcodes** or even custom codes such as **Snapchat's SnapCodes** or **Apple's AppClips** * Creating **snapchat-like filters**, e.g. draw a dog-mask filter over the user's face * Creating **color filters** with depth-detection +* Drawing boxes, text, overlays, or colors on the screen in realtime +* Rendering filters and shaders such as Blur, inverted colors, beauty filter, or more on the screen Because they are written in JS, Frame Processors are **simple**, **powerful**, **extensible** and **easy to create** while still running at **native performance**. (Frame Processors can run up to **1000 times a second**!) Also, you can use **fast-refresh** to quickly see changes while developing or publish [over-the-air updates](https://github.com/microsoft/react-native-code-push) to tweak the Hotdog detector's sensitivity in live apps without pushing a native update. :::note -Frame Processors require [**react-native-reanimated**](https://github.com/software-mansion/react-native-reanimated) 2.2.0 or higher. Also make sure to add - -```js -import 'react-native-reanimated' -``` - -to the top of the file when using `useFrameProcessor`. +Frame Processors require [**react-native-worklets**](https://github.com/chrfalch/react-native-worklets) 1.0.0 or higher. ::: ### Interacting with Frame Processors @@ -80,7 +76,7 @@ You can also easily read from, and assign to [**Shared Values**](https://docs.sw In this example, we detect a cat in the frame - if a cat was found, we assign the `catBounds` Shared Value to the coordinates of the cat (relative to the screen) which we can then use in a `useAnimatedStyle` hook to position the red rectangle surrounding the cat. This updates in realtime on the UI Thread, and can also be smoothly animated with `withSpring` or `withTiming`. -```tsx {6} +```tsx {7} // represents position of the cat on the screen 🐈 const catBounds = useSharedValue({ top: 0, left: 0, right: 0, bottom: 0 }) @@ -108,18 +104,18 @@ return ( ) ``` -And you can also call back to the React-JS thread by using [`runOnJS`](https://docs.swmansion.com/react-native-reanimated/docs/api/miscellaneous/runOnJS/): +And you can also call back to the React-JS thread by using `createRunInJsFn(...)`: -```tsx {9} -const onQRCodeDetected = useCallback((qrCode: string) => { +```tsx {1} +const onQRCodeDetected = Worklets.createRunInJsFn((qrCode: string) => { navigation.push("ProductPage", { productId: qrCode }) -}, []) +}) const frameProcessor = useFrameProcessor((frame) => { 'worklet' const qrCodes = scanQRCodes(frame) if (qrCodes.length > 0) { - runOnJS(onQRCodeDetected)(qrCodes[0]) + onQRCodeDetected(qrCodes[0]) } }, [onQRCodeDetected]) ``` @@ -133,23 +129,6 @@ npm i vision-camera-image-labeler cd ios && pod install ``` -Then add it to your `babel.config.js`. For the Image Labeler, this will be `__labelImage`: - -```js {6} -module.exports = { - plugins: [ - [ - 'react-native-reanimated/plugin', - { - globals: ['__labelImage'], - }, - ], -``` - -:::note -You have to restart metro-bundler for changes in the `babel.config.js` file to take effect. -::: - That's it! 🎉 Now you can use it: ```ts @@ -192,7 +171,7 @@ If you are using the [react-hooks ESLint plugin](https://www.npmjs.com/package/e #### Frame Processors -**Frame Processors** are JS functions that will be **workletized** using [react-native-reanimated](https://github.com/software-mansion/react-native-reanimated). They are created on a **parallel camera thread** using a separate JavaScript Runtime (_"VisionCamera JS-Runtime"_) and are **invoked synchronously** (using JSI) without ever going over the bridge. In a **Frame Processor** you can write normal JS code, call back to the React-JS Thread (e.g. `setState`), use [Shared Values](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/shared-values/) and call **Frame Processor Plugins**. +**Frame Processors** are JS functions that will be **workletized** using [react-native-worklets](https://github.com/chrfalch/react-native-worklets). They are created on a **parallel camera thread** using a separate JavaScript Runtime (_"VisionCamera JS-Runtime"_) and are **invoked synchronously** (using JSI) without ever going over the bridge. In a **Frame Processor** you can write normal JS code, call back to the React-JS Thread (e.g. `setState`), use [Shared Values](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/shared-values/) and call **Frame Processor Plugins**. > See [**the example Frame Processor**](https://github.com/mrousavy/react-native-vision-camera/blob/cf68a4c6476d085ec48fc424a53a96962e0c33f9/example/src/CameraPage.tsx#L199-L203) diff --git a/docs/docs/guides/FRAME_PROCESSORS_CREATE_OVERVIEW.mdx b/docs/docs/guides/FRAME_PROCESSORS_CREATE_OVERVIEW.mdx index ef83388..e22f472 100644 --- a/docs/docs/guides/FRAME_PROCESSORS_CREATE_OVERVIEW.mdx +++ b/docs/docs/guides/FRAME_PROCESSORS_CREATE_OVERVIEW.mdx @@ -61,7 +61,7 @@ Returns a `string` in JS: ```js export function detectObject(frame: Frame): string { 'worklet' - const result = __detectObject(frame) + const result = FrameProcessorPlugins.detectObject(frame) console.log(result) // <-- "cat" } ``` diff --git a/docs/docs/guides/FRAME_PROCESSOR_CREATE_FINAL.mdx b/docs/docs/guides/FRAME_PROCESSOR_CREATE_FINAL.mdx index 9b88815..80f7c3e 100644 --- a/docs/docs/guides/FRAME_PROCESSOR_CREATE_FINAL.mdx +++ b/docs/docs/guides/FRAME_PROCESSOR_CREATE_FINAL.mdx @@ -9,36 +9,17 @@ sidebar_label: Finish creating your Frame Processor Plugin To make the Frame Processor Plugin available to the Frame Processor Worklet Runtime, create the following wrapper function in JS/TS: ```ts -import type { Frame } from 'react-native-vision-camera' +import { FrameProcessorPlugins, Frame } from 'react-native-vision-camera' /** * Scans QR codes. */ export function scanQRCodes(frame: Frame): string[] { 'worklet' - return __scanQRCodes(frame) + return FrameProcessorPlugins.scanQRCodes(frame) } ``` -Users will then have to add the Frame Processor Plugin's name to their `babel.config.js`. - -For the QR Code Scanner, this will be `__scanQRCodes`: - -```js {6} -module.exports = { - plugins: [ - [ - 'react-native-reanimated/plugin', - { - globals: ['__scanQRCodes'], - }, - ], -``` - -:::note -You have to restart metro-bundler for changes in the `babel.config.js` file to take effect. -::: - ## Test it! Simply call the wrapper Worklet in your Frame Processor: @@ -64,11 +45,10 @@ If you want to distribute your Frame Processor Plugin, simply use npm. 1. Create a blank Native Module using [bob](https://github.com/callstack/react-native-builder-bob) or [create-react-native-module](https://github.com/brodybits/create-react-native-module) 2. Name it `vision-camera-plugin-xxxxx` where `xxxxx` is the name of your plugin 3. Remove the generated template code from the Example Native Module -4. Add VisionCamera to `peerDependencies`: `"react-native-vision-camera": ">= 2"` +4. Add VisionCamera to `peerDependencies`: `"react-native-vision-camera": ">= 3"` 5. Implement the Frame Processor Plugin in the iOS, Android and JS/TS Codebase using the guides above -6. Add installation instructions to the `README.md` to let users know they have to add your frame processor in the `babel.config.js` configuration. -7. Publish the plugin to npm. Users will only have to install the plugin using `npm i vision-camera-plugin-xxxxx` and add it to their `babel.config.js` file. -8. [Add the plugin to the **official VisionCamera plugin list**](https://github.com/mrousavy/react-native-vision-camera/edit/main/docs/docs/guides/FRAME_PROCESSOR_PLUGIN_LIST.mdx) for more visibility +6. Publish the plugin to npm. Users will only have to install the plugin using `npm i vision-camera-plugin-xxxxx` and add it to their `babel.config.js` file. +7. [Add the plugin to the **official VisionCamera plugin list**](https://github.com/mrousavy/react-native-vision-camera/edit/main/docs/docs/guides/FRAME_PROCESSOR_PLUGIN_LIST.mdx) for more visibility
diff --git a/docs/docs/guides/FRAME_PROCESSOR_CREATE_PLUGIN_ANDROID.mdx b/docs/docs/guides/FRAME_PROCESSOR_CREATE_PLUGIN_ANDROID.mdx index adbec44..63bf916 100644 --- a/docs/docs/guides/FRAME_PROCESSOR_CREATE_PLUGIN_ANDROID.mdx +++ b/docs/docs/guides/FRAME_PROCESSOR_CREATE_PLUGIN_ANDROID.mdx @@ -73,7 +73,7 @@ public class QRCodeFrameProcessorPlugin extends FrameProcessorPlugin { ``` :::note -The JS function name will be equal to the name you pass to the `super(...)` call (with a `__` prefix). Make sure it is unique across other Frame Processor Plugins. +The Frame Processor Plugin will be exposed to JS through the `FrameProcessorPlugins` object using the name you pass to the `super(...)` call. In this case, it would be `FrameProcessorPlugins.scanQRCodes(...)`. ::: 4. **Implement your Frame Processing.** See the [Example Plugin (Java)](https://github.com/mrousavy/react-native-vision-camera/blob/main/example/android/app/src/main/java/com/mrousavy/camera/example/ExampleFrameProcessorPlugin.java) for reference. @@ -137,7 +137,7 @@ class ExampleFrameProcessorPluginKotlin: FrameProcessorPlugin("scanQRCodes") { ``` :::note -The JS function name will be equal to the name you pass to the `FrameProcessorPlugin(...)` call (with a `__` prefix). Make sure it is unique across other Frame Processor Plugins. +The Frame Processor Plugin will be exposed to JS through the `FrameProcessorPlugins` object using the name you pass to the `FrameProcessorPlugin(...)` call. In this case, it would be `FrameProcessorPlugins.scanQRCodes(...)`. ::: 4. **Implement your Frame Processing.** See the [Example Plugin (Java)](https://github.com/mrousavy/react-native-vision-camera/blob/main/example/android/app/src/main/java/com/mrousavy/camera/example/ExampleFrameProcessorPlugin.java) for reference. diff --git a/docs/docs/guides/FRAME_PROCESSOR_CREATE_PLUGIN_IOS.mdx b/docs/docs/guides/FRAME_PROCESSOR_CREATE_PLUGIN_IOS.mdx index 1ef872a..4e5bd45 100644 --- a/docs/docs/guides/FRAME_PROCESSOR_CREATE_PLUGIN_IOS.mdx +++ b/docs/docs/guides/FRAME_PROCESSOR_CREATE_PLUGIN_IOS.mdx @@ -16,7 +16,7 @@ iOS Frame Processor Plugins can be written in either **Objective-C** or **Swift* ### Automatic setup -Run [Vision Camera Plugin Builder CLI](https://github.com/mateusz1913/vision-camera-plugin-builder), +Run [Vision Camera Plugin Builder CLI](https://github.com/mateusz1913/vision-camera-plugin-builder), ```sh npx vision-camera-plugin-builder ios @@ -63,7 +63,7 @@ VISION_EXPORT_FRAME_PROCESSOR(scanQRCodes) ``` :::note -The JS function name will be equal to the Objective-C function name you choose (with a `__` prefix). Make sure it is unique across other Frame Processor Plugins. +The Frame Processor Plugin will be exposed to JS through the `FrameProcessorPlugins` object using the same name as the Objective-C function. In this case, it would be `FrameProcessorPlugins.scanQRCodes(...)`. ::: 4. **Implement your Frame Processing.** See the [Example Plugin (Objective-C)](https://github.com/mrousavy/react-native-vision-camera/blob/main/example/ios/Frame%20Processor%20Plugins/Example%20Plugin%20%28Objective%2DC%29) for reference. diff --git a/docs/docs/guides/FRAME_PROCESSOR_PLUGIN_LIST.mdx b/docs/docs/guides/FRAME_PROCESSOR_PLUGIN_LIST.mdx index a4dd71f..b857caa 100644 --- a/docs/docs/guides/FRAME_PROCESSOR_PLUGIN_LIST.mdx +++ b/docs/docs/guides/FRAME_PROCESSOR_PLUGIN_LIST.mdx @@ -12,13 +12,10 @@ These are VisionCamera Frame Processor Plugins created by the community. ``` npm i vision-camera-xxxxx +cd ios && pod install ``` -2. Add the native function's name (the one with the `__` prefix) to your `babel.config.js`. (See their README for instructions) - -:::note -You have to restart metro-bundler for changes in the `babel.config.js` file to take effect. -::: +2. Rebuild your app and use the plugin ## Plugin List diff --git a/docs/docs/guides/SETUP.mdx b/docs/docs/guides/SETUP.mdx index 7942cd9..8ff0ca4 100644 --- a/docs/docs/guides/SETUP.mdx +++ b/docs/docs/guides/SETUP.mdx @@ -44,7 +44,7 @@ expo install react-native-vision-camera VisionCamera requires **iOS 11 or higher**, and **Android-SDK version 21 or higher**. See [Troubleshooting](/docs/guides/troubleshooting) if you're having installation issues. -> **(Optional)** If you want to use [**Frame Processors**](/docs/guides/frame-processors), you need to install [**react-native-reanimated**](https://github.com/software-mansion/react-native-reanimated) 2.2.0 or higher. +> **(Optional)** If you want to use [**Frame Processors**](/docs/guides/frame-processors), you need to install [**react-native-worklets**](https://github.com/chrfalch/react-native-worklets) 1.0.0 or higher. ## Updating manifests diff --git a/docs/docs/guides/TODO.md b/docs/docs/guides/TODO.md index a9ce6b3..fb6473c 100644 --- a/docs/docs/guides/TODO.md +++ b/docs/docs/guides/TODO.md @@ -6,5 +6,5 @@ This is an internal TODO list which I am using to keep track of some of the feat * [ ] Allow camera switching (front <-> back) while recording and stich videos together * [ ] Make `startRecording()` async. Due to NativeModules limitations, we can only have either one callback or one promise in a native function. For `startRecording()` we need both, since you probably also want to catch any errors that occured during a `startRecording()` call (or wait until the recording has actually started, since this can also take some time) * [ ] Return a `jsi::Value` reference for images (`UIImage`/`Bitmap`) on `takePhoto()` and `takeSnapshot()`. This way, we skip the entire file writing and reading, making image capture _a lot_ faster. -* [ ] Implement frame processors. The idea here is that the user passes a small JS function (reanimated worklet) to the `Camera::frameProcessor` prop which will then get called on every frame the camera previews. (I'd say we cap it to 30 times per second, even if the camera fps is higher) This can then be used to scan QR codes, detect faces, detect depth, render something ontop of the camera such as color filters, QR code boundaries or even dog filters, possibly even use AR - all from a single, small, and highly flexible JS function! +* [ ] Implement frame processors. The idea here is that the user passes a small JS function (worklet) to the `Camera::frameProcessor` prop which will then get called on every frame the camera previews. (I'd say we cap it to 30 times per second, even if the camera fps is higher) This can then be used to scan QR codes, detect faces, detect depth, render something ontop of the camera such as color filters, QR code boundaries or even dog filters, possibly even use AR - all from a single, small, and highly flexible JS function! * [ ] Create a custom MPEG4 encoder to allow for more customizability in `recordVideo()` (`bitRate`, `priority`, `minQuantizationParameter`, `allowFrameReordering`, `expectedFrameRate`, `realTime`, `minimizeMemoryUsage`) diff --git a/example/android/app/src/main/java/com/mrousavy/camera/example/MainApplication.java b/example/android/app/src/main/java/com/mrousavy/camera/example/MainApplication.java index 0650a2b..14f79ce 100644 --- a/example/android/app/src/main/java/com/mrousavy/camera/example/MainApplication.java +++ b/example/android/app/src/main/java/com/mrousavy/camera/example/MainApplication.java @@ -12,8 +12,6 @@ import com.facebook.react.defaults.DefaultReactNativeHost; import com.mrousavy.camera.CameraPackage; import com.mrousavy.camera.frameprocessor.FrameProcessorPlugin; -import com.facebook.react.bridge.JSIModulePackage; -import com.swmansion.reanimated.ReanimatedJSIModulePackage; public class MainApplication extends Application implements ReactApplication { @@ -47,11 +45,6 @@ public class MainApplication extends Application implements ReactApplication { protected Boolean isHermesEnabled() { return BuildConfig.IS_HERMES_ENABLED; } - - @Override - protected JSIModulePackage getJSIModulePackage() { - return new ReanimatedJSIModulePackage(); - } }; @Override diff --git a/example/android/build.gradle b/example/android/build.gradle index 1e63657..e74c245 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -13,7 +13,7 @@ buildscript { mavenCentral() } dependencies { - classpath("com.android.tools.build:gradle") + classpath('com.android.tools.build:gradle:7.4.1') classpath("com.facebook.react:react-native-gradle-plugin") } } diff --git a/example/babel.config.js b/example/babel.config.js index 0faa2af..c5038e7 100644 --- a/example/babel.config.js +++ b/example/babel.config.js @@ -5,12 +5,8 @@ const pak = require('../package.json'); module.exports = { presets: ['module:metro-react-native-babel-preset'], plugins: [ - [ - 'react-native-reanimated/plugin', - { - globals: ['__example_plugin', '__example_plugin_swift'], - }, - ], + ['react-native-reanimated/plugin'], + ['react-native-worklets/plugin'], [ 'module-resolver', { diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index cf2bd19..f2bb549 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -283,6 +283,10 @@ PODS: - react-native-video/Video (= 5.2.1) - react-native-video/Video (5.2.1): - React-Core + - react-native-worklets (0.1.0): + - React + - React-callinvoker + - React-Core - React-perflogger (0.71.2) - React-RCTActionSheet (0.71.2): - React-Core/RCTActionSheetHeaders (= 0.71.2) @@ -369,7 +373,7 @@ PODS: - React-perflogger (= 0.71.2) - RNGestureHandler (2.9.0): - React-Core - - RNReanimated (2.14.4): + - RNReanimated (3.0.0-rc.10): - DoubleConversion - FBLazyVector - FBReactNativeSpec @@ -407,6 +411,7 @@ PODS: - React - React-callinvoker - React-Core + - react-native-worklets - Yoga (1.14.0) DEPENDENCIES: @@ -437,6 +442,7 @@ DEPENDENCIES: - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) - "react-native-slider (from `../node_modules/@react-native-community/slider`)" - react-native-video (from `../node_modules/react-native-video`) + - react-native-worklets (from `../node_modules/react-native-worklets`) - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`) - React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`) @@ -514,6 +520,8 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-community/slider" react-native-video: :path: "../node_modules/react-native-video" + react-native-worklets: + :path: "../node_modules/react-native-worklets" React-perflogger: :path: "../node_modules/react-native/ReactCommon/reactperflogger" React-RCTActionSheet: @@ -583,6 +591,7 @@ SPEC CHECKSUMS: react-native-safe-area-context: 39c2d8be3328df5d437ac1700f4f3a4f75716acc react-native-slider: 33b8d190b59d4f67a541061bb91775d53d617d9d react-native-video: c26780b224543c62d5e1b2a7244a5cd1b50e8253 + react-native-worklets: c7576ad4ad0f030ff41e8d74ad0077c96054a6c1 React-perflogger: c7ccda3d1d1da837f7ff4e54e816022a6803ee87 React-RCTActionSheet: 01c125aebbad462a24228f68c584c7a921d6c28e React-RCTAnimation: 5277a9440acffc4a5b7baa6ae3880fe467277ae6 @@ -597,11 +606,11 @@ SPEC CHECKSUMS: React-runtimeexecutor: 4bf9a9086d27f74065fce1dddac274aa95216952 ReactCommon: f697c0ac52e999aa818e43e2b6f277787c735e2d RNGestureHandler: 071d7a9ad81e8b83fe7663b303d132406a7d8f39 - RNReanimated: cc5e3aa479cb9170bcccf8204291a6950a3be128 + RNReanimated: fbc356493970e3acddc15586b1bccb5eab3ff1ec RNScreens: ea4cd3a853063cda19a4e3c28d2e52180c80f4eb RNStaticSafeAreaInsets: 055ddbf5e476321720457cdaeec0ff2ba40ec1b8 RNVectorIcons: fcc2f6cb32f5735b586e66d14103a74ce6ad61f8 - VisionCamera: b9345e7da5eff343cc1603dd19153e2b9acd0c07 + VisionCamera: 312151eb95370d1d764720de3b7dad33d8c7fb40 Yoga: 5b0304b3dbef2b52e078052138e23a19c7dacaef PODFILE CHECKSUM: d53724fe402c2547f1dd1cc571bbe77d9820e636 diff --git a/example/package.json b/example/package.json index 046aaa3..95faa75 100644 --- a/example/package.json +++ b/example/package.json @@ -14,8 +14,8 @@ "typescript": "tsc --noEmit" }, "dependencies": { - "@react-native-community/blur": "^4.3.0", "@react-native-camera-roll/camera-roll": "^5.2.3", + "@react-native-community/blur": "^4.3.0", "@react-native-community/slider": "^4.4.2", "@react-navigation/native": "^6.1.3", "@react-navigation/native-stack": "^6.9.9", @@ -23,12 +23,13 @@ "react-native": "^0.71.2", "react-native-gesture-handler": "^2.9.0", "react-native-pressable-opacity": "^1.0.10", - "react-native-reanimated": "^2.14.4", + "react-native-reanimated": "^3.0.0-rc.10", "react-native-safe-area-context": "^4.5.0", "react-native-screens": "^3.19.0", "react-native-static-safe-area-insets": "^2.2.0", "react-native-vector-icons": "^9.2.0", - "react-native-video": "^5.2.1" + "react-native-video": "^5.2.1", + "react-native-worklets": "https://github.com/chrfalch/react-native-worklets#50950aa" }, "devDependencies": { "@babel/core": "^7.20.12", diff --git a/example/src/App.tsx b/example/src/App.tsx index 7c09ca5..0cfc764 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -6,6 +6,7 @@ import { MediaPage } from './MediaPage'; import { CameraPage } from './CameraPage'; import type { Routes } from './Routes'; import { Camera, CameraPermissionStatus } from 'react-native-vision-camera'; +import { GestureHandlerRootView } from 'react-native-gesture-handler'; const Stack = createNativeStackNavigator(); @@ -28,24 +29,26 @@ export function App(): React.ReactElement | null { const showPermissionsPage = cameraPermission !== 'authorized' || microphonePermission === 'not-determined'; return ( - - - - + - + initialRouteName={showPermissionsPage ? 'PermissionsPage' : 'CameraPage'}> + + + + +
); } diff --git a/example/src/frame-processors/ExamplePlugin.ts b/example/src/frame-processors/ExamplePlugin.ts index 3ce414c..fbc386f 100644 --- a/example/src/frame-processors/ExamplePlugin.ts +++ b/example/src/frame-processors/ExamplePlugin.ts @@ -1,20 +1,16 @@ -/* global __example_plugin __example_plugin_swift */ -import type { Frame } from 'react-native-vision-camera'; - -declare let _WORKLET: true | undefined; +import { FrameProcessorPlugins, Frame } from 'react-native-vision-camera'; export function examplePluginSwift(frame: Frame): string[] { 'worklet'; - if (!_WORKLET) throw new Error('examplePluginSwift must be called from a frame processor!'); - // @ts-expect-error because this function is dynamically injected by VisionCamera - return __example_plugin_swift(frame, 'hello!', 'parameter2', true, 42, { test: 0, second: 'test' }, ['another test', 5]); + return FrameProcessorPlugins.example_plugin_swift(frame, 'hello!', 'parameter2', true, 42, { test: 0, second: 'test' }, [ + 'another test', + 5, + ]); } export function examplePlugin(frame: Frame): string[] { 'worklet'; - if (!_WORKLET) throw new Error('examplePlugin must be called from a frame processor!'); - // @ts-expect-error because this function is dynamically injected by VisionCamera - return __example_plugin(frame, 'hello!', 'parameter2', true, 42, { test: 0, second: 'test' }, ['another test', 5]); + return FrameProcessorPlugins.example_plugin(frame, 'hello!', 'parameter2', true, 42, { test: 0, second: 'test' }, ['another test', 5]); } diff --git a/example/yarn.lock b/example/yarn.lock index 979f86a..2e3b322 100644 --- a/example/yarn.lock +++ b/example/yarn.lock @@ -1190,7 +1190,7 @@ dependencies: "@hapi/hoek" "^9.0.0" -"@sideway/formula@^3.0.0": +"@sideway/formula@^3.0.1": version "3.0.1" resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f" integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== @@ -2259,9 +2259,9 @@ ee-first@1.1.1: integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== electron-to-chromium@^1.4.284: - version "1.4.292" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.292.tgz#e3a3dca3780c8ce01e2c1866b5ec2fbe31c423e3" - integrity sha512-ESWOSyJy5odDlE8wvh5NNAMORv4r6assPwIPGHEMWrWD0SONXcG/xT+9aD9CQyeRwyYDPo6dJT4Bbeg5uevVQQ== + version "1.4.294" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.294.tgz#ad80317b85f0859a9454680fbc1c726fefa7e6fd" + integrity sha512-PuHZB3jEN7D8WPPjLmBQAsqQz8tWHlkkB4n0E2OYw8RwVdmBYV0Wn+rUFH8JqYyIRb4HQhhedgxlZL163wqLrQ== eme-encryption-scheme-polyfill@^2.0.1: version "2.1.1" @@ -2884,7 +2884,7 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3: +get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.0.tgz#7ad1dc0535f3a2904bba075772763e5051f6d05f" integrity sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q== @@ -3162,11 +3162,11 @@ inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== internal-slot@^1.0.3, internal-slot@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.4.tgz#8551e7baf74a7a6ba5f749cfb16aa60722f0d6f3" - integrity sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ== + version "1.0.5" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.5.tgz#f2a2ee21f668f8627a4667f309dc0f4fb6674986" + integrity sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ== dependencies: - get-intrinsic "^1.1.3" + get-intrinsic "^1.2.0" has "^1.0.3" side-channel "^1.0.4" @@ -3550,14 +3550,14 @@ jest-worker@^27.2.0: supports-color "^8.0.0" joi@^17.2.1: - version "17.7.0" - resolved "https://registry.yarnpkg.com/joi/-/joi-17.7.0.tgz#591a33b1fe1aca2bc27f290bcad9b9c1c570a6b3" - integrity sha512-1/ugc8djfn93rTE3WRKdCzGGt/EtiYKxITMO4Wiv6q5JL1gl9ePt4kBsl1S499nbosspfctIQTpYIhSmHA3WAg== + version "17.7.1" + resolved "https://registry.yarnpkg.com/joi/-/joi-17.7.1.tgz#854fc85c7fa3cfc47c91124d30bffdbb58e06cec" + integrity sha512-teoLhIvWE298R6AeJywcjR4sX2hHjB3/xJX4qPjg+gTg+c0mzUDsziYlqPmLomq9gVsfaMcgPaGc7VxtD/9StA== dependencies: "@hapi/hoek" "^9.0.0" "@hapi/topo" "^5.0.0" "@sideway/address" "^4.1.3" - "@sideway/formula" "^3.0.0" + "@sideway/formula" "^3.0.1" "@sideway/pinpoint" "^2.0.0" js-sdsl@^4.1.4: @@ -4444,9 +4444,9 @@ minimatch@^5.0.1: brace-expansion "^2.0.1" minimist@^1.2.6: - version "1.2.7" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" - integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== mixin-deep@^1.2.0: version "1.3.2" @@ -4984,9 +4984,9 @@ range-parser@~1.2.1: integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== react-devtools-core@^4.26.1: - version "4.27.1" - resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-4.27.1.tgz#167aa174383c65786cbb7e965a5b39c702f0a2d3" - integrity sha512-qXhcxxDWiFmFAOq48jts9YQYe1+wVoUXzJTlY4jbaATzyio6dd6CUGu3dXBhREeVgpZ+y4kg6vFJzIOZh6vY2w== + version "4.27.2" + resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-4.27.2.tgz#d20fc57e258c656eedabafc2c851d38b33583148" + integrity sha512-8SzmIkpO87alD7Xr6gWIEa1jHkMjawOZ+6egjazlnjB4UUcbnzGDf/vBJ4BzGuWWEM+pzrxuzsPpcMqlQkYK2g== dependencies: shell-quote "^1.6.1" ws "^7" @@ -5042,10 +5042,10 @@ react-native-pressable-opacity@^1.0.10: resolved "https://registry.yarnpkg.com/react-native-pressable-opacity/-/react-native-pressable-opacity-1.0.10.tgz#799df1a913d3b28f42ada765465fe7723eb7166d" integrity sha512-Py9YH9TlS3Lv1so5JCj6bgiqkeYYGupF4ZImlpoyhhId/t/RiSqR68LlASOHgdctqQuqVJObQiFfzX8oZI9+6w== -react-native-reanimated@^2.14.4: - version "2.14.4" - resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-2.14.4.tgz#3fa3da4e7b99f5dfb28f86bcf24d9d1024d38836" - integrity sha512-DquSbl7P8j4SAmc+kRdd75Ianm8G+IYQ9T4AQ6lrpLVeDkhZmjWI0wkutKWnp6L7c5XNVUrFDUf69dwETLCItQ== +react-native-reanimated@^3.0.0-rc.10: + version "3.0.0-rc.10" + resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-3.0.0-rc.10.tgz#40e5b628759aa81f94317fd0301231292b4eacf5" + integrity sha512-0P2jSO+dXHRxSzqSxNp08VaUy89nqeUIvqBS0wlI8lsli8CJcqulL3pjNqTGzBkxXjt13mGdIzJv4u9lSjHPzg== dependencies: "@babel/plugin-transform-object-assign" "^7.16.7" "@babel/preset-typescript" "^7.16.7" @@ -5091,6 +5091,10 @@ react-native-video@^5.2.1: prop-types "^15.7.2" shaka-player "^2.5.9" +"react-native-worklets@https://github.com/chrfalch/react-native-worklets#50950aa": + version "0.1.0" + resolved "https://github.com/chrfalch/react-native-worklets#50950aa1b671a0d8a9e79878e63a3445991e7192" + react-native@^0.71.2: version "0.71.2" resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.71.2.tgz#b6977eda2a6dc10baa006bf4ab1ee08318607ce9" diff --git a/ios/CameraBridge.h b/ios/CameraBridge.h index de423ef..e706a7f 100644 --- a/ios/CameraBridge.h +++ b/ios/CameraBridge.h @@ -19,12 +19,6 @@ #import "RCTBridge+runOnJS.h" #import "JSConsoleHelper.h" -#ifdef VISION_CAMERA_DISABLE_FRAME_PROCESSORS -static bool VISION_CAMERA_ENABLE_FRAME_PROCESSORS = false; -#else -static bool VISION_CAMERA_ENABLE_FRAME_PROCESSORS = true; -#endif - @interface CameraBridge: RCTViewManager @end diff --git a/ios/CameraView+Orientation.swift b/ios/CameraView+Orientation.swift index e647b8e..98c21f2 100644 --- a/ios/CameraView+Orientation.swift +++ b/ios/CameraView+Orientation.swift @@ -29,10 +29,10 @@ extension CameraView { internal func updateOrientation() { // Updates the Orientation for all rotable - let isMirrored = self.videoDeviceInput?.device.position == .front + let isMirrored = videoDeviceInput?.device.position == .front - let connectionOrientation = self.outputOrientation - self.captureSession.outputs.forEach { output in + let connectionOrientation = outputOrientation + captureSession.outputs.forEach { output in output.connections.forEach { connection in if connection.isVideoMirroringSupported { connection.automaticallyAdjustsVideoMirroring = false diff --git a/ios/CameraViewManager.m b/ios/CameraViewManager.m index 6c9df17..e1d8e85 100644 --- a/ios/CameraViewManager.m +++ b/ios/CameraViewManager.m @@ -60,6 +60,8 @@ RCT_EXTERN_METHOD(stopRecording:(nonnull NSNumber *)node resolve:(RCTPromiseReso RCT_EXTERN_METHOD(takePhoto:(nonnull NSNumber *)node options:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject); RCT_EXTERN_METHOD(focus:(nonnull NSNumber *)node point:(NSDictionary *)point resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject); +// Static Methods +RCT_EXTERN__BLOCKING_SYNCHRONOUS_METHOD(installFrameProcessorBindings); RCT_EXTERN_METHOD(getAvailableVideoCodecs:(nonnull NSNumber *)node fileType:(NSString *)fileType resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject); @end diff --git a/ios/CameraViewManager.swift b/ios/CameraViewManager.swift index 74df8cb..7c48efc 100644 --- a/ios/CameraViewManager.swift +++ b/ios/CameraViewManager.swift @@ -15,20 +15,6 @@ final class CameraViewManager: RCTViewManager { private var runtimeManager: FrameProcessorRuntimeManager? - override var bridge: RCTBridge! { - didSet { - // Install Frame Processor bindings and setup Runtime - if VISION_CAMERA_ENABLE_FRAME_PROCESSORS { - CameraQueues.frameProcessorQueue.async { - self.runtimeManager = FrameProcessorRuntimeManager(bridge: self.bridge) - self.bridge.runOnJS { - self.runtimeManager!.installFrameProcessorBindings() - } - } - } - } - } - override var methodQueue: DispatchQueue! { return DispatchQueue.main } @@ -43,6 +29,14 @@ final class CameraViewManager: RCTViewManager { // pragma MARK: React Functions + @objc + final func installFrameProcessorBindings() -> NSNumber { + // Runs on JS Thread + runtimeManager = FrameProcessorRuntimeManager() + runtimeManager!.installFrameProcessorBindings() + return NSNumber(booleanLiteral: true) + } + @objc final func startRecording(_ node: NSNumber, options: NSDictionary, onRecordCallback: @escaping RCTResponseSenderBlock) { let component = getCameraView(withTag: node) diff --git a/ios/Frame Processor/FrameProcessorPlugin.h b/ios/Frame Processor/FrameProcessorPlugin.h index 9408774..091a532 100644 --- a/ios/Frame Processor/FrameProcessorPlugin.h +++ b/ios/Frame Processor/FrameProcessorPlugin.h @@ -28,14 +28,13 @@ * * Make sure your frame processor returns a Value that can be converted to JS * * Make sure to use this Macro in an @implementation, not @interface * - * The JS function will have the same name as the given Objective-C function, but with a "__" prefix. - * Make sure to add that function to the babel.config.js under reanimated's "globals" option, and add TypeScript type declarations. + * The JS function will have the same name as the given Objective-C function. It can be accessed through the FrameProcessorPlugins object exposed by VisionCamera. */ #define VISION_EXPORT_FRAME_PROCESSOR(frame_processor) \ \ +(void)load \ { \ - [FrameProcessorPluginRegistry addFrameProcessorPlugin:@"__" @ #frame_processor callback:^id(Frame* frame, NSArray* args) { \ + [FrameProcessorPluginRegistry addFrameProcessorPlugin:@ #frame_processor callback:^id(Frame* frame, NSArray* args) { \ return frame_processor(frame, args); \ }]; \ } @@ -55,7 +54,7 @@ objc_name : NSObject \ +(void)load \ { \ - [FrameProcessorPluginRegistry addFrameProcessorPlugin:@"__" @ #name callback:^id(Frame* frame, NSArray* args) { \ + [FrameProcessorPluginRegistry addFrameProcessorPlugin:@ #name callback:^id(Frame* frame, NSArray* args) { \ return [objc_name callback:frame withArgs:args]; \ }]; \ } diff --git a/ios/Frame Processor/FrameProcessorRuntimeManager.h b/ios/Frame Processor/FrameProcessorRuntimeManager.h index a7e1190..9142e43 100644 --- a/ios/Frame Processor/FrameProcessorRuntimeManager.h +++ b/ios/Frame Processor/FrameProcessorRuntimeManager.h @@ -13,13 +13,7 @@ @interface FrameProcessorRuntimeManager : NSObject -- (instancetype)init NS_UNAVAILABLE; - -/** - Initializes the Frame Processor Runtime Manager with the given bridge. - This init is not thread safe, so only init this on the Thread you want the runtime to run on. - */ -- (instancetype) initWithBridge:(RCTBridge*)bridge; +- (instancetype) init; - (void) installFrameProcessorBindings; diff --git a/ios/Frame Processor/FrameProcessorRuntimeManager.mm b/ios/Frame Processor/FrameProcessorRuntimeManager.mm index 3841b20..fe8b483 100644 --- a/ios/Frame Processor/FrameProcessorRuntimeManager.mm +++ b/ios/Frame Processor/FrameProcessorRuntimeManager.mm @@ -19,25 +19,12 @@ #import #import -#ifndef VISION_CAMERA_DISABLE_FRAME_PROCESSORS - #if __has_include() - #if __has_include() - #import - #import - #import - #import "VisionCameraScheduler.h" - #define ENABLE_FRAME_PROCESSORS - #else - #warning Your react-native-reanimated version is not compatible with VisionCamera, Frame Processors are disabled. Make sure you're using reanimated 2.2.0 or above! - #endif - #else - #warning The NativeReanimatedModule.h header could not be found, Frame Processors are disabled. If you want to use Frame Processors, make sure you install react-native-reanimated! - #endif -#endif +#import "JsiWorkletContext.h" +#import "JsiWorkletApi.h" +#import "JsiWorklet.h" #import "FrameProcessorUtils.h" #import "FrameProcessorCallback.h" -#import "../React Utils/MakeJSIRuntime.h" #import "../React Utils/JSIUtils.h" // Forward declarations for the Swift classes @@ -51,84 +38,101 @@ __attribute__((objc_runtime_name("_TtC12VisionCamera10CameraView"))) @end @implementation FrameProcessorRuntimeManager { -#ifdef ENABLE_FRAME_PROCESSORS - std::unique_ptr runtimeManager; -#endif - __weak RCTBridge* weakBridge; + std::shared_ptr workletContext; } -- (instancetype) initWithBridge:(RCTBridge*)bridge { - self = [super init]; - if (self) { -#ifdef ENABLE_FRAME_PROCESSORS - NSLog(@"FrameProcessorBindings: Creating Runtime Manager..."); - weakBridge = bridge; - - auto runtime = vision::makeJSIRuntime(); - reanimated::RuntimeDecorator::decorateRuntime(*runtime, "FRAME_PROCESSOR"); - runtime->global().setProperty(*runtime, "_FRAME_PROCESSOR", jsi::Value(true)); - - auto callInvoker = bridge.jsCallInvoker; - auto scheduler = std::make_shared(callInvoker); - runtimeManager = std::make_unique(std::move(runtime), - std::make_shared(scheduler), - scheduler); - NSLog(@"FrameProcessorBindings: Runtime Manager created!"); - - NSLog(@"FrameProcessorBindings: Installing Frame Processor plugins..."); - auto& visionRuntime = *runtimeManager->runtime; - auto visionGlobal = visionRuntime.global(); - - for (NSString* pluginKey in [FrameProcessorPluginRegistry frameProcessorPlugins]) { - auto pluginName = [pluginKey UTF8String]; - - NSLog(@"FrameProcessorBindings: Installing Frame Processor plugin \"%s\"...", pluginName); - FrameProcessorPlugin callback = [[FrameProcessorPluginRegistry frameProcessorPlugins] valueForKey:pluginKey]; - - auto function = [callback, callInvoker](jsi::Runtime& runtime, - const jsi::Value& thisValue, - const jsi::Value* arguments, - size_t count) -> jsi::Value { - auto frameHostObject = arguments[0].asObject(runtime).asHostObject(runtime); - auto frame = static_cast(frameHostObject.get()); - - auto args = convertJSICStyleArrayToNSArray(runtime, - arguments + 1, // start at index 1 since first arg = Frame - count - 1, // use smaller count - callInvoker); - id result = callback(frame->frame, args); - - return convertObjCObjectToJSIValue(runtime, result); - }; - - visionGlobal.setProperty(visionRuntime, pluginName, jsi::Function::createFromHostFunction(visionRuntime, - jsi::PropNameID::forAscii(visionRuntime, pluginName), - 1, // frame - function)); +- (instancetype)init { + if (self = [super init]) { + // Initialize self } + return self; +} - NSLog(@"FrameProcessorBindings: Frame Processor plugins installed!"); -#else - NSLog(@"Reanimated not found, Frame Processors are disabled."); -#endif +- (void) setupWorkletContext:(jsi::Runtime&)runtime { + NSLog(@"FrameProcessorBindings: Creating Worklet Context..."); + + auto callInvoker = RCTBridge.currentBridge.jsCallInvoker; + + auto runOnJS = [callInvoker](std::function&& f) { + // Run on React JS Runtime + callInvoker->invokeAsync(std::move(f)); + }; + auto runOnWorklet = [](std::function&& f) { + // Run on Frame Processor Worklet Runtime + dispatch_async(CameraQueues.frameProcessorQueue, [f = std::move(f)](){ + f(); + }); + }; + + workletContext = std::make_shared("VisionCamera"); + workletContext->initialize("VisionCamera", + &runtime, + runOnJS, + runOnWorklet); + + NSLog(@"FrameProcessorBindings: Worklet Context Created!"); + + NSLog(@"FrameProcessorBindings: Installing Frame Processor plugins..."); + + jsi::Object frameProcessorPlugins(runtime); + + // Iterate through all registered plugins (+init) + for (NSString* pluginKey in [FrameProcessorPluginRegistry frameProcessorPlugins]) { + auto pluginName = [pluginKey UTF8String]; + + NSLog(@"FrameProcessorBindings: Installing Frame Processor plugin \"%s\"...", pluginName); + // Get the Plugin callback func + FrameProcessorPlugin callback = [[FrameProcessorPluginRegistry frameProcessorPlugins] valueForKey:pluginKey]; + + // Create the JSI host function + auto function = [callback, callInvoker](jsi::Runtime& runtime, + const jsi::Value& thisValue, + const jsi::Value* arguments, + size_t count) -> jsi::Value { + // Get the first parameter, which is always the native Frame Host Object. + auto frameHostObject = arguments[0].asObject(runtime).asHostObject(runtime); + auto frame = static_cast(frameHostObject.get()); + + // Convert any additional parameters to the Frame Processor to ObjC objects + auto args = convertJSICStyleArrayToNSArray(runtime, + arguments + 1, // start at index 1 since first arg = Frame + count - 1, // use smaller count + callInvoker); + // Call the FP Plugin, which might return something. + id result = callback(frame->frame, args); + + // Convert the return value (or null) to a JS Value and return it to JS + return convertObjCObjectToJSIValue(runtime, result); + }; + + // Assign it to the Proxy. + // A FP Plugin called "example_plugin" can be now called from JS using "FrameProcessorPlugins.example_plugin(frame)" + frameProcessorPlugins.setProperty(runtime, + pluginName, + jsi::Function::createFromHostFunction(runtime, + jsi::PropNameID::forAscii(runtime, pluginName), + 1, // frame + function)); } - return self; + + // global.FrameProcessorPlugins Proxy + runtime.global().setProperty(runtime, "FrameProcessorPlugins", frameProcessorPlugins); + + NSLog(@"FrameProcessorBindings: Frame Processor plugins installed!"); } - (void) installFrameProcessorBindings { -#ifdef ENABLE_FRAME_PROCESSORS - if (!weakBridge) { - NSLog(@"FrameProcessorBindings: Failed to install Frame Processor Bindings - bridge was null!"); - return; - } - NSLog(@"FrameProcessorBindings: Installing Frame Processor Bindings for Bridge..."); - RCTCxxBridge *cxxBridge = (RCTCxxBridge *)weakBridge; + RCTCxxBridge *cxxBridge = (RCTCxxBridge *)[RCTBridge currentBridge]; if (!cxxBridge.runtime) { return; } jsi::Runtime& jsiRuntime = *(jsi::Runtime*)cxxBridge.runtime; + + // Install the Worklet Runtime in the main React JS Runtime + [self setupWorkletContext:jsiRuntime]; + NSLog(@"FrameProcessorBindings: Installing global functions..."); // setFrameProcessor(viewTag: number, frameProcessor: (frame: Frame) => void) @@ -139,27 +143,21 @@ __attribute__((objc_runtime_name("_TtC12VisionCamera10CameraView"))) NSLog(@"FrameProcessorBindings: Setting new frame processor..."); if (!arguments[0].isNumber()) throw jsi::JSError(runtime, "Camera::setFrameProcessor: First argument ('viewTag') must be a number!"); if (!arguments[1].isObject()) throw jsi::JSError(runtime, "Camera::setFrameProcessor: Second argument ('frameProcessor') must be a function!"); - if (!runtimeManager || !runtimeManager->runtime) throw jsi::JSError(runtime, "Camera::setFrameProcessor: The RuntimeManager is not yet initialized!"); auto viewTag = arguments[0].asNumber(); - NSLog(@"FrameProcessorBindings: Adapting Shareable value from function (conversion to worklet)..."); - auto worklet = reanimated::ShareableValue::adapt(runtime, arguments[1], runtimeManager.get()); - NSLog(@"FrameProcessorBindings: Successfully created worklet!"); + NSLog(@"FrameProcessorBindings: Converting JSI Function to Worklet..."); + auto worklet = std::make_shared(runtime, arguments[1]); RCTExecuteOnMainQueue([=]() { auto currentBridge = [RCTBridge currentBridge]; auto anonymousView = [currentBridge.uiManager viewForReactTag:[NSNumber numberWithDouble:viewTag]]; auto view = static_cast(anonymousView); - dispatch_async(CameraQueues.frameProcessorQueue, [=]() { - NSLog(@"FrameProcessorBindings: Converting worklet to Objective-C callback..."); + NSLog(@"FrameProcessorBindings: Converting worklet to Objective-C callback..."); - auto& rt = *runtimeManager->runtime; - auto function = worklet->getValue(rt).asObject(rt).asFunction(rt); + view.frameProcessorCallback = convertWorkletToFrameProcessorCallback(workletContext->getWorkletRuntime(), worklet); - view.frameProcessorCallback = convertJSIFunctionToFrameProcessorCallback(rt, function); - NSLog(@"FrameProcessorBindings: Frame processor set!"); - }); + NSLog(@"FrameProcessorBindings: Frame processor set!"); }); return jsi::Value::undefined(); @@ -198,7 +196,6 @@ __attribute__((objc_runtime_name("_TtC12VisionCamera10CameraView"))) unsetFrameProcessor)); NSLog(@"FrameProcessorBindings: Finished installing bindings."); -#endif } @end diff --git a/ios/Frame Processor/FrameProcessorUtils.h b/ios/Frame Processor/FrameProcessorUtils.h index 0da0390..ae51a52 100644 --- a/ios/Frame Processor/FrameProcessorUtils.h +++ b/ios/Frame Processor/FrameProcessorUtils.h @@ -17,7 +17,9 @@ #endif #import +#import "JsiWorklet.h" +#import using namespace facebook; -FrameProcessorCallback convertJSIFunctionToFrameProcessorCallback(jsi::Runtime &runtime, const jsi::Function &value); +FrameProcessorCallback convertWorkletToFrameProcessorCallback(jsi::Runtime& runtime, std::shared_ptr worklet); diff --git a/ios/Frame Processor/FrameProcessorUtils.mm b/ios/Frame Processor/FrameProcessorUtils.mm index 25daa3e..233bdcf 100644 --- a/ios/Frame Processor/FrameProcessorUtils.mm +++ b/ios/Frame Processor/FrameProcessorUtils.mm @@ -19,18 +19,25 @@ #import "JSConsoleHelper.h" #import -FrameProcessorCallback convertJSIFunctionToFrameProcessorCallback(jsi::Runtime& runtime, const jsi::Function& value) { - __block auto cb = value.getFunction(runtime); +#import "JsiWorklet.h" +FrameProcessorCallback convertWorkletToFrameProcessorCallback(jsi::Runtime& runtime, std::shared_ptr worklet) { + + auto workletInvoker = std::make_shared(worklet); + + // Converts a Worklet to a callable Objective-C block function return ^(Frame* frame) { auto frameHostObject = std::make_shared(frame); try { - cb.callWithThis(runtime, cb, jsi::Object::createFromHostObject(runtime, frameHostObject)); + // Call JS Frame Processor function with boxed Frame Host Object + auto argument = jsi::Object::createFromHostObject(runtime, frameHostObject); + jsi::Value jsValue(std::move(argument)); + workletInvoker->call(runtime, jsi::Value::undefined(), &jsValue, 1); } catch (jsi::JSError& jsError) { auto stack = std::regex_replace(jsError.getStack(), std::regex("\n"), "\n "); auto message = [NSString stringWithFormat:@"Frame Processor threw an error: %s\nIn: %s", jsError.getMessage().c_str(), stack.c_str()]; - + RCTBridge* bridge = [RCTBridge currentBridge]; if (bridge != nil) { bridge.jsCallInvoker->invokeAsync([bridge, message]() { diff --git a/ios/Frame Processor/VisionCameraScheduler.h b/ios/Frame Processor/VisionCameraScheduler.h deleted file mode 100644 index 07dfb70..0000000 --- a/ios/Frame Processor/VisionCameraScheduler.h +++ /dev/null @@ -1,38 +0,0 @@ -// -// VisionCameraScheduler.h -// VisionCamera -// -// Created by Marc Rousavy on 23.07.21. -// Copyright © 2021 mrousavy. All rights reserved. -// - -#pragma once - -#include -#import - -#if __has_include() - #import -#else - // dummy placeholder - namespace reanimated { - class Scheduler { - public: - virtual void scheduleOnUI(std::function job); - protected: - std::shared_ptr jsCallInvoker_; - }; - } -#endif - -namespace vision { - -class VisionCameraScheduler : public reanimated::Scheduler { -public: - VisionCameraScheduler(std::shared_ptr jsInvoker); - virtual ~VisionCameraScheduler(); - - void scheduleOnUI(std::function job) override; -}; - -} // namespace vision diff --git a/ios/Frame Processor/VisionCameraScheduler.mm b/ios/Frame Processor/VisionCameraScheduler.mm deleted file mode 100644 index 5067d64..0000000 --- a/ios/Frame Processor/VisionCameraScheduler.mm +++ /dev/null @@ -1,39 +0,0 @@ -// -// VisionCameraScheduler.mm -// VisionCamera -// -// Created by Marc Rousavy on 23.07.21. -// Copyright © 2021 mrousavy. All rights reserved. -// - -#import -#import "VisionCameraScheduler.h" - -#import - -// Forward declarations for the Swift classes -__attribute__((objc_runtime_name("_TtC12VisionCamera12CameraQueues"))) -@interface CameraQueues : NSObject -@property (nonatomic, class, readonly, strong) dispatch_queue_t _Nonnull frameProcessorQueue; -@end - -namespace vision { - -using namespace facebook; - -VisionCameraScheduler::VisionCameraScheduler(std::shared_ptr jsInvoker) { - this->jsCallInvoker_ = jsInvoker; -} - -// does not schedule on UI thread but rather on Frame Processor Thread -void VisionCameraScheduler::scheduleOnUI(std::function job) { - dispatch_async(CameraQueues.frameProcessorQueue, ^{ - job(); - }); -} - -VisionCameraScheduler::~VisionCameraScheduler(){ -} - - -} // namespace vision diff --git a/ios/React Utils/MakeJSIRuntime.h b/ios/React Utils/MakeJSIRuntime.h deleted file mode 100644 index a428c01..0000000 --- a/ios/React Utils/MakeJSIRuntime.h +++ /dev/null @@ -1,42 +0,0 @@ -// -// MakeJSIRuntime.h -// VisionCamera -// -// Created by Marc Rousavy on 06.07.21. -// Copyright © 2021 mrousavy. All rights reserved. -// - -#pragma once - -#include -#include - -#if __has_include() - // Hermes (https://hermesengine.dev) (RN 0.65+) - #include -#elif __has_include() - // Hermes (https://hermesengine.dev) - #include -#elif __has_include() - // V8 (https://github.com/Kudo/react-native-v8) - #include -#else - // JSC - #include -#endif - -using namespace facebook; - -namespace vision { - -static std::unique_ptr makeJSIRuntime() { -#if __has_include() || __has_include() - return facebook::hermes::makeHermesRuntime(); -#elif __has_include() - return facebook::createV8Runtime(""); -#else - return facebook::jsc::makeJSCRuntime(); -#endif -} - -} // namespace vision diff --git a/ios/VisionCamera.xcodeproj/project.pbxproj b/ios/VisionCamera.xcodeproj/project.pbxproj index bda8f78..0b56817 100644 --- a/ios/VisionCamera.xcodeproj/project.pbxproj +++ b/ios/VisionCamera.xcodeproj/project.pbxproj @@ -94,7 +94,6 @@ B84760A22608EE38004C3180 /* FrameHostObject.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FrameHostObject.h; sourceTree = ""; }; B84760A52608EE7C004C3180 /* FrameHostObject.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FrameHostObject.mm; sourceTree = ""; }; B84760DE2608F57D004C3180 /* CameraQueues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraQueues.swift; sourceTree = ""; }; - B84C10592694A182006EFA70 /* MakeJSIRuntime.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MakeJSIRuntime.h; sourceTree = ""; }; B864004F27849A2400E9D2CA /* UIInterfaceOrientation+descriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIInterfaceOrientation+descriptor.swift"; sourceTree = ""; }; B86400512784A23400E9D2CA /* CameraView+Orientation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CameraView+Orientation.swift"; sourceTree = ""; }; B86DC970260E2D5200FB17B2 /* AVAudioSession+trySetAllowHaptics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVAudioSession+trySetAllowHaptics.swift"; sourceTree = ""; }; @@ -231,7 +230,6 @@ B8994E6B263F03E100069589 /* JSIUtils.mm */, B8805065266798AB00EAD7F2 /* JSConsoleHelper.h */, B8805066266798B600EAD7F2 /* JSConsoleHelper.mm */, - B84C10592694A182006EFA70 /* MakeJSIRuntime.h */, ); path = "React Utils"; sourceTree = ""; diff --git a/package.json b/package.json index 9997c39..5bd79f4 100644 --- a/package.json +++ b/package.json @@ -82,13 +82,13 @@ "react": "^18.2.0", "react-native": "^0.71.2", "react-native-builder-bob": "^0.20.3", - "react-native-reanimated": "^2.14.4", "release-it": "^15.6.0", "typescript": "^4.9.5" }, "peerDependencies": { "react": "*", - "react-native": "*" + "react-native": "*", + "react-native-worklets": "*" }, "prettier": { "bracketSpacing": true, diff --git a/scripts/cpplint.sh b/scripts/cpplint.sh index bc7ef05..4abec7c 100755 --- a/scripts/cpplint.sh +++ b/scripts/cpplint.sh @@ -1,7 +1,7 @@ #!/bin/bash if which cpplint >/dev/null; then - cpplint --linelength=230 --filter=-legal/copyright,-readability/todo,-build/namespaces,-whitespace/comments,-build/include_order,-build/c++11 --quiet --recursive --exclude "android/src/main/cpp/reanimated-headers" cpp android/src/main/cpp + cpplint --linelength=230 --filter=-legal/copyright,-readability/todo,-build/namespaces,-whitespace/comments,-build/include_order,-build/c++11 --quiet --recursive cpp android/src/main/cpp else echo "warning: cpplint not installed, download from https://github.com/cpplint/cpplint" fi diff --git a/src/Camera.tsx b/src/Camera.tsx index 68417ac..32ffb63 100644 --- a/src/Camera.tsx +++ b/src/Camera.tsx @@ -315,6 +315,14 @@ export class Camera extends React.PureComponent { } //#region Static Functions (NativeModule) + /** + * Install JSI Bindings for Frame Processors + */ + public static installFrameProcessorBindings(): void { + const result = CameraModule.installFrameProcessorBindings() as unknown; + if (result !== true) throw new Error('Failed to install Frame Processor JSI bindings!'); + } + /** * Get a list of all available camera devices on the current phone. * diff --git a/src/FrameProcessorPlugins.ts b/src/FrameProcessorPlugins.ts new file mode 100644 index 0000000..bb5363e --- /dev/null +++ b/src/FrameProcessorPlugins.ts @@ -0,0 +1,13 @@ +import type { Frame } from './Frame'; +import { Camera } from './Camera'; + +// Install VisionCamera Frame Processor JSI Bindings and Plugins +Camera.installFrameProcessorBindings(); + +type FrameProcessor = (frame: Frame, ...args: unknown[]) => unknown; +type TFrameProcessorPlugins = Record; + +/** + * All natively installed Frame Processor Plugins. + */ +export const FrameProcessorPlugins = global.FrameProcessorPlugins as TFrameProcessorPlugins; diff --git a/src/globals.d.ts b/src/globals.d.ts index a69dd25..94da873 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -1,24 +1,6 @@ /* eslint-disable no-var */ /** - * `true` if currently running in a Frame Processor runtime + * The global Frame Processor plugins registry - will be initialized after the `installFrameProcessorBindings()` call */ -declare var _FRAME_PROCESSOR: true | undefined; -/** - * `true` if currently running in a reanimated UI runtime - */ -declare var _UI: true | undefined; -/** - * `true` if currently running in a Worklet runtime (frame processor, multithreading, reanimated) - */ -declare var _WORKLET: true | undefined; - -/** - * A native logging function (outputs to Xcode console/Android Logcat) - */ -declare var _log: (message: string) => void | undefined; - -/** - * Set a Proxy for global.console in this Runtime - */ -declare var _setGlobalConsole: (console: unknown) => void; +declare var FrameProcessorPlugins: Record | undefined; diff --git a/src/hooks/useFrameProcessor.ts b/src/hooks/useFrameProcessor.ts index fbebe66..cdf314c 100644 --- a/src/hooks/useFrameProcessor.ts +++ b/src/hooks/useFrameProcessor.ts @@ -1,12 +1,10 @@ -/* global _setGlobalConsole */ - import { DependencyList, useCallback } from 'react'; import type { Frame } from '../Frame'; +// Install RN Worklets by importing it +import 'react-native-worklets/src'; type FrameProcessor = (frame: Frame) => void; -const capturableConsole = console; - /** * Returns a memoized Frame Processor function wich you can pass to the ``. (See ["Frame Processors"](https://mrousavy.github.io/react-native-vision-camera/docs/guides/frame-processors)) * @@ -25,34 +23,6 @@ const capturableConsole = console; * ``` */ export function useFrameProcessor(frameProcessor: FrameProcessor, dependencies: DependencyList): FrameProcessor { - return useCallback((frame: Frame) => { - 'worklet'; - - // @ts-expect-error - if (global.didSetConsole == null || global.didSetConsole === false) { - const console = { - // @ts-expect-error __callAsync is injected by native REA - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - debug: capturableConsole.debug.__callAsync, - // @ts-expect-error __callAsync is injected by native REA - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - log: capturableConsole.log.__callAsync, - // @ts-expect-error __callAsync is injected by native REA - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - warn: capturableConsole.warn.__callAsync, - // @ts-expect-error __callAsync is injected by native REA - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - error: capturableConsole.error.__callAsync, - // @ts-expect-error __callAsync is injected by native REA - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - info: capturableConsole.info.__callAsync, - }; - _setGlobalConsole(console); - // @ts-expect-error - global.didSetConsole = true; - } - - frameProcessor(frame); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, dependencies); + // eslint-disable-next-line react-hooks/exhaustive-deps + return useCallback(frameProcessor, dependencies); } diff --git a/src/index.ts b/src/index.ts index 80fd909..206b87f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ export * from './CameraPosition'; export * from './CameraPreset'; export * from './CameraProps'; export * from './Frame'; +export * from './FrameProcessorPlugins'; export * from './CameraProps'; export * from './PhotoFile'; export * from './Point'; diff --git a/yarn.lock b/yarn.lock index aa2f30d..06a03f3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -758,13 +758,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-transform-object-assign@^7.16.7": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-assign/-/plugin-transform-object-assign-7.18.6.tgz#7830b4b6f83e1374a5afb9f6111bcfaea872cdd2" - integrity sha512-mQisZ3JfqWh2gVXvfqYCAAyRs6+7oev+myBsTwW5RnPhYXOTuCEw2oe3YgxlXMViXUS53lG8koulI7mJ+8JE+A== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-transform-object-super@^7.0.0", "@babel/plugin-transform-object-super@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz#fb3c6ccdd15939b6ff7939944b51971ddc35912c" @@ -1034,7 +1027,7 @@ "@babel/plugin-transform-react-jsx-development" "^7.18.6" "@babel/plugin-transform-react-pure-annotations" "^7.18.6" -"@babel/preset-typescript@^7.13.0", "@babel/preset-typescript@^7.16.7", "@babel/preset-typescript@^7.17.12": +"@babel/preset-typescript@^7.13.0", "@babel/preset-typescript@^7.17.12": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.18.6.tgz#ce64be3e63eddc44240c6358daefac17b3186399" integrity sha512-s9ik86kXBAnD760aybBucdpnLsAt0jK1xqJn2juOn9lkOvSHV60os5hxoVJsPzMQxvnUJFAlkont2DvvaYEBtQ== @@ -1703,7 +1696,7 @@ dependencies: "@hapi/hoek" "^9.0.0" -"@sideway/formula@^3.0.0": +"@sideway/formula@^3.0.1": version "3.0.1" resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f" integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== @@ -3313,9 +3306,9 @@ ee-first@1.1.1: integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== electron-to-chromium@^1.4.284: - version "1.4.292" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.292.tgz#e3a3dca3780c8ce01e2c1866b5ec2fbe31c423e3" - integrity sha512-ESWOSyJy5odDlE8wvh5NNAMORv4r6assPwIPGHEMWrWD0SONXcG/xT+9aD9CQyeRwyYDPo6dJT4Bbeg5uevVQQ== + version "1.4.294" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.294.tgz#ad80317b85f0859a9454680fbc1c726fefa7e6fd" + integrity sha512-PuHZB3jEN7D8WPPjLmBQAsqQz8tWHlkkB4n0E2OYw8RwVdmBYV0Wn+rUFH8JqYyIRb4HQhhedgxlZL163wqLrQ== emoji-regex@^8.0.0: version "8.0.0" @@ -4078,7 +4071,7 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3: +get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.0.tgz#7ad1dc0535f3a2904bba075772763e5051f6d05f" integrity sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q== @@ -4611,11 +4604,11 @@ inquirer@9.1.4: wrap-ansi "^8.0.1" internal-slot@^1.0.3, internal-slot@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.4.tgz#8551e7baf74a7a6ba5f749cfb16aa60722f0d6f3" - integrity sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ== + version "1.0.5" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.5.tgz#f2a2ee21f668f8627a4667f309dc0f4fb6674986" + integrity sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ== dependencies: - get-intrinsic "^1.1.3" + get-intrinsic "^1.2.0" has "^1.0.3" side-channel "^1.0.4" @@ -5189,14 +5182,14 @@ jetifier@^2.0.0: integrity sha512-J4Au9KuT74te+PCCCHKgAjyLlEa+2VyIAEPNCdE5aNkAJ6FAJcAqcdzEkSnzNksIa9NkGmC4tPiClk2e7tCJuQ== joi@^17.2.1: - version "17.7.0" - resolved "https://registry.yarnpkg.com/joi/-/joi-17.7.0.tgz#591a33b1fe1aca2bc27f290bcad9b9c1c570a6b3" - integrity sha512-1/ugc8djfn93rTE3WRKdCzGGt/EtiYKxITMO4Wiv6q5JL1gl9ePt4kBsl1S499nbosspfctIQTpYIhSmHA3WAg== + version "17.7.1" + resolved "https://registry.yarnpkg.com/joi/-/joi-17.7.1.tgz#854fc85c7fa3cfc47c91124d30bffdbb58e06cec" + integrity sha512-teoLhIvWE298R6AeJywcjR4sX2hHjB3/xJX4qPjg+gTg+c0mzUDsziYlqPmLomq9gVsfaMcgPaGc7VxtD/9StA== dependencies: "@hapi/hoek" "^9.0.0" "@hapi/topo" "^5.0.0" "@sideway/address" "^4.1.3" - "@sideway/formula" "^3.0.0" + "@sideway/formula" "^3.0.1" "@sideway/pinpoint" "^2.0.0" js-sdsl@^4.1.4: @@ -5454,11 +5447,6 @@ lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== -lodash.isequal@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" - integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== - lodash.ismatch@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37" @@ -5979,9 +5967,9 @@ minimist-options@4.1.0: kind-of "^6.0.3" minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: - version "1.2.7" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" - integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== mixin-deep@^1.2.0: version "1.3.2" @@ -6830,9 +6818,9 @@ rc@1.2.8: strip-json-comments "~2.0.1" react-devtools-core@^4.26.1: - version "4.27.1" - resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-4.27.1.tgz#167aa174383c65786cbb7e965a5b39c702f0a2d3" - integrity sha512-qXhcxxDWiFmFAOq48jts9YQYe1+wVoUXzJTlY4jbaATzyio6dd6CUGu3dXBhREeVgpZ+y4kg6vFJzIOZh6vY2w== + version "4.27.2" + resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-4.27.2.tgz#d20fc57e258c656eedabafc2c851d38b33583148" + integrity sha512-8SzmIkpO87alD7Xr6gWIEa1jHkMjawOZ+6egjazlnjB4UUcbnzGDf/vBJ4BzGuWWEM+pzrxuzsPpcMqlQkYK2g== dependencies: shell-quote "^1.6.1" ws "^7" @@ -6894,19 +6882,6 @@ react-native-gradle-plugin@^0.71.14: resolved "https://registry.yarnpkg.com/react-native-gradle-plugin/-/react-native-gradle-plugin-0.71.14.tgz#cc399662f04fbfcc0e352d03eae1d3efbd5f635a" integrity sha512-nnLawTZEPPRAKq92UqDkzoGgCBl9aa9zAihFHMwmwzn4WRVdK4O6Cd4XYiyoNOiQzx3Hh9k5WOckHE80C92ivQ== -react-native-reanimated@^2.14.4: - version "2.14.4" - resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-2.14.4.tgz#3fa3da4e7b99f5dfb28f86bcf24d9d1024d38836" - integrity sha512-DquSbl7P8j4SAmc+kRdd75Ianm8G+IYQ9T4AQ6lrpLVeDkhZmjWI0wkutKWnp6L7c5XNVUrFDUf69dwETLCItQ== - dependencies: - "@babel/plugin-transform-object-assign" "^7.16.7" - "@babel/preset-typescript" "^7.16.7" - convert-source-map "^1.7.0" - invariant "^2.2.4" - lodash.isequal "^4.5.0" - setimmediate "^1.0.5" - string-hash-64 "^1.0.3" - react-native@^0.71.2: version "0.71.2" resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.71.2.tgz#b6977eda2a6dc10baa006bf4ab1ee08318607ce9" @@ -7431,11 +7406,6 @@ set-value@^2.0.0, set-value@^2.0.1: is-plain-object "^2.0.3" split-string "^3.0.1" -setimmediate@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" - integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== - setprototypeof@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" @@ -7725,11 +7695,6 @@ stream-buffers@2.2.x: resolved "https://registry.yarnpkg.com/stream-buffers/-/stream-buffers-2.2.0.tgz#91d5f5130d1cef96dcfa7f726945188741d09ee4" integrity sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg== -string-hash-64@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/string-hash-64/-/string-hash-64-1.0.3.tgz#0deb56df58678640db5c479ccbbb597aaa0de322" - integrity sha512-D5OKWKvDhyVWWn2x5Y9b+37NUllks34q1dCDhk/vYcso9fmhs+Tl3KR/gE4v5UNj2UA35cnX4KdVVGkG1deKqw== - string-natural-compare@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" @@ -8094,9 +8059,9 @@ type-fest@^2.13.0, type-fest@^2.5.1: integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== type-fest@^3.0.0: - version "3.5.6" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-3.5.6.tgz#f8f3a630c185fb5d66ca6950c7cbc2893deb6b84" - integrity sha512-6bd2bflx8ed7c99tc6zSTIzHr1/QG29bQoK4Qh8MYGnlPbODUzGxklLShjwc/xWQQFHgIci+y5Arv7Rbb0LjXw== + version "3.5.7" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-3.5.7.tgz#1ee9efc9a172f4002c40b896689928a7bba537f2" + integrity sha512-6J4bYzb4sdkcLBty4XW7F18VPI66M4boXNE+CY40532oq2OJe6AVMB5NmjOp6skt/jw5mRjz/hLRpuglz0U+FA== typed-array-length@^1.0.4: version "1.0.4"