From 44ed42d5d6ea9c8dd51fc792baa89d0355095633 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Fri, 21 Jul 2023 17:52:30 +0200 Subject: [PATCH] feat: Expose unified `VisionCameraProxy` object, make `FrameProcessorPlugin`s object-oriented (#1660) * feat: Replace `FrameProcessorRuntimeManager` with `VisionCameraProxy` (iOS) * Make `FrameProcessorPlugin` a constructable HostObject * fix: Fix `name` override * Simplify `useFrameProcessor * fix: Fix lint errors * Remove FrameProcessorPlugin::name * JSIUtils -> JSINSObjectConversion --- VisionCamera.podspec | 24 +- cpp/JSITypedArray.cpp | 6 +- cpp/JSITypedArray.h | 19 +- docs/docs/guides/FRAME_PROCESSORS.mdx | 44 +++- .../FRAME_PROCESSORS_CREATE_OVERVIEW.mdx | 50 ++--- docs/docs/guides/FRAME_PROCESSORS_SKIA.mdx | 110 +++++++++ .../guides/FRAME_PROCESSOR_CREATE_FINAL.mdx | 14 +- .../FRAME_PROCESSOR_CREATE_PLUGIN_ANDROID.mdx | 47 ++-- .../FRAME_PROCESSOR_CREATE_PLUGIN_IOS.mdx | 45 ++-- docs/docs/guides/TROUBLESHOOTING.mdx | 22 +- docs/static/img/slow-log-2.png | Bin 20192 -> 0 bytes docs/static/img/slow-log.png | Bin 29349 -> 0 bytes .../ExamplePluginSwift.swift | 48 ---- .../ExampleFrameProcessorPlugin.m | 14 +- example/ios/Podfile.lock | 2 +- .../project.pbxproj | 18 +- example/src/frame-processors/ExamplePlugin.ts | 23 +- ios/CameraBridge.h | 2 +- ios/CameraViewManager.swift | 11 +- ios/Frame Processor/FrameProcessor.h | 4 +- ios/Frame Processor/FrameProcessor.mm | 6 +- ios/Frame Processor/FrameProcessorPlugin.h | 24 +- ios/Frame Processor/FrameProcessorPlugin.m | 31 --- .../FrameProcessorPluginHostObject.h | 32 +++ .../FrameProcessorPluginHostObject.mm | 52 +++++ .../FrameProcessorPluginRegistry.h | 9 +- .../FrameProcessorPluginRegistry.m | 23 +- .../FrameProcessorRuntimeManager.h | 18 -- .../FrameProcessorRuntimeManager.mm | 203 ----------------- .../JSINSObjectConversion.h} | 6 +- .../JSINSObjectConversion.mm} | 4 +- ios/Frame Processor/VisionCameraProxy.h | 44 ++++ ios/Frame Processor/VisionCameraProxy.mm | 211 ++++++++++++++++++ ios/Skia Render Layer/SkiaFrameProcessor.h | 4 +- ios/Skia Render Layer/SkiaFrameProcessor.mm | 8 +- ios/VisionCamera.xcodeproj/project.pbxproj | 28 +-- src/Camera.tsx | 19 +- src/FrameProcessorPlugins.ts | 48 +++- src/JSIHelper.ts | 12 - .../withDisableFrameProcessorsIOS.ts | 23 +- src/hooks/useFrameProcessor.ts | 61 ++--- 41 files changed, 762 insertions(+), 607 deletions(-) create mode 100644 docs/docs/guides/FRAME_PROCESSORS_SKIA.mdx delete mode 100644 docs/static/img/slow-log-2.png delete mode 100644 docs/static/img/slow-log.png delete mode 100644 example/ios/Frame Processor Plugins/Example Plugin (Swift)/ExamplePluginSwift.swift rename example/ios/Frame Processor Plugins/{Example Plugin (Objective-C) => Example Plugin}/ExampleFrameProcessorPlugin.m (78%) delete mode 100644 ios/Frame Processor/FrameProcessorPlugin.m create mode 100644 ios/Frame Processor/FrameProcessorPluginHostObject.h create mode 100644 ios/Frame Processor/FrameProcessorPluginHostObject.mm delete mode 100644 ios/Frame Processor/FrameProcessorRuntimeManager.h delete mode 100644 ios/Frame Processor/FrameProcessorRuntimeManager.mm rename ios/{React Utils/JSIUtils.h => Frame Processor/JSINSObjectConversion.h} (94%) rename ios/{React Utils/JSIUtils.mm => Frame Processor/JSINSObjectConversion.mm} (99%) create mode 100644 ios/Frame Processor/VisionCameraProxy.h create mode 100644 ios/Frame Processor/VisionCameraProxy.mm diff --git a/VisionCamera.podspec b/VisionCamera.podspec index 88bdf93..86babae 100644 --- a/VisionCamera.podspec +++ b/VisionCamera.podspec @@ -10,14 +10,25 @@ while !Dir.exist?(File.join(nodeModules, "node_modules")) && tries < 10 end nodeModules = File.join(nodeModules, "node_modules") -puts("[VisionCamera] node modules #{Dir.exist?(nodeModules) ? "found at #{nodeModules}" : "not found!"}") +forceDisableFrameProcessors = false +if defined?($VCDisableFrameProcessors) + Pod::UI.puts "[VisionCamera] $VCDisableFrameProcesors is set to #{$VCDisableFrameProcessors}!" + forceDisableFrameProcessors = $VCDisableFrameProcessors +end +forceDisableSkia = false +if defined?($VCDisableSkia) + Pod::UI.puts "[VisionCamera] $VCDisableSkia is set to #{$VCDisableSkia}!" + forceDisableSkia = $VCDisableSkia +end + +Pod::UI.puts("[VisionCamera] node modules #{Dir.exist?(nodeModules) ? "found at #{nodeModules}" : "not found!"}") workletsPath = File.join(nodeModules, "react-native-worklets") -hasWorklets = File.exist?(workletsPath) -puts "[VisionCamera] react-native-worklets #{hasWorklets ? "found" : "not found"}, Frame Processors #{hasWorklets ? "enabled" : "disabled"}!" +hasWorklets = File.exist?(workletsPath) && !forceDisableFrameProcessors +Pod::UI.puts("[VisionCamera] react-native-worklets #{hasWorklets ? "found" : "not found"}, Frame Processors #{hasWorklets ? "enabled" : "disabled"}!") skiaPath = File.join(nodeModules, "@shopify", "react-native-skia") -hasSkia = hasWorklets && File.exist?(skiaPath) -puts "[VisionCamera] react-native-skia #{hasSkia ? "found" : "not found"}, Skia Frame Processors #{hasSkia ? "enabled" : "disabled"}!" +hasSkia = hasWorklets && File.exist?(skiaPath) && !forceDisableSkia +Pod::UI.puts("[VisionCamera] react-native-skia #{hasSkia ? "found" : "not found"}, Skia Frame Processors #{hasSkia ? "enabled" : "disabled"}!") Pod::Spec.new do |s| s.name = "VisionCamera" @@ -54,8 +65,9 @@ Pod::Spec.new do |s| hasWorklets ? "ios/Frame Processor/*.{m,mm,swift}" : "", hasWorklets ? "ios/Frame Processor/Frame.h" : "", hasWorklets ? "ios/Frame Processor/FrameProcessor.h" : "", - hasWorklets ? "ios/Frame Processor/FrameProcessorRuntimeManager.h" : "", hasWorklets ? "ios/Frame Processor/FrameProcessorPlugin.h" : "", + hasWorklets ? "ios/Frame Processor/FrameProcessorPluginRegistry.h" : "", + hasWorklets ? "ios/Frame Processor/VisionCameraProxy.h" : "", hasWorklets ? "cpp/**/*.{cpp}" : "", # Skia Frame Processors diff --git a/cpp/JSITypedArray.cpp b/cpp/JSITypedArray.cpp index aedd71e..9b81069 100644 --- a/cpp/JSITypedArray.cpp +++ b/cpp/JSITypedArray.cpp @@ -73,10 +73,8 @@ class PropNameIDCache { PropNameIDCache propNameIDCache; -InvalidateCacheOnDestroy::InvalidateCacheOnDestroy(jsi::Runtime &runtime) { - key = reinterpret_cast(&runtime); -} -InvalidateCacheOnDestroy::~InvalidateCacheOnDestroy() { +void invalidateArrayBufferCache(jsi::Runtime& runtime) { + auto key = reinterpret_cast(&runtime); propNameIDCache.invalidate(key); } diff --git a/cpp/JSITypedArray.h b/cpp/JSITypedArray.h index eb470da..9e5639e 100644 --- a/cpp/JSITypedArray.h +++ b/cpp/JSITypedArray.h @@ -74,24 +74,7 @@ struct typedArrayTypeMap { typedef double type; }; -// Instance of this class will invalidate PropNameIDCache when destructor is called. -// Attach this object to global in specific jsi::Runtime to make sure lifecycle of -// the cache object is connected to the lifecycle of the js runtime -class InvalidateCacheOnDestroy : public jsi::HostObject { - public: - explicit InvalidateCacheOnDestroy(jsi::Runtime &runtime); - virtual ~InvalidateCacheOnDestroy(); - virtual jsi::Value get(jsi::Runtime &, const jsi::PropNameID &name) { - return jsi::Value::null(); - } - virtual void set(jsi::Runtime &, const jsi::PropNameID &name, const jsi::Value &value) {} - virtual std::vector getPropertyNames(jsi::Runtime &rt) { - return {}; - } - - private: - uintptr_t key; -}; +void invalidateArrayBufferCache(jsi::Runtime& runtime); class TypedArrayBase : public jsi::Object { public: diff --git a/docs/docs/guides/FRAME_PROCESSORS.mdx b/docs/docs/guides/FRAME_PROCESSORS.mdx index a9d9e6a..f1337d7 100644 --- a/docs/docs/guides/FRAME_PROCESSORS.mdx +++ b/docs/docs/guides/FRAME_PROCESSORS.mdx @@ -120,6 +120,36 @@ const frameProcessor = useFrameProcessor((frame) => { }, [onQRCodeDetected]) ``` +### Running asynchronously + +Since Frame Processors run synchronously with the Camera Pipeline, anything taking longer than one Frame interval might block the Camera from streaming new Frames. To avoid this, you can use `runAsync` to run code asynchronously on a different Thread: + +```ts +const frameProcessor = useFrameProcessor((frame) => { + 'worklet' + console.log('I'm running synchronously at 60 FPS!') + runAsync(() => { + 'worklet' + console.log('I'm running asynchronously, possibly at a lower FPS rate!') + }) +}, []) +``` + +### Running at a throttled FPS rate + +Some Frame Processor Plugins don't need to run on every Frame, for example a Frame Processor that detects the brightness in a Frame only needs to run twice per second: + +```ts +const frameProcessor = useFrameProcessor((frame) => { + 'worklet' + console.log('I'm running synchronously at 60 FPS!') + runAtTargetFps(2, () => { + 'worklet' + console.log('I'm running synchronously at 2 FPS!') + }) +}, []) +``` + ### Using Frame Processor Plugins Frame Processor Plugins are distributed through npm. To install the [**vision-camera-image-labeler**](https://github.com/mrousavy/vision-camera-image-labeler) plugin, run: @@ -204,7 +234,7 @@ The Frame Processor API spawns a secondary JavaScript Runtime which consumes a s Inside your `gradle.properties` file, add the `disableFrameProcessors` flag: -``` +```groovy disableFrameProcessors=true ``` @@ -212,18 +242,12 @@ Then, clean and rebuild your project. #### iOS -Inside your `project.pbxproj`, find the `GCC_PREPROCESSOR_DEFINITIONS` group and add the flag: +Inside your `Podfile`, add the `VCDisableFrameProcessors` flag: -```txt {3} -GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "VISION_CAMERA_DISABLE_FRAME_PROCESSORS=1", - "$(inherited)", -); +```ruby +$VCDisableFrameProcessors = true ``` -Make sure to add this to your Debug-, as well as your Release-configuration. - diff --git a/docs/docs/guides/FRAME_PROCESSORS_CREATE_OVERVIEW.mdx b/docs/docs/guides/FRAME_PROCESSORS_CREATE_OVERVIEW.mdx index e22f472..e94f8b7 100644 --- a/docs/docs/guides/FRAME_PROCESSORS_CREATE_OVERVIEW.mdx +++ b/docs/docs/guides/FRAME_PROCESSORS_CREATE_OVERVIEW.mdx @@ -12,14 +12,14 @@ import TabItem from '@theme/TabItem'; Frame Processor Plugins are **native functions** which can be directly called from a JS Frame Processor. (See ["Frame Processors"](frame-processors)) -They **receive a frame from the Camera** as an input and can return any kind of output. For example, a `scanQRCodes` function returns an array of detected QR code strings in the frame: +They **receive a frame from the Camera** as an input and can return any kind of output. For example, a `detectFaces` function returns an array of detected faces in the frame: ```tsx {4-5} function App() { const frameProcessor = useFrameProcessor((frame) => { 'worklet' - const qrCodes = scanQRCodes(frame) - console.log(`QR Codes in Frame: ${qrCodes}`) + const faces = detectFaces(frame) + console.log(`Faces in Frame: ${faces}`) }, []) return ( @@ -28,7 +28,7 @@ function App() { } ``` -To achieve **maximum performance**, the `scanQRCodes` function is written in a native language (e.g. Objective-C), but it will be directly called from the VisionCamera Frame Processor JavaScript-Runtime. +To achieve **maximum performance**, the `detectFaces` function is written in a native language (e.g. Objective-C), but it will be directly called from the VisionCamera Frame Processor JavaScript-Runtime. ### Types @@ -43,7 +43,7 @@ Similar to a TurboModule, the Frame Processor Plugin Registry API automatically | `{}` | `NSDictionary*` | `ReadableNativeMap` | | `undefined` / `null` | `nil` | `null` | | `(any, any) => void` | [`RCTResponseSenderBlock`][4] | `(Object, Object) -> void` | -| [`Frame`][1] | [`Frame*`][2] | [`ImageProxy`][3] | +| [`Frame`][1] | [`Frame*`][2] | [`Frame`][3] | ### Return values @@ -51,7 +51,7 @@ Return values will automatically be converted to JS values, assuming they are re ```java @Override -public Object callback(ImageProxy image, Object[] params) { +public Object callback(Frame frame, Object[] params) { return "cat"; } ``` @@ -66,13 +66,13 @@ export function detectObject(frame: Frame): string { } ``` -You can also manipulate the buffer and return it (or a copy of it) by returning a [`Frame`][2]/[`ImageProxy`][3] instance: +You can also manipulate the buffer and return it (or a copy of it) by returning a [`Frame`][2]/[`Frame`][3] instance: ```java @Override -public Object callback(ImageProxy image, Object[] params) { - ImageProxy resizedImage = new ImageProxy(/* ... */); - return resizedImage; +public Object callback(Frame frame, Object[] params) { + Frame resizedFrame = new Frame(/* ... */); + return resizedFrame; } ``` @@ -97,16 +97,7 @@ Frame Processors can also accept parameters, following the same type convention ```ts const frameProcessor = useFrameProcessor((frame) => { 'worklet' - const codes = scanCodes(frame, ['qr', 'barcode']) -}, []) -``` - -Or with multiple ("variadic") parameters: - -```ts -const frameProcessor = useFrameProcessor((frame) => { - 'worklet' - const codes = scanCodes(frame, true, 'hello-world', 42) + const codes = scanCodes(frame, { codes: ['qr', 'barcode'] }) }, []) ``` @@ -116,7 +107,7 @@ To let the user know that something went wrong you can use Exceptions: ```java @Override -public Object callback(ImageProxy image, Object[] params) { +public Object callback(Frame frame, Object[] params) { if (params[0] instanceof String) { // ... } else { @@ -152,13 +143,13 @@ For example, a realtime video chat application might use WebRTC to send the fram ```java @Override -public Object callback(ImageProxy image, Object[] params) { +public Object callback(Frame frame, Object[] params) { String serverURL = (String)params[0]; - ImageProxy imageCopy = new ImageProxy(/* ... */); + Frame frameCopy = new Frame(/* ... */); uploaderQueue.runAsync(() -> { - WebRTC.uploadImage(imageCopy, serverURL); - imageCopy.close(); + WebRTC.uploadImage(frameCopy, serverURL); + frameCopy.close(); }); return null; @@ -195,14 +186,7 @@ This way you can handle queueing up the frames yourself and asynchronously call ### Benchmarking Frame Processor Plugins -Your Frame Processor Plugins have to be fast. VisionCamera automatically detects slow Frame Processors and outputs relevant information in the native console (Xcode: **Debug Area**, Android Studio: **Logcat**): - -
- -
-
- -
+Your Frame Processor Plugins have to be fast. Use the FPS Graph (`enableFpsGraph`) to see how fast your Camera is running, if it is not running at the target FPS, your Frame Processor is too slow.
diff --git a/docs/docs/guides/FRAME_PROCESSORS_SKIA.mdx b/docs/docs/guides/FRAME_PROCESSORS_SKIA.mdx new file mode 100644 index 0000000..6addd34 --- /dev/null +++ b/docs/docs/guides/FRAME_PROCESSORS_SKIA.mdx @@ -0,0 +1,110 @@ +--- +id: frame-processors-skia +title: Skia Frame Processors +sidebar_label: Skia Frame Processors +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; +import useBaseUrl from '@docusaurus/useBaseUrl'; + +
+ + + + +
+ +### What are Skia Frame Processors? + +Skia Frame Processors are [Frame Processors](frame-processors) that allow you to draw onto the Frame using [react-native-skia](https://github.com/Shopify/react-native-skia). + +For example, you might want to draw a rectangle around a user's face **without writing any native code**, while still **achieving native performance**: + +```jsx +function App() { + const frameProcessor = useSkiaFrameProcessor((frame) => { + 'worklet' + const faces = detectFaces(frame) + faces.forEach((face) => { + frame.drawRect(face.rectangle, redPaint) + }) + }, []) + + return ( + + ) +} +``` + +With Skia, you can also implement realtime filters, blurring, shaders, and much more. For example, this is how you invert the colors in a Frame: + +```jsx +const INVERTED_COLORS_SHADER = ` +uniform shader image; + +half4 main(vec2 pos) { + vec4 color = image.eval(pos); + return vec4(1.0 - color.rgb, 1.0); +} +`; + +function App() { + const imageFilter = Skia.ImageFilter.MakeRuntimeShader(/* INVERTED_COLORS_SHADER */) + const paint = Skia.Paint() + paint.setImageFilter(imageFilter) + + const frameProcessor = useSkiaFrameProcessor((frame) => { + 'worklet' + frame.render(paint) + }, []) + + return ( + + ) +} +``` + +### Rendered outputs + +The rendered results of the Skia Frame Processor are rendered to an offscreen context and will be displayed in the Camera Preview, recorded to a video file (`startRecording()`) and captured in a photo (`takePhoto()`). In other words, you draw into the Frame, not just ontop of it. + +### Performance + +VisionCamera sets up an additional Skia rendering context which requires a few resources. + +On iOS, Metal is used for GPU Acceleration. On Android, OpenGL is used for GPU Acceleration. +C++/JSI is used for highly efficient communication between JS and Skia. + +### Disabling Skia Frame Processors + +Skia Frame Processors ship with additional C++ files which might slightly increase the app's build time. If you're not using Skia Frame Processors at all, you can disable them: + +#### Android + +Inside your `gradle.properties` file, add the `disableSkia` flag: + +```groovy +disableSkia=true +``` + +Then, clean and rebuild your project. + +#### iOS + +Inside your `Podfile`, add the `VCDisableSkia` flag: + +```ruby +$VCDisableSkia = true +``` + + +
+ +#### 🚀 Next section: [Zooming](/docs/guides/zooming) (or [creating a Frame Processor Plugin](/docs/guides/frame-processors-plugins-overview)) diff --git a/docs/docs/guides/FRAME_PROCESSOR_CREATE_FINAL.mdx b/docs/docs/guides/FRAME_PROCESSOR_CREATE_FINAL.mdx index 80f7c3e..1467ca7 100644 --- a/docs/docs/guides/FRAME_PROCESSOR_CREATE_FINAL.mdx +++ b/docs/docs/guides/FRAME_PROCESSOR_CREATE_FINAL.mdx @@ -9,14 +9,16 @@ 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 { FrameProcessorPlugins, Frame } from 'react-native-vision-camera' +import { VisionCameraProxy, Frame } from 'react-native-vision-camera' + +const plugin = VisionCameraProxy.getFrameProcessorPlugin('scanFaces') /** - * Scans QR codes. + * Scans faces. */ -export function scanQRCodes(frame: Frame): string[] { +export function scanFaces(frame: Frame): object { 'worklet' - return FrameProcessorPlugins.scanQRCodes(frame) + return plugin.call(frame) } ``` @@ -28,8 +30,8 @@ Simply call the wrapper Worklet in your Frame Processor: function App() { const frameProcessor = useFrameProcessor((frame) => { 'worklet' - const qrCodes = scanQRCodes(frame) - console.log(`QR Codes in Frame: ${qrCodes}`) + const faces = scanFaces(frame) + console.log(`Faces in Frame: ${faces}`) }, []) return ( diff --git a/docs/docs/guides/FRAME_PROCESSOR_CREATE_PLUGIN_ANDROID.mdx b/docs/docs/guides/FRAME_PROCESSOR_CREATE_PLUGIN_ANDROID.mdx index 63bf916..d6667bf 100644 --- a/docs/docs/guides/FRAME_PROCESSOR_CREATE_PLUGIN_ANDROID.mdx +++ b/docs/docs/guides/FRAME_PROCESSOR_CREATE_PLUGIN_ANDROID.mdx @@ -10,7 +10,7 @@ import TabItem from '@theme/TabItem'; ## Creating a Frame Processor Plugin for Android The Frame Processor Plugin API is built to be as extensible as possible, which allows you to create custom Frame Processor Plugins. -In this guide we will create a custom QR Code Scanner Plugin which can be used from JS. +In this guide we will create a custom Face Detector Plugin which can be used from JS. Android Frame Processor Plugins can be written in either **Java**, **Kotlin** or **C++ (JNI)**. @@ -23,7 +23,7 @@ npx vision-camera-plugin-builder android ``` :::info -The CLI will ask you for the path to project's Android Manifest file, name of the plugin (e.g. `QRCodeFrameProcessor`), name of the exposed method (e.g. `scanQRCodes`) and language you want to use for plugin development (Java or Kotlin). +The CLI will ask you for the path to project's Android Manifest file, name of the plugin (e.g. `FaceDetectorFrameProcessorPlugin`), name of the exposed method (e.g. `detectFaces`) and language you want to use for plugin development (Java or Kotlin). For reference see the [CLI's docs](https://github.com/mateusz1913/vision-camera-plugin-builder#%EF%B8%8F-options). ::: @@ -35,7 +35,7 @@ For reference see the [CLI's docs](https://github.com/mateusz1913/vision-camera- @SuppressWarnings("UnnecessaryLocalVariable") List packages = new PackageList(this).getPackages(); ... - packages.add(new QRCodeFrameProcessorPluginPackage()); // <- add + packages.add(new FaceDetectorFrameProcessorPluginPackage()); // <- add return packages; } ``` @@ -51,33 +51,34 @@ For reference see the [CLI's docs](https://github.com/mateusz1913/vision-camera- 1. Open your Project in Android Studio -2. Create a Java source file, for the QR Code Plugin this will be called `QRCodeFrameProcessorPlugin.java`. +2. Create a Java source file, for the Face Detector Plugin this will be called `FaceDetectorFrameProcessorPlugin.java`. 3. Add the following code: ```java {8} import androidx.camera.core.ImageProxy; import com.mrousavy.camera.frameprocessor.FrameProcessorPlugin; -public class QRCodeFrameProcessorPlugin extends FrameProcessorPlugin { +public class FaceDetectorFrameProcessorPlugin extends FrameProcessorPlugin { @Override - public Object callback(ImageProxy image, Object[] params) { + public Object callback(ImageProxy image, ReadableNativeMap arguments) { // code goes here return null; } - QRCodeFrameProcessorPlugin() { - super("scanQRCodes"); + @Override + public String getName() { + return "detectFaces"; } } ``` :::note -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(...)`. +The Frame Processor Plugin will be exposed to JS through the `VisionCameraProxy` object. In this case, it would be `VisionCameraProxy.getFrameProcessorPlugin("detectFaces")`. ::: 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. -5. Create a new Java file which registers the Frame Processor Plugin in a React Package, for the QR Code Scanner plugin this file will be called `QRCodeFrameProcessorPluginPackage.java`: +5. Create a new Java file which registers the Frame Processor Plugin in a React Package, for the Face Detector plugin this file will be called `FaceDetectorFrameProcessorPluginPackage.java`: ```java {12} import com.facebook.react.ReactPackage; @@ -87,11 +88,11 @@ import com.facebook.react.uimanager.ViewManager; import com.mrousavy.camera.frameprocessor.FrameProcessorPlugin; import javax.annotation.Nonnull; -public class QRCodeFrameProcessorPluginPackage implements ReactPackage { +public class FaceDetectorFrameProcessorPluginPackage implements ReactPackage { @NonNull @Override public List createNativeModules(@NonNull ReactApplicationContext reactContext) { - FrameProcessorPlugin.register(new QRCodeFrameProcessorPlugin()); + FrameProcessorPlugin.register(new FaceDetectorFrameProcessorPlugin()); return Collections.emptyList(); } @@ -111,7 +112,7 @@ public class QRCodeFrameProcessorPluginPackage implements ReactPackage { @SuppressWarnings("UnnecessaryLocalVariable") List packages = new PackageList(this).getPackages(); ... - packages.add(new QRCodeFrameProcessorPluginPackage()); // <- add + packages.add(new FaceDetectorFrameProcessorPluginPackage()); // <- add return packages; } ``` @@ -120,28 +121,32 @@ public class QRCodeFrameProcessorPluginPackage implements ReactPackage { 1. Open your Project in Android Studio -2. Create a Kotlin source file, for the QR Code Plugin this will be called `QRCodeFrameProcessorPlugin.kt`. +2. Create a Kotlin source file, for the Face Detector Plugin this will be called `FaceDetectorFrameProcessorPlugin.kt`. 3. Add the following code: ```kotlin {7} import androidx.camera.core.ImageProxy import com.mrousavy.camera.frameprocessor.FrameProcessorPlugin -class ExampleFrameProcessorPluginKotlin: FrameProcessorPlugin("scanQRCodes") { +class FaceDetectorFrameProcessorPlugin: FrameProcessorPlugin() { - override fun callback(image: ImageProxy, params: Array): Any? { + override fun callback(image: ImageProxy, arguments: ReadableNativeMap): Any? { // code goes here return null } + + override fun getName(): String { + return "detectFaces" + } } ``` :::note -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(...)`. +The Frame Processor Plugin will be exposed to JS through the `VisionCameraProxy` object. In this case, it would be `VisionCameraProxy.getFrameProcessorPlugin("detectFaces")`. ::: 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. -5. Create a new Kotlin file which registers the Frame Processor Plugin in a React Package, for the QR Code Scanner plugin this file will be called `QRCodeFrameProcessorPluginPackage.kt`: +5. Create a new Kotlin file which registers the Frame Processor Plugin in a React Package, for the Face Detector plugin this file will be called `FaceDetectorFrameProcessorPluginPackage.kt`: ```kotlin {9} import com.facebook.react.ReactPackage @@ -150,9 +155,9 @@ import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.uimanager.ViewManager import com.mrousavy.camera.frameprocessor.FrameProcessorPlugin -class QRCodeFrameProcessorPluginPackage : ReactPackage { +class FaceDetectorFrameProcessorPluginPackage : ReactPackage { override fun createNativeModules(reactContext: ReactApplicationContext): List { - FrameProcessorPlugin.register(ExampleFrameProcessorPluginKotlin()) + FrameProcessorPlugin.register(FaceDetectorFrameProcessorPlugin()) return emptyList() } @@ -170,7 +175,7 @@ class QRCodeFrameProcessorPluginPackage : ReactPackage { @SuppressWarnings("UnnecessaryLocalVariable") List packages = new PackageList(this).getPackages(); ... - packages.add(new QRCodeFrameProcessorPluginPackage()); // <- add + packages.add(new FaceDetectorFrameProcessorPluginPackage()); // <- add return packages; } ``` diff --git a/docs/docs/guides/FRAME_PROCESSOR_CREATE_PLUGIN_IOS.mdx b/docs/docs/guides/FRAME_PROCESSOR_CREATE_PLUGIN_IOS.mdx index fd31aa6..b36448a 100644 --- a/docs/docs/guides/FRAME_PROCESSOR_CREATE_PLUGIN_IOS.mdx +++ b/docs/docs/guides/FRAME_PROCESSOR_CREATE_PLUGIN_IOS.mdx @@ -10,7 +10,7 @@ import TabItem from '@theme/TabItem'; ## Creating a Frame Processor Plugin for iOS The Frame Processor Plugin API is built to be as extensible as possible, which allows you to create custom Frame Processor Plugins. -In this guide we will create a custom QR Code Scanner Plugin which can be used from JS. +In this guide we will create a custom Face Detector Plugin which can be used from JS. iOS Frame Processor Plugins can be written in either **Objective-C** or **Swift**. @@ -23,7 +23,7 @@ npx vision-camera-plugin-builder ios ``` :::info -The CLI will ask you for the path to project's .xcodeproj file, name of the plugin (e.g. `QRCodeFrameProcessor`), name of the exposed method (e.g. `scanQRCodes`) and language you want to use for plugin development (Objective-C, Objective-C++ or Swift). +The CLI will ask you for the path to project's .xcodeproj file, name of the plugin (e.g. `FaceDetectorFrameProcessorPlugin`), name of the exposed method (e.g. `detectFaces`) and language you want to use for plugin development (Objective-C, Objective-C++ or Swift). For reference see the [CLI's docs](https://github.com/mateusz1913/vision-camera-plugin-builder#%EF%B8%8F-options). ::: @@ -38,23 +38,25 @@ For reference see the [CLI's docs](https://github.com/mateusz1913/vision-camera- 1. Open your Project in Xcode -2. Create an Objective-C source file, for the QR Code Plugin this will be called `QRCodeFrameProcessorPlugin.m`. +2. Create an Objective-C source file, for the Face Detector Plugin this will be called `FaceDetectorFrameProcessorPlugin.m`. 3. Add the following code: ```objc #import +#import #import -@interface QRCodeFrameProcessorPlugin : FrameProcessorPlugin +@interface FaceDetectorFrameProcessorPlugin : FrameProcessorPlugin @end -@implementation QRCodeFrameProcessorPlugin +@implementation FaceDetectorFrameProcessorPlugin -- (NSString *)name { - return @"scanQRCodes"; +- (instancetype) initWithOptions:(NSDictionary*)options; { + self = [super init]; + return self; } -- (id)callback:(Frame *)frame withArguments:(NSArray *)arguments { +- (id)callback:(Frame*)frame withArguments:(NSDictionary*)arguments { CMSampleBufferRef buffer = frame.buffer; UIImageOrientation orientation = frame.orientation; // code goes here @@ -62,14 +64,17 @@ For reference see the [CLI's docs](https://github.com/mateusz1913/vision-camera- } + (void) load { - [self registerPlugin:[[ExampleFrameProcessorPlugin alloc] init]]; + [FrameProcessorPluginRegistry addFrameProcessorPlugin:@"detectFaces" + withInitializer:^FrameProcessorPlugin*(NSDictionary* options) { + return [[FaceDetectorFrameProcessorPlugin alloc] initWithOptions:options]; + }]; } @end ``` :::note -The Frame Processor Plugin will be exposed to JS through the `FrameProcessorPlugins` object using the name returned from the `name` getter. In this case, it would be `FrameProcessorPlugins.scanQRCodes(...)`. +The Frame Processor Plugin will be exposed to JS through the `VisionCameraProxy` object. In this case, it would be `VisionCameraProxy.getFrameProcessorPlugin("detectFaces")`. ::: 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. @@ -78,7 +83,7 @@ The Frame Processor Plugin will be exposed to JS through the `FrameProcessorPlug 1. Open your Project in Xcode -2. Create a Swift file, for the QR Code Plugin this will be `QRCodeFrameProcessorPlugin.swift`. If Xcode asks you to create a Bridging Header, press **create**. +2. Create a Swift file, for the Face Detector Plugin this will be `FaceDetectorFrameProcessorPlugin.swift`. If Xcode asks you to create a Bridging Header, press **create**. ![Xcode "Create Bridging Header" alert](https://docs-assets.developer.apple.com/published/7ebca7212c/2a065d1a-7e53-4907-a889-b7fa4f2206c9.png) @@ -92,13 +97,9 @@ The Frame Processor Plugin will be exposed to JS through the `FrameProcessorPlug 4. In the Swift file, add the following code: ```swift -@objc(QRCodeFrameProcessorPlugin) -public class QRCodeFrameProcessorPlugin: FrameProcessorPlugin { - override public func name() -> String! { - return "scanQRCodes" - } - - public override func callback(_ frame: Frame!, withArguments arguments: [Any]!) -> Any! { +@objc(FaceDetectorFrameProcessorPlugin) +public class FaceDetectorFrameProcessorPlugin: FrameProcessorPlugin { + public override func callback(_ frame: Frame!, withArguments arguments: [String:Any]) -> Any { let buffer = frame.buffer let orientation = frame.orientation // code goes here @@ -107,11 +108,12 @@ public class QRCodeFrameProcessorPlugin: FrameProcessorPlugin { } ``` -5. In your `AppDelegate.m`, add the following imports (you can skip this if your AppDelegate is in Swift): +5. In your `AppDelegate.m`, add the following imports: ```objc #import "YOUR_XCODE_PROJECT_NAME-Swift.h" #import +#import ``` 6. In your `AppDelegate.m`, add the following code to `application:didFinishLaunchingWithOptions:`: @@ -121,7 +123,10 @@ public class QRCodeFrameProcessorPlugin: FrameProcessorPlugin { { ... - [FrameProcessorPlugin registerPlugin:[[QRCodeFrameProcessorPlugin alloc] init]]; + [FrameProcessorPluginRegistry addFrameProcessorPlugin:@"detectFaces" + withInitializer:^FrameProcessorPlugin*(NSDictionary* options) { + return [[FaceDetectorFrameProcessorPlugin alloc] initWithOptions:options]; + }]; return [super application:application didFinishLaunchingWithOptions:launchOptions]; } diff --git a/docs/docs/guides/TROUBLESHOOTING.mdx b/docs/docs/guides/TROUBLESHOOTING.mdx index d8624dd..4320032 100644 --- a/docs/docs/guides/TROUBLESHOOTING.mdx +++ b/docs/docs/guides/TROUBLESHOOTING.mdx @@ -21,10 +21,10 @@ Before opening an issue, make sure you try the following: npm i # or "yarn" cd ios && pod repo update && pod update && pod install ``` -2. Check your minimum iOS version. VisionCamera requires a minimum iOS version of **11.0**. +2. Check your minimum iOS version. VisionCamera requires a minimum iOS version of **12.4**. 1. Open your `Podfile` - 2. Make sure `platform :ios` is set to `11.0` or higher - 3. Make sure `iOS Deployment Target` is set to `11.0` or higher (`IPHONEOS_DEPLOYMENT_TARGET` in `project.pbxproj`) + 2. Make sure `platform :ios` is set to `12.4` or higher + 3. Make sure `iOS Deployment Target` is set to `12.4` or higher (`IPHONEOS_DEPLOYMENT_TARGET` in `project.pbxproj`) 3. Check your Swift version. VisionCamera requires a minimum Swift version of **5.2**. 1. Open `project.pbxproj` in a Text Editor 2. If the `LIBRARY_SEARCH_PATH` value is set, make sure there is no explicit reference to Swift-5.0. If there is, remove it. See [this StackOverflow answer](https://stackoverflow.com/a/66281846/1123156). @@ -35,9 +35,12 @@ Before opening an issue, make sure you try the following: 3. Select **Swift File** and press **Next** 4. Choose whatever name you want, e.g. `File.swift` and press **Create** 5. Press **Create Bridging Header** when promted. -5. If you're having runtime issues, check the logs in Xcode to find out more. In Xcode, go to **View** > **Debug Area** > **Activate Console** (⇧+⌘+C). +5. If you're having build issues, try: + 1. Building without Skia. Set `$VCDisableSkia = true` in the top of your Podfile, and try rebuilding. + 2. Building without Frame Processors. Set `$VCDisableFrameProcessors = true` in the top of your Podfile, and try rebuilding. +6. If you're having runtime issues, check the logs in Xcode to find out more. In Xcode, go to **View** > **Debug Area** > **Activate Console** (⇧+⌘+C). * For errors without messages, there's often an error code attached. Look up the error code on [osstatus.com](https://www.osstatus.com) to get more information about a specific error. -6. If your Frame Processor is not running, make sure you check the native Xcode logs to find out why. Also make sure you are not using a remote JS debugger such as Google Chrome, since those don't work with JSI. +7. If your Frame Processor is not running, make sure you check the native Xcode logs to find out why. Also make sure you are not using a remote JS debugger such as Google Chrome, since those don't work with JSI. ## Android @@ -64,9 +67,12 @@ Before opening an issue, make sure you try the following: ``` distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip ``` -5. If you're having runtime issues, check the logs in Android Studio/Logcat to find out more. In Android Studio, go to **View** > **Tool Windows** > **Logcat** (⌘+6) or run `adb logcat` in Terminal. -6. If a camera device is not being returned by [`Camera.getAvailableCameraDevices()`](/docs/api/classes/Camera#getavailablecameradevices), make sure it is a Camera2 compatible device. See [this section in the Android docs](https://developer.android.com/reference/android/hardware/camera2/CameraDevice#reprocessing) for more information. -7. If your Frame Processor is not running, make sure you check the native Android Studio/Logcat logs to find out why. Also make sure you are not using a remote JS debugger such as Google Chrome, since those don't work with JSI. +5. If you're having build issues, try: + 1. Building without Skia. Set `disableSkia = true` in your `gradle.properties`, and try rebuilding. + 2. Building without Frame Processors. Set `disableFrameProcessors = true` in your `gradle.properties`, and try rebuilding. +6. If you're having runtime issues, check the logs in Android Studio/Logcat to find out more. In Android Studio, go to **View** > **Tool Windows** > **Logcat** (⌘+6) or run `adb logcat` in Terminal. +7. If a camera device is not being returned by [`Camera.getAvailableCameraDevices()`](/docs/api/classes/Camera#getavailablecameradevices), make sure it is a Camera2 compatible device. See [this section in the Android docs](https://developer.android.com/reference/android/hardware/camera2/CameraDevice#reprocessing) for more information. +8. If your Frame Processor is not running, make sure you check the native Android Studio/Logcat logs to find out why. Also make sure you are not using a remote JS debugger such as Google Chrome, since those don't work with JSI. ## Issues diff --git a/docs/static/img/slow-log-2.png b/docs/static/img/slow-log-2.png deleted file mode 100644 index 3777c9191277b247c5eb3bd17996448663c9fa3f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20192 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
    eGQw_j2cmEmIh0jnfUFr^P47?}j zr(=78UNc+VRZ`15mXrAEx|0Dm<8LN5n8XIX#?S70gGwChZnX{aXp~p9yuyE=Pg!WJz z1I0g|e|l$afau?*>pAfco0KxSM6A(wG*vzVK0Pv8xpM0V>{z|5(%0mYulG?xU8Q%u zkf*+pOV)T*k&?slpKUR>>TAgz%dlSs+iKo%zz8pK^Hs3tWZ*(4i@K%+jzlGW_42I6 zt#mWRsE$PRc*;<+;X_;b@_k6N!*zO;_1I#-z0JbB$=P*eQ~25<2CMQ#r+B_eFWz0C z*9)CGlBt_B@44*i1fd7%gU+)LW1hy+Wg7i59nVD@)|Vn@!?W+GQT>6cA+>>etBp5MjZE_8*J+Y<##6@CF(_5Wkjb|6oCIUql-1D*OR*T`@ujWi-8bnatx4HyhR5H|2^-Gz zKNVanldYZMP!Ux$Io9c#P=%K&YI0nqe{V}hb7L#NxeS_*x+u{bv>qMfgDg7J8Y$mn z*;|GOA`w2^1$w$X+)1WgL7d9{l^Dnee9uUp)nodGl!dAmvj@=|yN2kcd<8+((axSVt!?q^EAYvc=u2#?kQ*ns$5Kv+Pk(TC};a*DFHvOYp zy6k147Fng;9G~Crq_PY1OSdGBsxhUvJ=?Yo#!(9@e{{NelL4u* zRYyX90HT9`^QXIQ{Vy*Bp4Ma);V3$0adrZjrQT06@V|w?|5cVVSY0gh;eS!1(LTfa UIwohI&MH)rQ String! { - return "example_plugin_swift" - } - - public override func callback(_ frame: Frame!, withArguments arguments: [Any]!) -> Any! { - guard let imageBuffer = CMSampleBufferGetImageBuffer(frame.buffer) else { - return nil - } - NSLog("ExamplePlugin: \(CVPixelBufferGetWidth(imageBuffer)) x \(CVPixelBufferGetHeight(imageBuffer)) Image. Logging \(arguments.count) parameters:") - - arguments.forEach { arg in - var string = "\(arg)" - if let array = arg as? NSArray { - string = (array as Array).description - } else if let map = arg as? NSDictionary { - string = (map as Dictionary).description - } - NSLog("ExamplePlugin: -> \(string) (\(type(of: arg)))") - } - - return [ - "example_str": "Test", - "example_bool": true, - "example_double": 5.3, - "example_array": [ - "Hello", - true, - 17.38, - ], - ] - } -} -#endif diff --git a/example/ios/Frame Processor Plugins/Example Plugin (Objective-C)/ExampleFrameProcessorPlugin.m b/example/ios/Frame Processor Plugins/Example Plugin/ExampleFrameProcessorPlugin.m similarity index 78% rename from example/ios/Frame Processor Plugins/Example Plugin (Objective-C)/ExampleFrameProcessorPlugin.m rename to example/ios/Frame Processor Plugins/Example Plugin/ExampleFrameProcessorPlugin.m index ba2cf7d..c228fd3 100644 --- a/example/ios/Frame Processor Plugins/Example Plugin (Objective-C)/ExampleFrameProcessorPlugin.m +++ b/example/ios/Frame Processor Plugins/Example Plugin/ExampleFrameProcessorPlugin.m @@ -8,6 +8,7 @@ #if __has_include() #import #import +#import #import // Example for an Objective-C Frame Processor plugin @@ -17,18 +18,14 @@ @implementation ExampleFrameProcessorPlugin -- (NSString *)name { - return @"example_plugin"; -} - - (id)callback:(Frame *)frame withArguments:(NSArray *)arguments { CVPixelBufferRef imageBuffer = CMSampleBufferGetImageBuffer(frame.buffer); NSLog(@"ExamplePlugin: %zu x %zu Image. Logging %lu parameters:", CVPixelBufferGetWidth(imageBuffer), CVPixelBufferGetHeight(imageBuffer), (unsigned long)arguments.count); - + for (id param in arguments) { NSLog(@"ExamplePlugin: -> %@ (%@)", param == nil ? @"(nil)" : [param description], NSStringFromClass([param classForCoder])); } - + return @{ @"example_str": @"Test", @"example_bool": @true, @@ -42,7 +39,10 @@ } + (void) load { - [self registerPlugin:[[ExampleFrameProcessorPlugin alloc] init]]; + [FrameProcessorPluginRegistry addFrameProcessorPlugin:@"example_plugin" + withInitializer:^FrameProcessorPlugin*(NSDictionary* options) { + return [[ExampleFrameProcessorPlugin alloc] initWithOptions:options]; + }]; } @end diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index b5423f7..b8ee479 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -713,7 +713,7 @@ SPEC CHECKSUMS: RNStaticSafeAreaInsets: 055ddbf5e476321720457cdaeec0ff2ba40ec1b8 RNVectorIcons: fcc2f6cb32f5735b586e66d14103a74ce6ad61f8 SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 - VisionCamera: b4e91836f577249470ae42707782f4b44d875cd9 + VisionCamera: 29727c3ed48328b246e3a7448f7c14cc12d2fd11 Yoga: 65286bb6a07edce5e4fe8c90774da977ae8fc009 PODFILE CHECKSUM: ab9c06b18c63e741c04349c0fd630c6d3145081c diff --git a/example/ios/VisionCameraExample.xcodeproj/project.pbxproj b/example/ios/VisionCameraExample.xcodeproj/project.pbxproj index ffb1f61..fd120ba 100644 --- a/example/ios/VisionCameraExample.xcodeproj/project.pbxproj +++ b/example/ios/VisionCameraExample.xcodeproj/project.pbxproj @@ -13,7 +13,6 @@ 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; B8DB3BD5263DE8B7004C18D7 /* BuildFile in Sources */ = {isa = PBXBuildFile; }; B8DB3BDC263DEA31004C18D7 /* ExampleFrameProcessorPlugin.m in Sources */ = {isa = PBXBuildFile; fileRef = B8DB3BD8263DEA31004C18D7 /* ExampleFrameProcessorPlugin.m */; }; - B8DB3BDD263DEA31004C18D7 /* ExamplePluginSwift.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8DB3BDA263DEA31004C18D7 /* ExamplePluginSwift.swift */; }; B8F0E10825E0199F00586F16 /* File.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F0E10725E0199F00586F16 /* File.swift */; }; C0B129659921D2EA967280B2 /* libPods-VisionCameraExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3CDCFE89C25C89320B98945E /* libPods-VisionCameraExample.a */; }; /* End PBXBuildFile section */ @@ -29,7 +28,6 @@ 3CDCFE89C25C89320B98945E /* libPods-VisionCameraExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-VisionCameraExample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = VisionCameraExample/LaunchScreen.storyboard; sourceTree = ""; }; B8DB3BD8263DEA31004C18D7 /* ExampleFrameProcessorPlugin.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ExampleFrameProcessorPlugin.m; sourceTree = ""; }; - B8DB3BDA263DEA31004C18D7 /* ExamplePluginSwift.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExamplePluginSwift.swift; sourceTree = ""; }; B8F0E10625E0199F00586F16 /* VisionCameraExample-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "VisionCameraExample-Bridging-Header.h"; sourceTree = ""; }; B8F0E10725E0199F00586F16 /* File.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = File.swift; sourceTree = ""; }; C1D342AD8210E7627A632602 /* Pods-VisionCameraExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-VisionCameraExample.debug.xcconfig"; path = "Target Support Files/Pods-VisionCameraExample/Pods-VisionCameraExample.debug.xcconfig"; sourceTree = ""; }; @@ -118,26 +116,17 @@ B8DB3BD6263DEA31004C18D7 /* Frame Processor Plugins */ = { isa = PBXGroup; children = ( - B8DB3BD7263DEA31004C18D7 /* Example Plugin (Objective-C) */, - B8DB3BD9263DEA31004C18D7 /* Example Plugin (Swift) */, + B8DB3BD7263DEA31004C18D7 /* Example Plugin */, ); path = "Frame Processor Plugins"; sourceTree = ""; }; - B8DB3BD7263DEA31004C18D7 /* Example Plugin (Objective-C) */ = { + B8DB3BD7263DEA31004C18D7 /* Example Plugin */ = { isa = PBXGroup; children = ( B8DB3BD8263DEA31004C18D7 /* ExampleFrameProcessorPlugin.m */, ); - path = "Example Plugin (Objective-C)"; - sourceTree = ""; - }; - B8DB3BD9263DEA31004C18D7 /* Example Plugin (Swift) */ = { - isa = PBXGroup; - children = ( - B8DB3BDA263DEA31004C18D7 /* ExamplePluginSwift.swift */, - ); - path = "Example Plugin (Swift)"; + path = "Example Plugin"; sourceTree = ""; }; /* End PBXGroup section */ @@ -381,7 +370,6 @@ 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */, B8DB3BDC263DEA31004C18D7 /* ExampleFrameProcessorPlugin.m in Sources */, B8DB3BD5263DE8B7004C18D7 /* BuildFile in Sources */, - B8DB3BDD263DEA31004C18D7 /* ExamplePluginSwift.swift in Sources */, B8F0E10825E0199F00586F16 /* File.swift in Sources */, 13B07FC11A68108700A75B9A /* main.m in Sources */, ); diff --git a/example/src/frame-processors/ExamplePlugin.ts b/example/src/frame-processors/ExamplePlugin.ts index fbc386f..8d4b707 100644 --- a/example/src/frame-processors/ExamplePlugin.ts +++ b/example/src/frame-processors/ExamplePlugin.ts @@ -1,16 +1,17 @@ -import { FrameProcessorPlugins, Frame } from 'react-native-vision-camera'; +import { VisionCameraProxy, Frame } from 'react-native-vision-camera'; -export function examplePluginSwift(frame: Frame): string[] { - 'worklet'; - // @ts-expect-error because this function is dynamically injected by VisionCamera - return FrameProcessorPlugins.example_plugin_swift(frame, 'hello!', 'parameter2', true, 42, { test: 0, second: 'test' }, [ - 'another test', - 5, - ]); -} +const plugin = VisionCameraProxy.getFrameProcessorPlugin('example_plugin'); export function examplePlugin(frame: Frame): string[] { 'worklet'; - // @ts-expect-error because this function is dynamically injected by VisionCamera - return FrameProcessorPlugins.example_plugin(frame, 'hello!', 'parameter2', true, 42, { test: 0, second: 'test' }, ['another test', 5]); + + if (plugin == null) throw new Error('Failed to load Frame Processor Plugin "example_plugin"!'); + + return plugin.call(frame, { + someString: 'hello!', + someBoolean: true, + someNumber: 42, + someObject: { test: 0, second: 'test' }, + someArray: ['another test', 5], + }) as string[]; } diff --git a/ios/CameraBridge.h b/ios/CameraBridge.h index c602df9..be86b2b 100644 --- a/ios/CameraBridge.h +++ b/ios/CameraBridge.h @@ -17,6 +17,6 @@ #if VISION_CAMERA_ENABLE_FRAME_PROCESSORS #import "FrameProcessor.h" -#import "FrameProcessorRuntimeManager.h" #import "Frame.h" +#import "VisionCameraProxy.h" #endif diff --git a/ios/CameraViewManager.swift b/ios/CameraViewManager.swift index c66418e..b35b798 100644 --- a/ios/CameraViewManager.swift +++ b/ios/CameraViewManager.swift @@ -13,10 +13,6 @@ import Foundation final class CameraViewManager: RCTViewManager { // pragma MARK: Properties - #if VISION_CAMERA_ENABLE_FRAME_PROCESSORS - private var runtimeManager: FrameProcessorRuntimeManager? - #endif - override var methodQueue: DispatchQueue! { return DispatchQueue.main } @@ -34,10 +30,9 @@ final class CameraViewManager: RCTViewManager { @objc final func installFrameProcessorBindings() -> NSNumber { #if VISION_CAMERA_ENABLE_FRAME_PROCESSORS - // Runs on JS Thread - runtimeManager = FrameProcessorRuntimeManager() - runtimeManager!.installFrameProcessorBindings() - return true as NSNumber + // Called on JS Thread (blocking sync method) + let result = VisionCameraInstaller.install(to: bridge) + return NSNumber(value: result) #else return false as NSNumber #endif diff --git a/ios/Frame Processor/FrameProcessor.h b/ios/Frame Processor/FrameProcessor.h index 9ba38ad..577e8c0 100644 --- a/ios/Frame Processor/FrameProcessor.h +++ b/ios/Frame Processor/FrameProcessor.h @@ -22,8 +22,8 @@ @interface FrameProcessor : NSObject #ifdef __cplusplus -- (instancetype _Nonnull)initWithWorklet:(std::shared_ptr)context - worklet:(std::shared_ptr)worklet; +- (instancetype _Nonnull)initWithWorklet:(std::shared_ptr)worklet + context:(std::shared_ptr)context; - (void)callWithFrameHostObject:(std::shared_ptr)frameHostObject; #endif diff --git a/ios/Frame Processor/FrameProcessor.mm b/ios/Frame Processor/FrameProcessor.mm index 414a773..4803d50 100644 --- a/ios/Frame Processor/FrameProcessor.mm +++ b/ios/Frame Processor/FrameProcessor.mm @@ -21,11 +21,11 @@ using namespace facebook; std::shared_ptr _workletInvoker; } -- (instancetype)initWithWorklet:(std::shared_ptr)context - worklet:(std::shared_ptr)worklet { +- (instancetype)initWithWorklet:(std::shared_ptr)worklet + context:(std::shared_ptr)context { if (self = [super init]) { - _workletContext = context; _workletInvoker = std::make_shared(worklet); + _workletContext = context; } return self; } diff --git a/ios/Frame Processor/FrameProcessorPlugin.h b/ios/Frame Processor/FrameProcessorPlugin.h index dc63ca4..a242200 100644 --- a/ios/Frame Processor/FrameProcessorPlugin.h +++ b/ios/Frame Processor/FrameProcessorPlugin.h @@ -15,18 +15,24 @@ /// /// Subclass this class in a Swift or Objective-C class and override the `callback:withArguments:` method, and /// implement your Frame Processing there. -/// Then, in your App's startup (AppDelegate.m), call `FrameProcessorPluginBase.registerPlugin(YourNewPlugin())` +/// +/// Use `[FrameProcessorPluginRegistry addFrameProcessorPlugin:]` to register the Plugin to the VisionCamera Runtime. @interface FrameProcessorPlugin : NSObject -/// Get the name of the Frame Processor Plugin. -/// This will be exposed to JS under the `FrameProcessorPlugins` Proxy object. -- (NSString * _Nonnull)name; - /// The actual callback when calling this plugin. Any Frame Processing should be handled there. /// Make sure your code is optimized, as this is a hot path. -- (id _Nullable) callback:(Frame* _Nonnull)frame withArguments:(NSArray* _Nullable)arguments; - -/// Register the given plugin in the Plugin Registry. This should be called on App Startup. -+ (void) registerPlugin:(FrameProcessorPlugin* _Nonnull)plugin; +- (id _Nullable) callback:(Frame* _Nonnull)frame withArguments:(NSDictionary* _Nullable)arguments; + +@end + + +// Base implementation (empty) +@implementation FrameProcessorPlugin + +- (id _Nullable)callback:(Frame* _Nonnull)frame withArguments:(NSDictionary* _Nullable)arguments { + [NSException raise:NSInternalInconsistencyException + format:@"Frame Processor Plugin does not override the `callback(frame:withArguments:)` method!"]; + return nil; +} @end diff --git a/ios/Frame Processor/FrameProcessorPlugin.m b/ios/Frame Processor/FrameProcessorPlugin.m deleted file mode 100644 index ec4275e..0000000 --- a/ios/Frame Processor/FrameProcessorPlugin.m +++ /dev/null @@ -1,31 +0,0 @@ -// -// FrameProcessorPlugin.m -// VisionCamera -// -// Created by Marc Rousavy on 24.02.23. -// Copyright © 2023 mrousavy. All rights reserved. -// - -#import -#import "FrameProcessorPlugin.h" -#import "FrameProcessorPluginRegistry.h" - -@implementation FrameProcessorPlugin - -- (NSString *)name { - [NSException raise:NSInternalInconsistencyException - format:@"Frame Processor Plugin \"%@\" does not override the `name` getter!", [self name]]; - return nil; -} - -- (id _Nullable)callback:(Frame* _Nonnull)frame withArguments:(NSArray* _Nullable)arguments { - [NSException raise:NSInternalInconsistencyException - format:@"Frame Processor Plugin \"%@\" does not override the `callback(frame:withArguments:)` method!", [self name]]; - return nil; -} - -+ (void)registerPlugin:(FrameProcessorPlugin* _Nonnull)plugin { - [FrameProcessorPluginRegistry addFrameProcessorPlugin:plugin]; -} - -@end diff --git a/ios/Frame Processor/FrameProcessorPluginHostObject.h b/ios/Frame Processor/FrameProcessorPluginHostObject.h new file mode 100644 index 0000000..542aa6c --- /dev/null +++ b/ios/Frame Processor/FrameProcessorPluginHostObject.h @@ -0,0 +1,32 @@ +// +// FrameProcessorPluginHostObject.h +// VisionCamera +// +// Created by Marc Rousavy on 21.07.23. +// Copyright © 2023 mrousavy. All rights reserved. +// + +#pragma once + +#import +#import "FrameProcessorPlugin.h" +#import +#import + +using namespace facebook; + +class FrameProcessorPluginHostObject: public jsi::HostObject { +public: + explicit FrameProcessorPluginHostObject(FrameProcessorPlugin* plugin, + std::shared_ptr callInvoker): + _plugin(plugin), _callInvoker(callInvoker) { } + ~FrameProcessorPluginHostObject() { } + +public: + std::vector getPropertyNames(jsi::Runtime& runtime) override; + jsi::Value get(jsi::Runtime& runtime, const jsi::PropNameID& name) override; + +private: + FrameProcessorPlugin* _plugin; + std::shared_ptr _callInvoker; +}; diff --git a/ios/Frame Processor/FrameProcessorPluginHostObject.mm b/ios/Frame Processor/FrameProcessorPluginHostObject.mm new file mode 100644 index 0000000..a91b5f8 --- /dev/null +++ b/ios/Frame Processor/FrameProcessorPluginHostObject.mm @@ -0,0 +1,52 @@ +// +// FrameProcessorPluginHostObject.mm +// VisionCamera +// +// Created by Marc Rousavy on 21.07.23. +// Copyright © 2023 mrousavy. All rights reserved. +// + +#import "FrameProcessorPluginHostObject.h" +#import +#import +#import "FrameHostObject.h" +#import "JSINSObjectConversion.h" + +std::vector FrameProcessorPluginHostObject::getPropertyNames(jsi::Runtime& runtime) { + std::vector result; + result.push_back(jsi::PropNameID::forUtf8(runtime, std::string("call"))); + return result; +} + +jsi::Value FrameProcessorPluginHostObject::get(jsi::Runtime& runtime, const jsi::PropNameID& propName) { + auto name = propName.utf8(runtime); + + if (name == "call") { + return jsi::Function::createFromHostFunction(runtime, + jsi::PropNameID::forUtf8(runtime, "call"), + 2, + [=](jsi::Runtime& runtime, + const jsi::Value& thisValue, + const jsi::Value* arguments, + size_t count) -> jsi::Value { + // Frame is first argument + auto frameHostObject = arguments[0].asObject(runtime).asHostObject(runtime); + Frame* frame = frameHostObject->frame; + + // Options are second argument (possibly undefined) + NSDictionary* options = nil; + if (count > 1) { + auto optionsObject = arguments[1].asObject(runtime); + options = JSINSObjectConversion::convertJSIObjectToNSDictionary(runtime, optionsObject, _callInvoker); + } + + // Call actual Frame Processor Plugin + id result = [_plugin callback:frame withArguments:nil]; + + // Convert result value to jsi::Value (possibly undefined) + return JSINSObjectConversion::convertObjCObjectToJSIValue(runtime, result); + }); + } + + return jsi::Value::undefined(); +} diff --git a/ios/Frame Processor/FrameProcessorPluginRegistry.h b/ios/Frame Processor/FrameProcessorPluginRegistry.h index 80ae89b..36a6d4d 100644 --- a/ios/Frame Processor/FrameProcessorPluginRegistry.h +++ b/ios/Frame Processor/FrameProcessorPluginRegistry.h @@ -14,7 +14,12 @@ @interface FrameProcessorPluginRegistry : NSObject -+ (NSMutableDictionary*)frameProcessorPlugins; -+ (void) addFrameProcessorPlugin:(FrameProcessorPlugin* _Nonnull)plugin; +typedef FrameProcessorPlugin* _Nonnull (^PluginInitializerFunction)(NSDictionary* _Nullable options); + ++ (void)addFrameProcessorPlugin:(NSString* _Nonnull)name + withInitializer:(PluginInitializerFunction _Nonnull)pluginInitializer; + ++ (FrameProcessorPlugin* _Nullable)getPlugin:(NSString* _Nonnull)name + withOptions:(NSDictionary* _Nullable)options; @end diff --git a/ios/Frame Processor/FrameProcessorPluginRegistry.m b/ios/Frame Processor/FrameProcessorPluginRegistry.m index 86df213..d4160ce 100644 --- a/ios/Frame Processor/FrameProcessorPluginRegistry.m +++ b/ios/Frame Processor/FrameProcessorPluginRegistry.m @@ -1,5 +1,5 @@ // -// FrameProcessorPluginRegistry.mm +// FrameProcessorPluginRegistry.m // VisionCamera // // Created by Marc Rousavy on 24.03.21. @@ -11,19 +11,28 @@ @implementation FrameProcessorPluginRegistry -+ (NSMutableDictionary*)frameProcessorPlugins { - static NSMutableDictionary* plugins = nil; ++ (NSMutableDictionary*)frameProcessorPlugins { + static NSMutableDictionary* plugins = nil; if (plugins == nil) { plugins = [[NSMutableDictionary alloc] init]; } return plugins; } -+ (void) addFrameProcessorPlugin:(FrameProcessorPlugin*)plugin { - BOOL alreadyExists = [[FrameProcessorPluginRegistry frameProcessorPlugins] valueForKey:plugin.name] != nil; - NSAssert(!alreadyExists, @"Tried to add a Frame Processor Plugin with a name that already exists! Either choose unique names, or remove the unused plugin. Name: %@", plugin.name); ++ (void) addFrameProcessorPlugin:(NSString *)name withInitializer:(PluginInitializerFunction)pluginInitializer { + BOOL alreadyExists = [[FrameProcessorPluginRegistry frameProcessorPlugins] valueForKey:name] != nil; + NSAssert(!alreadyExists, @"Tried to add a Frame Processor Plugin with a name that already exists! Either choose unique names, or remove the unused plugin. Name: %@", name); - [[FrameProcessorPluginRegistry frameProcessorPlugins] setValue:plugin forKey:plugin.name]; + [[FrameProcessorPluginRegistry frameProcessorPlugins] setValue:pluginInitializer forKey:name]; +} + ++ (FrameProcessorPlugin*)getPlugin:(NSString* _Nonnull)name withOptions:(NSDictionary* _Nullable)options { + PluginInitializerFunction initializer = [[FrameProcessorPluginRegistry frameProcessorPlugins] objectForKey:name]; + if (initializer == nil) { + return nil; + } + + return initializer(options); } @end diff --git a/ios/Frame Processor/FrameProcessorRuntimeManager.h b/ios/Frame Processor/FrameProcessorRuntimeManager.h deleted file mode 100644 index a137abb..0000000 --- a/ios/Frame Processor/FrameProcessorRuntimeManager.h +++ /dev/null @@ -1,18 +0,0 @@ -// -// FrameProcessorRuntimeManager.h -// VisionCamera -// -// Created by Marc Rousavy on 23.03.21. -// Copyright © 2021 mrousavy. All rights reserved. -// - -#pragma once - -#import -#import - -@interface FrameProcessorRuntimeManager : NSObject - -- (void) installFrameProcessorBindings; - -@end diff --git a/ios/Frame Processor/FrameProcessorRuntimeManager.mm b/ios/Frame Processor/FrameProcessorRuntimeManager.mm deleted file mode 100644 index 46aa2d5..0000000 --- a/ios/Frame Processor/FrameProcessorRuntimeManager.mm +++ /dev/null @@ -1,203 +0,0 @@ -// -// FrameProcessorRuntimeManager.m -// VisionCamera -// -// Created by Marc Rousavy on 23.03.21. -// Copyright © 2021 mrousavy. All rights reserved. -// - -#import -#import "FrameProcessorRuntimeManager.h" -#import "FrameProcessorPluginRegistry.h" -#import "FrameProcessorPlugin.h" -#import "FrameProcessor.h" -#import "FrameHostObject.h" - -#import - -#import -#import -#import -#import -#import - -#import "WKTJsiWorkletContext.h" -#import "WKTJsiWorklet.h" - -#import "../React Utils/JSIUtils.h" -#import "../../cpp/JSITypedArray.h" - -#if VISION_CAMERA_ENABLE_SKIA -#import "../Skia Render Layer/SkiaFrameProcessor.h" -#endif - -// Forward declarations for the Swift classes -__attribute__((objc_runtime_name("_TtC12VisionCamera12CameraQueues"))) -@interface CameraQueues : NSObject -@property (nonatomic, class, readonly, strong) dispatch_queue_t _Nonnull videoQueue; -@end -__attribute__((objc_runtime_name("_TtC12VisionCamera10CameraView"))) -@interface CameraView : UIView -@property (nonatomic, copy) FrameProcessor* _Nullable frameProcessor; -- (SkiaRenderer* _Nonnull)getSkiaRenderer; -@end - -@implementation FrameProcessorRuntimeManager { - // Separate Camera Worklet Context - std::shared_ptr workletContext; -} - -- (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.videoQueue, [f = std::move(f)](){ - f(); - }); - }; - - workletContext = std::make_shared("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 - FrameProcessorPlugin* plugin = [[FrameProcessorPluginRegistry frameProcessorPlugins] valueForKey:pluginKey]; - - // Create the JSI host function - auto function = [plugin, 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 = [plugin callback:frame->frame withArguments: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)); - } - - // global.FrameProcessorPlugins Proxy - runtime.global().setProperty(runtime, "FrameProcessorPlugins", frameProcessorPlugins); - - NSLog(@"FrameProcessorBindings: Frame Processor plugins installed!"); -} - -- (void) installFrameProcessorBindings { - NSLog(@"FrameProcessorBindings: Installing Frame Processor Bindings for Bridge..."); - RCTCxxBridge *cxxBridge = (RCTCxxBridge *)[RCTBridge currentBridge]; - if (!cxxBridge.runtime) { - return; - } - - jsi::Runtime& jsiRuntime = *(jsi::Runtime*)cxxBridge.runtime; - - // HostObject that attaches the cache to the lifecycle of the Runtime. On Runtime destroy, we destroy the cache. - auto propNameCacheObject = std::make_shared(jsiRuntime); - jsiRuntime.global().setProperty(jsiRuntime, - "__visionCameraArrayBufferCache", - jsi::Object::createFromHostObject(jsiRuntime, propNameCacheObject)); - - // 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) - auto setFrameProcessor = JSI_HOST_FUNCTION_LAMBDA { - NSLog(@"FrameProcessorBindings: Setting new frame processor..."); - auto viewTag = arguments[0].asNumber(); - auto object = arguments[1].asObject(runtime); - auto frameProcessorType = object.getProperty(runtime, "type").asString(runtime).utf8(runtime); - auto worklet = std::make_shared(runtime, object.getProperty(runtime, "frameProcessor")); - - RCTExecuteOnMainQueue(^{ - auto currentBridge = [RCTBridge currentBridge]; - auto anonymousView = [currentBridge.uiManager viewForReactTag:[NSNumber numberWithDouble:viewTag]]; - auto view = static_cast(anonymousView); - if (frameProcessorType == "frame-processor") { - view.frameProcessor = [[FrameProcessor alloc] initWithWorklet:self->workletContext - worklet:worklet]; - - } else if (frameProcessorType == "skia-frame-processor") { -#if VISION_CAMERA_ENABLE_SKIA - SkiaRenderer* skiaRenderer = [view getSkiaRenderer]; - view.frameProcessor = [[SkiaFrameProcessor alloc] initWithWorklet:self->workletContext - worklet:worklet - skiaRenderer:skiaRenderer]; -#else - throw std::runtime_error("system/skia-unavailable: Skia is not installed!"); -#endif - } else { - throw std::runtime_error("Unknown FrameProcessor.type passed! Received: " + frameProcessorType); - } - }); - - return jsi::Value::undefined(); - }; - jsiRuntime.global().setProperty(jsiRuntime, "setFrameProcessor", jsi::Function::createFromHostFunction(jsiRuntime, - jsi::PropNameID::forAscii(jsiRuntime, "setFrameProcessor"), - 2, // viewTag, frameProcessor - setFrameProcessor)); - - // unsetFrameProcessor(viewTag: number) - auto unsetFrameProcessor = JSI_HOST_FUNCTION_LAMBDA { - NSLog(@"FrameProcessorBindings: Removing frame processor..."); - auto viewTag = arguments[0].asNumber(); - - RCTExecuteOnMainQueue(^{ - auto currentBridge = [RCTBridge currentBridge]; - if (!currentBridge) return; - - auto anonymousView = [currentBridge.uiManager viewForReactTag:[NSNumber numberWithDouble:viewTag]]; - auto view = static_cast(anonymousView); - view.frameProcessor = nil; - }); - - return jsi::Value::undefined(); - }; - jsiRuntime.global().setProperty(jsiRuntime, "unsetFrameProcessor", jsi::Function::createFromHostFunction(jsiRuntime, - jsi::PropNameID::forAscii(jsiRuntime, "unsetFrameProcessor"), - 1, // viewTag - unsetFrameProcessor)); - - NSLog(@"FrameProcessorBindings: Finished installing bindings."); -} - -@end diff --git a/ios/React Utils/JSIUtils.h b/ios/Frame Processor/JSINSObjectConversion.h similarity index 94% rename from ios/React Utils/JSIUtils.h rename to ios/Frame Processor/JSINSObjectConversion.h index 5705b90..946a17a 100644 --- a/ios/React Utils/JSIUtils.h +++ b/ios/Frame Processor/JSINSObjectConversion.h @@ -1,5 +1,5 @@ // -// JSIUtils.h +// JSINSObjectConversion.h // VisionCamera // // Created by Marc Rousavy on 30.04.21. @@ -12,6 +12,8 @@ #import #import +namespace JSINSObjectConversion { + using namespace facebook; using namespace facebook::react; @@ -53,3 +55,5 @@ id convertJSIValueToObjCObject(jsi::Runtime& runtime, const jsi::Value& value, s // (any...) => any -> (void)(id, id) RCTResponseSenderBlock convertJSIFunctionToCallback(jsi::Runtime& runtime, const jsi::Function& value, std::shared_ptr jsInvoker); + +} // namespace JSINSObjectConversion diff --git a/ios/React Utils/JSIUtils.mm b/ios/Frame Processor/JSINSObjectConversion.mm similarity index 99% rename from ios/React Utils/JSIUtils.mm rename to ios/Frame Processor/JSINSObjectConversion.mm index 60fce85..092eeab 100644 --- a/ios/React Utils/JSIUtils.mm +++ b/ios/Frame Processor/JSINSObjectConversion.mm @@ -1,5 +1,5 @@ // -// JSIUtils.mm +// JSINSObjectConversion.mm // VisionCamera // // Forked and Adjusted by Marc Rousavy on 02.05.21. @@ -14,7 +14,7 @@ // LICENSE file in the root directory of this source tree. // -#import "JSIUtils.h" +#import "JSINSObjectConversion.h" #import #import #import diff --git a/ios/Frame Processor/VisionCameraProxy.h b/ios/Frame Processor/VisionCameraProxy.h new file mode 100644 index 0000000..6187edd --- /dev/null +++ b/ios/Frame Processor/VisionCameraProxy.h @@ -0,0 +1,44 @@ +// +// VisionCameraProxy.h +// VisionCamera +// +// Created by Marc Rousavy on 20.07.23. +// Copyright © 2023 mrousavy. All rights reserved. +// + +#pragma once + +#import +#import + +#ifdef __cplusplus +#import +#import "WKTJsiWorkletContext.h" +#import + +using namespace facebook; + +class VisionCameraProxy: public jsi::HostObject { +public: + explicit VisionCameraProxy(jsi::Runtime& runtime, + std::shared_ptr callInvoker); + ~VisionCameraProxy(); + +public: + std::vector getPropertyNames(jsi::Runtime& runtime) override; + jsi::Value get(jsi::Runtime& runtime, const jsi::PropNameID& name) override; + +private: + void setFrameProcessor(jsi::Runtime& runtime, int viewTag, const jsi::Object& frameProcessor); + void removeFrameProcessor(jsi::Runtime& runtime, int viewTag); + jsi::Value getFrameProcessorPlugin(jsi::Runtime& runtime, std::string name, const jsi::Object& options); + +private: + std::shared_ptr _workletContext; + std::shared_ptr _callInvoker; +}; +#endif + +@interface VisionCameraInstaller : NSObject ++ (BOOL)installToBridge:(RCTBridge* _Nonnull)bridge; +@end diff --git a/ios/Frame Processor/VisionCameraProxy.mm b/ios/Frame Processor/VisionCameraProxy.mm new file mode 100644 index 0000000..7845707 --- /dev/null +++ b/ios/Frame Processor/VisionCameraProxy.mm @@ -0,0 +1,211 @@ +// +// VisionCameraProxy.mm +// VisionCamera +// +// Created by Marc Rousavy on 20.07.23. +// Copyright © 2023 mrousavy. All rights reserved. +// + +#import "VisionCameraProxy.h" +#import +#import + +#import "FrameProcessorPluginRegistry.h" +#import "FrameProcessorPluginHostObject.h" +#import "FrameProcessor.h" +#import "FrameHostObject.h" +#import "JSINSObjectConversion.h" +#import "../../cpp/JSITypedArray.h" +#import "WKTJsiWorklet.h" + +#import +#import +#import +#import +#import + +#if VISION_CAMERA_ENABLE_SKIA +#import "SkiaRenderer.h" +#import "../Skia Render Layer/SkiaFrameProcessor.h" +#endif + +// Swift forward-declarations +__attribute__((objc_runtime_name("_TtC12VisionCamera12CameraQueues"))) +@interface CameraQueues: NSObject +@property (nonatomic, class, readonly, strong) dispatch_queue_t _Nonnull videoQueue; +@end + +__attribute__((objc_runtime_name("_TtC12VisionCamera10CameraView"))) +@interface CameraView: UIView +@property (nonatomic, copy) FrameProcessor* _Nullable frameProcessor; +#if VISION_CAMERA_ENABLE_SKIA +- (SkiaRenderer* _Nonnull)getSkiaRenderer; +#endif +@end + +using namespace facebook; + +VisionCameraProxy::VisionCameraProxy(jsi::Runtime& runtime, + std::shared_ptr callInvoker) { + _callInvoker = callInvoker; + + NSLog(@"VisionCameraProxy: Creating Worklet Context..."); + 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.videoQueue, [f = std::move(f)](){ + f(); + }); + }; + + _workletContext = std::make_shared("VisionCamera", + &runtime, + runOnJS, + runOnWorklet); + NSLog(@"VisionCameraProxy: Worklet Context Created!"); +} + +VisionCameraProxy::~VisionCameraProxy() { + NSLog(@"VisionCameraProxy: Destroying context..."); + // Destroy ArrayBuffer cache for both the JS and the Worklet Runtime. + vision::invalidateArrayBufferCache(*_workletContext->getJsRuntime()); + vision::invalidateArrayBufferCache(_workletContext->getWorkletRuntime()); +} + +std::vector VisionCameraProxy::getPropertyNames(jsi::Runtime& runtime) { + std::vector result; + result.push_back(jsi::PropNameID::forUtf8(runtime, std::string("setFrameProcessor"))); + result.push_back(jsi::PropNameID::forUtf8(runtime, std::string("removeFrameProcessor"))); + result.push_back(jsi::PropNameID::forUtf8(runtime, std::string("getFrameProcessorPlugin"))); + result.push_back(jsi::PropNameID::forUtf8(runtime, std::string("isSkiaEnabled"))); + return result; +} + +void VisionCameraProxy::setFrameProcessor(jsi::Runtime& runtime, int viewTag, const jsi::Object& object) { + auto frameProcessorType = object.getProperty(runtime, "type").asString(runtime).utf8(runtime); + auto worklet = std::make_shared(runtime, object.getProperty(runtime, "frameProcessor")); + + RCTExecuteOnMainQueue(^{ + auto currentBridge = [RCTBridge currentBridge]; + auto anonymousView = [currentBridge.uiManager viewForReactTag:[NSNumber numberWithDouble:viewTag]]; + auto view = static_cast(anonymousView); + if (frameProcessorType == "frame-processor") { + view.frameProcessor = [[FrameProcessor alloc] initWithWorklet:worklet + context:_workletContext]; + + } else if (frameProcessorType == "skia-frame-processor") { +#if VISION_CAMERA_ENABLE_SKIA + SkiaRenderer* skiaRenderer = [view getSkiaRenderer]; + view.frameProcessor = [[SkiaFrameProcessor alloc] initWithWorklet:worklet + context:_workletContext + skiaRenderer:skiaRenderer]; +#else + throw std::runtime_error("system/skia-unavailable: Skia is not installed!"); +#endif + } else { + throw std::runtime_error("Unknown FrameProcessor.type passed! Received: " + frameProcessorType); + } + }); +} + +void VisionCameraProxy::removeFrameProcessor(jsi::Runtime& runtime, int viewTag) { + RCTExecuteOnMainQueue(^{ + auto currentBridge = [RCTBridge currentBridge]; + auto anonymousView = [currentBridge.uiManager viewForReactTag:[NSNumber numberWithDouble:viewTag]]; + auto view = static_cast(anonymousView); + view.frameProcessor = nil; + }); +} + +jsi::Value VisionCameraProxy::getFrameProcessorPlugin(jsi::Runtime& runtime, std::string name, const jsi::Object& options) { + NSString* key = [NSString stringWithUTF8String:name.c_str()]; + NSDictionary* optionsObjc = JSINSObjectConversion::convertJSIObjectToNSDictionary(runtime, options, _callInvoker); + FrameProcessorPlugin* plugin = [FrameProcessorPluginRegistry getPlugin:key withOptions:optionsObjc]; + if (plugin == nil) { + return jsi::Value::undefined(); + } + + auto pluginHostObject = std::make_shared(plugin, _callInvoker); + return jsi::Object::createFromHostObject(runtime, pluginHostObject); +} + +jsi::Value VisionCameraProxy::get(jsi::Runtime& runtime, const jsi::PropNameID& propName) { + auto name = propName.utf8(runtime); + + if (name == "isSkiaEnabled") { +#ifdef VISION_CAMERA_ENABLE_SKIA + return jsi::Value(true); +#else + return jsi::Value(false); +#endif + } + if (name == "setFrameProcessor") { + return jsi::Function::createFromHostFunction(runtime, + jsi::PropNameID::forUtf8(runtime, "setFrameProcessor"), + 1, + [this](jsi::Runtime& runtime, + const jsi::Value& thisValue, + const jsi::Value* arguments, + size_t count) -> jsi::Value { + auto viewTag = arguments[0].asNumber(); + auto object = arguments[1].asObject(runtime); + this->setFrameProcessor(runtime, static_cast(viewTag), object); + return jsi::Value::undefined(); + }); + } + if (name == "removeFrameProcessor") { + return jsi::Function::createFromHostFunction(runtime, + jsi::PropNameID::forUtf8(runtime, "removeFrameProcessor"), + 1, + [this](jsi::Runtime& runtime, + const jsi::Value& thisValue, + const jsi::Value* arguments, + size_t count) -> jsi::Value { + auto viewTag = arguments[0].asNumber(); + this->removeFrameProcessor(runtime, static_cast(viewTag)); + return jsi::Value::undefined(); + }); + } + if (name == "getFrameProcessorPlugin") { + return jsi::Function::createFromHostFunction(runtime, + jsi::PropNameID::forUtf8(runtime, "getFrameProcessorPlugin"), + 1, + [this](jsi::Runtime& runtime, + const jsi::Value& thisValue, + const jsi::Value* arguments, + size_t count) -> jsi::Value { + if (count != 1 || !arguments[0].isString()) { + throw jsi::JSError(runtime, "First argument needs to be a string (pluginName)!"); + } + auto pluginName = arguments[0].asString(runtime).utf8(runtime); + auto options = count > 1 ? arguments[1].asObject(runtime) : jsi::Object(runtime); + + return this->getFrameProcessorPlugin(runtime, pluginName, options); + }); + } + + return jsi::Value::undefined(); +} + + +@implementation VisionCameraInstaller ++ (BOOL)installToBridge:(RCTBridge* _Nonnull)bridge { + RCTCxxBridge* cxxBridge = (RCTCxxBridge*)[RCTBridge currentBridge]; + if (!cxxBridge.runtime) { + return NO; + } + + jsi::Runtime& runtime = *(jsi::Runtime*)cxxBridge.runtime; + + // global.VisionCameraProxy + auto visionCameraProxy = std::make_shared(runtime, bridge.jsCallInvoker); + runtime.global().setProperty(runtime, + "VisionCameraProxy", + jsi::Object::createFromHostObject(runtime, visionCameraProxy)); + + return YES; +} +@end diff --git a/ios/Skia Render Layer/SkiaFrameProcessor.h b/ios/Skia Render Layer/SkiaFrameProcessor.h index be00b2c..10ffafd 100644 --- a/ios/Skia Render Layer/SkiaFrameProcessor.h +++ b/ios/Skia Render Layer/SkiaFrameProcessor.h @@ -19,8 +19,8 @@ @interface SkiaFrameProcessor: FrameProcessor #ifdef __cplusplus -- (instancetype _Nonnull) initWithWorklet:(std::shared_ptr)context - worklet:(std::shared_ptr)worklet +- (instancetype _Nonnull) initWithWorklet:(std::shared_ptr)worklet + context:(std::shared_ptr)context skiaRenderer:(SkiaRenderer* _Nonnull)skiaRenderer; #endif diff --git a/ios/Skia Render Layer/SkiaFrameProcessor.mm b/ios/Skia Render Layer/SkiaFrameProcessor.mm index 90f0639..c702308 100644 --- a/ios/Skia Render Layer/SkiaFrameProcessor.mm +++ b/ios/Skia Render Layer/SkiaFrameProcessor.mm @@ -25,11 +25,11 @@ using namespace facebook; std::shared_ptr _skiaCanvas; } -- (instancetype _Nonnull)initWithWorklet:(std::shared_ptr)context - worklet:(std::shared_ptr)worklet +- (instancetype _Nonnull)initWithWorklet:(std::shared_ptr)worklet + context:(std::shared_ptr)context skiaRenderer:(SkiaRenderer* _Nonnull)skiaRenderer { - if (self = [super initWithWorklet:context - worklet:worklet]) { + if (self = [super initWithWorklet:worklet + context:context]) { _skiaRenderer = skiaRenderer; auto platformContext = std::make_shared(context->getJsRuntime(), RCTBridge.currentBridge); diff --git a/ios/VisionCamera.xcodeproj/project.pbxproj b/ios/VisionCamera.xcodeproj/project.pbxproj index f2b0f77..e3bdfc1 100644 --- a/ios/VisionCamera.xcodeproj/project.pbxproj +++ b/ios/VisionCamera.xcodeproj/project.pbxproj @@ -54,8 +54,7 @@ B88751A825E0102000DB86D6 /* CameraError.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887518325E0102000DB86D6 /* CameraError.swift */; }; B88751A925E0102000DB86D6 /* CameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887518425E0102000DB86D6 /* CameraView.swift */; }; B88B47472667C8E00091F538 /* AVCaptureSession+setVideoStabilizationMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88B47462667C8E00091F538 /* AVCaptureSession+setVideoStabilizationMode.swift */; }; - B8994E6C263F03E100069589 /* JSIUtils.mm in Sources */ = {isa = PBXBuildFile; fileRef = B8994E6B263F03E100069589 /* JSIUtils.mm */; }; - B8A751D82609E4B30011C623 /* FrameProcessorRuntimeManager.mm in Sources */ = {isa = PBXBuildFile; fileRef = B8A751D72609E4B30011C623 /* FrameProcessorRuntimeManager.mm */; }; + B8994E6C263F03E100069589 /* JSINSObjectConversion.mm in Sources */ = {isa = PBXBuildFile; fileRef = B8994E6B263F03E100069589 /* JSINSObjectConversion.mm */; }; B8BD3BA2266E22D2006C80A2 /* Callback.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BD3BA1266E22D2006C80A2 /* Callback.swift */; }; B8D22CDC2642DB4D00234472 /* AVAssetWriterInputPixelBufferAdaptor+initWithVideoSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D22CDB2642DB4D00234472 /* AVAssetWriterInputPixelBufferAdaptor+initWithVideoSettings.swift */; }; B8DB3BC8263DC28C004C18D7 /* AVAssetWriter.Status+descriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8DB3BC7263DC28C004C18D7 /* AVAssetWriter.Status+descriptor.swift */; }; @@ -80,13 +79,15 @@ /* Begin PBXFileReference section */ 134814201AA4EA6300B7C361 /* libVisionCamera.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libVisionCamera.a; sourceTree = BUILT_PRODUCTS_DIR; }; B80A319E293A5C10003EE681 /* SkiaRenderContext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SkiaRenderContext.h; sourceTree = ""; }; + B80C02EB2A6A954D001975E2 /* FrameProcessorPluginHostObject.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FrameProcessorPluginHostObject.mm; sourceTree = ""; }; + B80C02EC2A6A9552001975E2 /* FrameProcessorPluginHostObject.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FrameProcessorPluginHostObject.h; sourceTree = ""; }; B80C0DFE260BDD97001699AB /* FrameProcessorPluginRegistry.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FrameProcessorPluginRegistry.h; sourceTree = ""; }; B80C0DFF260BDDF7001699AB /* FrameProcessorPluginRegistry.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FrameProcessorPluginRegistry.m; sourceTree = ""; }; B80E069F266632F000728644 /* AVAudioSession+updateCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVAudioSession+updateCategory.swift"; sourceTree = ""; }; B8103E5725FF56F0007A1684 /* Frame.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Frame.h; sourceTree = ""; }; B8127E382A68871C00B06972 /* SkiaPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SkiaPreviewView.swift; sourceTree = ""; }; B81BE1BE26B936FF002696CC /* AVCaptureDevice.Format+videoDimensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVCaptureDevice.Format+videoDimensions.swift"; sourceTree = ""; }; - B81D41EF263C86F900B041FD /* JSIUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JSIUtils.h; sourceTree = ""; }; + B81D41EF263C86F900B041FD /* JSINSObjectConversion.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JSINSObjectConversion.h; sourceTree = ""; }; B82F3A0A2A6896E3002BB804 /* PreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewView.swift; sourceTree = ""; }; B83D5EE629377117000AFD2F /* NativePreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativePreviewView.swift; sourceTree = ""; }; B841262E292E41A1001AB448 /* SkImageHelpers.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = SkImageHelpers.mm; sourceTree = ""; }; @@ -100,7 +101,6 @@ B86DC970260E2D5200FB17B2 /* AVAudioSession+trySetAllowHaptics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVAudioSession+trySetAllowHaptics.swift"; sourceTree = ""; }; B86DC973260E310600FB17B2 /* CameraView+AVAudioSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CameraView+AVAudioSession.swift"; sourceTree = ""; }; B86DC976260E315100FB17B2 /* CameraView+AVCaptureSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CameraView+AVCaptureSession.swift"; sourceTree = ""; }; - B86F803429A90DBD00205E48 /* FrameProcessorPlugin.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FrameProcessorPlugin.m; sourceTree = ""; }; B882720F26AEB1A100B14107 /* AVCaptureConnection+setInterfaceOrientation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVCaptureConnection+setInterfaceOrientation.swift"; sourceTree = ""; }; B887515C25E0102000DB86D6 /* PhotoCaptureDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCaptureDelegate.swift; sourceTree = ""; }; B887515D25E0102000DB86D6 /* CameraView+RecordVideo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CameraView+RecordVideo.swift"; sourceTree = ""; }; @@ -137,11 +137,9 @@ B887518425E0102000DB86D6 /* CameraView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraView.swift; sourceTree = ""; }; B88873E5263D46C7008B1D0E /* FrameProcessorPlugin.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FrameProcessorPlugin.h; sourceTree = ""; }; B88B47462667C8E00091F538 /* AVCaptureSession+setVideoStabilizationMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVCaptureSession+setVideoStabilizationMode.swift"; sourceTree = ""; }; - B8994E6B263F03E100069589 /* JSIUtils.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = JSIUtils.mm; sourceTree = ""; }; + B8994E6B263F03E100069589 /* JSINSObjectConversion.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = JSINSObjectConversion.mm; sourceTree = ""; }; B89A28742A68795E0092207F /* SkiaRenderer.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = SkiaRenderer.mm; sourceTree = ""; }; B89A28752A68796A0092207F /* SkiaRenderer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SkiaRenderer.h; sourceTree = ""; }; - B8A751D62609E4980011C623 /* FrameProcessorRuntimeManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FrameProcessorRuntimeManager.h; sourceTree = ""; }; - B8A751D72609E4B30011C623 /* FrameProcessorRuntimeManager.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FrameProcessorRuntimeManager.mm; sourceTree = ""; }; B8BD3BA1266E22D2006C80A2 /* Callback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Callback.swift; sourceTree = ""; }; B8C1FD222A613607007A06D6 /* SkiaFrameProcessor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SkiaFrameProcessor.h; sourceTree = ""; }; B8C1FD232A613612007A06D6 /* SkiaFrameProcessor.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = SkiaFrameProcessor.mm; sourceTree = ""; }; @@ -151,6 +149,8 @@ B8DB3BCB263DC97E004C18D7 /* AVFileType+descriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVFileType+descriptor.swift"; sourceTree = ""; }; B8DFBA362A68A17E00941736 /* DrawableFrameHostObject.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = DrawableFrameHostObject.mm; sourceTree = ""; }; B8DFBA372A68A17E00941736 /* DrawableFrameHostObject.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DrawableFrameHostObject.h; sourceTree = ""; }; + B8E8467D2A696F44000D6A11 /* VisionCameraProxy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VisionCameraProxy.h; sourceTree = ""; }; + B8E8467E2A696F4D000D6A11 /* VisionCameraProxy.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = VisionCameraProxy.mm; sourceTree = ""; }; B8E957CD2A6939A6008F5480 /* CameraView+Preview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CameraView+Preview.swift"; sourceTree = ""; }; B8E957CF2A693AD2008F5480 /* CameraView+Torch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CameraView+Torch.swift"; sourceTree = ""; }; B8F0825E2A6046FC00C17EB6 /* FrameProcessor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FrameProcessor.h; sourceTree = ""; }; @@ -237,8 +237,6 @@ B887516F25E0102000DB86D6 /* ReactLogger.swift */, B887517025E0102000DB86D6 /* Promise.swift */, B8BD3BA1266E22D2006C80A2 /* Callback.swift */, - B81D41EF263C86F900B041FD /* JSIUtils.h */, - B8994E6B263F03E100069589 /* JSIUtils.mm */, ); path = "React Utils"; sourceTree = ""; @@ -273,12 +271,15 @@ B8F7DDD1266F715D00120533 /* Frame.m */, B84760A22608EE38004C3180 /* FrameHostObject.h */, B84760A52608EE7C004C3180 /* FrameHostObject.mm */, - B8A751D62609E4980011C623 /* FrameProcessorRuntimeManager.h */, - B8A751D72609E4B30011C623 /* FrameProcessorRuntimeManager.mm */, B80C0DFE260BDD97001699AB /* FrameProcessorPluginRegistry.h */, B80C0DFF260BDDF7001699AB /* FrameProcessorPluginRegistry.m */, B88873E5263D46C7008B1D0E /* FrameProcessorPlugin.h */, - B86F803429A90DBD00205E48 /* FrameProcessorPlugin.m */, + B8E8467D2A696F44000D6A11 /* VisionCameraProxy.h */, + B8E8467E2A696F4D000D6A11 /* VisionCameraProxy.mm */, + B80C02EC2A6A9552001975E2 /* FrameProcessorPluginHostObject.h */, + B80C02EB2A6A954D001975E2 /* FrameProcessorPluginHostObject.mm */, + B81D41EF263C86F900B041FD /* JSINSObjectConversion.h */, + B8994E6B263F03E100069589 /* JSINSObjectConversion.mm */, ); path = "Frame Processor"; sourceTree = ""; @@ -436,7 +437,6 @@ B887518A25E0102000DB86D6 /* AVCaptureDevice+neutralZoom.swift in Sources */, B88751A325E0102000DB86D6 /* AVCaptureDevice.FlashMode+descriptor.swift in Sources */, B8E957CE2A6939A6008F5480 /* CameraView+Preview.swift in Sources */, - B8A751D82609E4B30011C623 /* FrameProcessorRuntimeManager.mm in Sources */, B887519A25E0102000DB86D6 /* AVVideoCodecType+descriptor.swift in Sources */, B88751A825E0102000DB86D6 /* CameraError.swift in Sources */, B88751A625E0102000DB86D6 /* CameraViewManager.swift in Sources */, @@ -450,7 +450,7 @@ B88751A025E0102000DB86D6 /* AVAuthorizationStatus+descriptor.swift in Sources */, B80C0E00260BDDF7001699AB /* FrameProcessorPluginRegistry.m in Sources */, B887519C25E0102000DB86D6 /* AVCaptureDevice.TorchMode+descriptor.swift in Sources */, - B8994E6C263F03E100069589 /* JSIUtils.mm in Sources */, + B8994E6C263F03E100069589 /* JSINSObjectConversion.mm in Sources */, B88751A525E0102000DB86D6 /* CameraView+Focus.swift in Sources */, B86DC971260E2D5200FB17B2 /* AVAudioSession+trySetAllowHaptics.swift in Sources */, B88B47472667C8E00091F538 /* AVCaptureSession+setVideoStabilizationMode.swift in Sources */, diff --git a/src/Camera.tsx b/src/Camera.tsx index 655b66e..2a908c5 100644 --- a/src/Camera.tsx +++ b/src/Camera.tsx @@ -1,16 +1,16 @@ import React from 'react'; import { requireNativeComponent, NativeSyntheticEvent, findNodeHandle, NativeMethods, Platform } from 'react-native'; -import type { VideoFileType } from '.'; import type { CameraDevice } from './CameraDevice'; import type { ErrorWithCause } from './CameraError'; import { CameraCaptureError, CameraRuntimeError, tryParseNativeCameraError, isErrorWithCause } from './CameraError'; import type { CameraProps, FrameProcessor } from './CameraProps'; -import { assertFrameProcessorsAvailable, assertJSIAvailable } from './JSIHelper'; +import { assertJSIAvailable } from './JSIHelper'; import { CameraModule } from './NativeCameraModule'; import type { PhotoFile, TakePhotoOptions } from './PhotoFile'; import type { Point } from './Point'; import type { TakeSnapshotOptions } from './Snapshot'; -import type { CameraVideoCodec, RecordVideoOptions, VideoFile } from './VideoFile'; +import type { CameraVideoCodec, RecordVideoOptions, VideoFile, VideoFileType } from './VideoFile'; +import { VisionCameraProxy } from './FrameProcessorPlugins'; //#region Types export type CameraPermissionStatus = 'authorized' | 'not-determined' | 'denied' | 'restricted'; @@ -82,7 +82,7 @@ export class Camera extends React.PureComponent { this.lastFrameProcessor = undefined; } - private get handle(): number | null { + private get handle(): number { const nodeHandle = findNodeHandle(this.ref.current); if (nodeHandle == null || nodeHandle === -1) { throw new CameraRuntimeError( @@ -312,7 +312,8 @@ export class Camera extends React.PureComponent { public static installFrameProcessorBindings(): void { assertJSIAvailable(); const result = CameraModule.installFrameProcessorBindings() as unknown; - if (result !== true) throw new Error('Failed to install Frame Processor JSI bindings!'); + if (result !== true) + throw new CameraRuntimeError('system/frame-processors-unavailable', 'Failed to install Frame Processor JSI bindings!'); } /** @@ -418,15 +419,11 @@ export class Camera extends React.PureComponent { //#region Lifecycle private setFrameProcessor(frameProcessor: FrameProcessor): void { - assertFrameProcessorsAvailable(); - // @ts-expect-error JSI functions aren't typed - global.setFrameProcessor(this.handle, frameProcessor); + VisionCameraProxy.setFrameProcessor(this.handle, frameProcessor); } private unsetFrameProcessor(): void { - assertFrameProcessorsAvailable(); - // @ts-expect-error JSI functions aren't typed - global.unsetFrameProcessor(this.handle); + VisionCameraProxy.removeFrameProcessor(this.handle); } private onViewReady(): void { diff --git a/src/FrameProcessorPlugins.ts b/src/FrameProcessorPlugins.ts index b5afad0..10ec6d6 100644 --- a/src/FrameProcessorPlugins.ts +++ b/src/FrameProcessorPlugins.ts @@ -1,26 +1,50 @@ import type { Frame, FrameInternal } from './Frame'; +import type { FrameProcessor } from './CameraProps'; import { Camera } from './Camera'; import { Worklets } from 'react-native-worklets/src'; +import { CameraRuntimeError } from './CameraError'; + +type BasicParameterType = string | number | boolean | undefined; +type ParameterType = BasicParameterType | BasicParameterType[] | Record; + +interface FrameProcessorPlugin { + /** + * Call the native Frame Processor Plugin with the given Frame and options. + * @param frame The Frame from the Frame Processor. + * @param options (optional) Additional options. Options will be converted to a native dictionary + * @returns (optional) A value returned from the native Frame Processor Plugin (or undefined) + */ + call: (frame: Frame, options?: Record) => ParameterType; +} + +interface TVisionCameraProxy { + setFrameProcessor: (viewTag: number, frameProcessor: FrameProcessor) => void; + removeFrameProcessor: (viewTag: number) => void; + /** + * Creates a new instance of a Frame Processor Plugin. + * The Plugin has to be registered on the native side, otherwise this returns `undefined` + */ + getFrameProcessorPlugin: (name: string) => FrameProcessorPlugin | undefined; + isSkiaEnabled: boolean; +} -// Install VisionCamera Frame Processor JSI Bindings and Plugins Camera.installFrameProcessorBindings(); +// @ts-expect-error global is untyped, it's a C++ host-object +export const VisionCameraProxy = global.VisionCameraProxy as TVisionCameraProxy; +// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition +if (VisionCameraProxy == null) { + throw new CameraRuntimeError( + 'system/frame-processors-unavailable', + 'Failed to install VisionCameraProxy. Are Frame Processors properly enabled?', + ); +} + declare global { // eslint-disable-next-line no-var var __frameProcessorRunAtTargetFpsMap: Record | undefined; } -type BasicParameterType = string | number | boolean | undefined; -type ParameterType = BasicParameterType | BasicParameterType[] | Record; -type FrameProcessor = (frame: Frame, parameters?: Record) => unknown; -type TFrameProcessorPlugins = Record; - -/** - * All natively installed Frame Processor Plugins. - */ -// @ts-expect-error The global JSI Proxy object is not typed. -export const FrameProcessorPlugins = global.FrameProcessorPlugins as TFrameProcessorPlugins; - function getLastFrameProcessorCall(frameProcessorFuncId: string): number { 'worklet'; return global.__frameProcessorRunAtTargetFpsMap?.[frameProcessorFuncId] ?? 0; diff --git a/src/JSIHelper.ts b/src/JSIHelper.ts index cbb6984..7f5bec8 100644 --- a/src/JSIHelper.ts +++ b/src/JSIHelper.ts @@ -10,15 +10,3 @@ export function assertJSIAvailable(): void { ); } } - -export function assertFrameProcessorsAvailable(): void { - assertJSIAvailable(); - - // @ts-expect-error JSI functions aren't typed - if (global.setFrameProcessor == null || global.unsetFrameProcessor == null) { - throw new CameraRuntimeError( - 'system/frame-processors-unavailable', - 'Frame Processors are not enabled. See https://mrousavy.github.io/react-native-vision-camera/docs/guides/troubleshooting', - ); - } -} diff --git a/src/expo-plugin/withDisableFrameProcessorsIOS.ts b/src/expo-plugin/withDisableFrameProcessorsIOS.ts index b77d1d0..af58f86 100644 --- a/src/expo-plugin/withDisableFrameProcessorsIOS.ts +++ b/src/expo-plugin/withDisableFrameProcessorsIOS.ts @@ -1,28 +1,13 @@ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -import { ConfigPlugin, withXcodeProject, XcodeProject } from '@expo/config-plugins'; +import { ConfigPlugin, withPodfileProperties } from '@expo/config-plugins'; /** * Set the `disableFrameProcessors` inside of the XcodeProject. * This is used to disable frame processors if you don't need it on iOS. (will save CPU and Memory) */ export const withDisableFrameProcessorsIOS: ConfigPlugin = (c) => { - return withXcodeProject(c, (config) => { - const xcodeProject: XcodeProject = config.modResults; - - const configurations = xcodeProject.pbxXCBuildConfigurationSection(); - - const inheritKey = '"$(inherited)"'; - const valueKey = '"VISION_CAMERA_DISABLE_FRAME_PROCESSORS=1"'; - - for (const key in configurations) { - const buildSettings = configurations[key].buildSettings; - if (buildSettings == null) continue; - - const preprocessorDefinitions = (buildSettings.GCC_PREPROCESSOR_DEFINITIONS ?? [inheritKey]) as string[]; - - if (!preprocessorDefinitions.includes(valueKey)) preprocessorDefinitions.push(valueKey); - buildSettings.GCC_PREPROCESSOR_DEFINITIONS = preprocessorDefinitions; - } + return withPodfileProperties(c, (config) => { + // TODO: Implement Podfile writing + config.ios = config.ios; return config; }); }; diff --git a/src/hooks/useFrameProcessor.ts b/src/hooks/useFrameProcessor.ts index f5d01cd..38f71e3 100644 --- a/src/hooks/useFrameProcessor.ts +++ b/src/hooks/useFrameProcessor.ts @@ -4,6 +4,25 @@ import { FrameProcessor } from '../CameraProps'; // Install RN Worklets by importing it import 'react-native-worklets/src'; +export function createFrameProcessor(frameProcessor: FrameProcessor['frameProcessor'], type: FrameProcessor['type']): FrameProcessor { + return { + frameProcessor: (frame: Frame | DrawableFrame) => { + 'worklet'; + // Increment ref-count by one + (frame as FrameInternal).incrementRefCount(); + try { + // Call sync frame processor + // @ts-expect-error the frame type is ambiguous here + frameProcessor(frame); + } finally { + // Potentially delete Frame if we were the last ref (no runAsync) + (frame as FrameInternal).decrementRefCount(); + } + }, + type: type, + }; +} + /** * 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)) @@ -23,25 +42,8 @@ import 'react-native-worklets/src'; * ``` */ export function useFrameProcessor(frameProcessor: (frame: Frame) => void, dependencies: DependencyList): FrameProcessor { - return useMemo( - () => ({ - frameProcessor: (frame: Frame) => { - 'worklet'; - // Increment ref-count by one - (frame as FrameInternal).incrementRefCount(); - try { - // Call sync frame processor - frameProcessor(frame); - } finally { - // Potentially delete Frame if we were the last ref (no runAsync) - (frame as FrameInternal).decrementRefCount(); - } - }, - type: 'frame-processor', - }), - // eslint-disable-next-line react-hooks/exhaustive-deps - dependencies, - ); + // eslint-disable-next-line react-hooks/exhaustive-deps + return useMemo(() => createFrameProcessor(frameProcessor, 'frame-processor'), dependencies); } /** @@ -65,23 +67,6 @@ export function useFrameProcessor(frameProcessor: (frame: Frame) => void, depend * ``` */ export function useSkiaFrameProcessor(frameProcessor: (frame: DrawableFrame) => void, dependencies: DependencyList): FrameProcessor { - return useMemo( - () => ({ - frameProcessor: (frame: DrawableFrame) => { - 'worklet'; - // Increment ref-count by one - (frame as FrameInternal).incrementRefCount(); - try { - // Call sync frame processor - frameProcessor(frame); - } finally { - // Potentially delete Frame if we were the last ref (no runAsync) - (frame as FrameInternal).decrementRefCount(); - } - }, - type: 'skia-frame-processor', - }), - // eslint-disable-next-line react-hooks/exhaustive-deps - dependencies, - ); + // eslint-disable-next-line react-hooks/exhaustive-deps + return useMemo(() => createFrameProcessor(frameProcessor, 'skia-frame-processor'), dependencies); }