Native Frame Processor Plugins

Understanding Native Frame Processor Plugins, using them and building them using Swift/Kotlin

Native Frame Processor Plugins are essentially native Swift/Kotlin functions that can be called from a JS Frame Processor (see "The Frame Output").

const myNativePlugin = ...
const frameOutput = useFrameOutput({
  onFrame(frame) {
    'worklet'
    myNativePlugin.call(frame)
    frame.dispose()
  }
})

Implementation

Native Frame Processor Plugins are Nitro Modules.

1. Bootstrap a Nitro Module

To get started, follow the "Nitro: How to build a Nitro Module" guide, and create a Swift/Kotlin Hybrid Object.

2. Add a dependency on react-native-vision-camera

Then, in your Nitro Module, add a dependency on react-native-vision-camera in your package.json:

package.json
{
  // ...
  "devDependencies": {
    // ...
    "react-native-vision-camera": "*",
  },
  "peerDependencies": {
    // ...
    "react-native-vision-camera": "*"
  },
}

..as well as to your native iOS/Android dependencies:

Add the VisionCamera pod to your *.podspec:

MyNativePlugin.podspec
Pod::Spec.new do |s|
  # ...
  s.dependency 'VisionCamera'
end

Add the react-native-vision-camera project to your build.gradle:

android/build.gradle
// ...
dependencies {
  // ...
  implementation project(":react-native-vision-camera")

..and the prefab to your CMakeLists:

android/CMakeLists.txt
// ...
find_package(react-native-vision-camera REQUIRED)

# Link all libraries together
target_link_libraries(
        ${PACKAGE_NAME}
        // ...
        react-native-vision-camera::VisionCamera
)

3. Using the Frame in the specs

Now, define a Nitro HybridObject that takes a Frame from react-native-vision-camera:

MyNativePlugin.nitro.ts
import type { HybridObject } from 'react-native-nitro-modules'
import type { Frame } from 'react-native-vision-camera'

export interface MyNativePlugin extends HybridObject<{
  ios: 'swift',
  android: 'kotlin'
}> {
  call(frame: Frame): void
}

Tip

The names, arguments, and return values are irrelevant - you can name this method whatever you like, pass any kind of arguments and return any return value - given Nitro supports it.

4. Implement the native plugin

Then, generate your Nitro specs via nitrogen. In your native iOS/Android project, you can now implement MyNativePlugin and its call(...) function:

HybridMyNativePlugin.swift
import VisionCamera
import NitroModules

class HybridMyNativePlugin: HybridMyNativePluginSpec {
  func call(frame: any HybridFrameSpec) {
    // TODO: Implementation
  }
}
HybridMyNativePlugin.kt
package com.margelo.nitro.mynativeplugin
import com.margelo.nitro.camera.HybridFrameSpec

class HybridMyNativePlugin: HybridMyNativePluginSpec() {
  fun call(frame: HybridFrameSpec) {
    // TODO: Implementation
  }
}

To unwrap the native CMSampleBuffer/Image from the HybridFrameSpec, cast it to NativeFrame - a public type exposed by VisionCamera:

HybridMyNativePlugin.swift
import VisionCamera
import AVFoundation
import NitroModules

class HybridMyNativePlugin: HybridMyNativePluginSpec {
  func call(frame: any HybridFrameSpec) {
    guard let frame = frame as? NativeFrame else { return }
    let buffer = frame.sampleBuffer // CMSampleBuffer
  }
}
HybridMyNativePlugin.kt
package com.margelo.nitro.mynativeplugin
import com.margelo.nitro.camera.HybridFrameSpec
import com.margelo.nitro.camera.public.NativeFrame

class HybridMyNativePlugin: HybridMyNativePluginSpec() {
  fun call(frame: HybridFrameSpec) {
    val frame = frame as? NativeFrame ?: return
    val image = frame.image // ImageProxy
  }
}

5. Call it from JS

In JS, you can now create an instance of your Nitro HybridObject and use it in a Frame Processor:

import { NitroModules } from 'react-native-nitro-modules'

const plugin = NitroModules.createHybridObject<MyNativePlugin>('MyNativePlugin')
const frameOutput = useFrameOutput({
  onFrame(frame) {
    'worklet'
    plugin.call(frame)
    frame.dispose()
  }
})

Thanks to Nitro's powerful architecture, your Native Frame Processor Plugin can accept any kinds of arguments, return any kind of values (even a Frame!), and even be asynchronous.

Tip

If your HybridObject needs arguments, you should create a separate HybridObject that acts as a factory and has a create-function for your HybridObject with arguments.

C++ Plugins

Because Nitro supports C++, you can also implement your Frame Processor Plugin in pure C++.

HybridMyNativePlugin.hpp
#include <VisionCamera/HybridFrameSpec.hpp>

namespace margelo::nitro::myplugin {
  class HybridMyNativePlugin: public HybridMyNativePluginSpec {
  public:
    HybridMyNativePlugin(): HybridObject(TAG) {}
    void call(const std::shared_ptr<HybridFrameSpec>& frame) {
      // TODO: Implementation
    }
  };
}

Since the Frame is a platform type, you cannot reliably downcast it cross-platform directly. Instead, you can use the public JS APIs (like Frame.getNativeBuffer()) from C++, and downcast per platform if needed:

void call(const std::shared_ptr<HybridFrameSpec>& frame) {
  auto nativeBuffer = frame->getNativeBuffer();
  auto orientation = frame->getOrientation();
#ifdef ANDROID
  AHardwareBuffer* hardwareBuffer = reinterpret_cast<AHardwareBuffer*>(nativeBuffer.pointer);
#else // iOS
  CVPixelBufferRef pixelBuffer = reinterpret_cast<CVPixelBufferRef>(nativeBuffer.pointer);
#endif
}

Tip

See A Frame's NativeBuffer for more information.

Alternatively, you can add platform-specific C++/Objective-C++ code to your existing Swift/Kotlin codebases, and use the respective CVPixelBuffer or AHardwareBuffer* APIs.

Depth

While this guide covered the Frame type, the same principle applies to Depth as well. Simply use the Depth type instead of Frame, and cast to NativeDepth.