feat: Add zero-copy SharedArray type to Frame Processor Plugins (#2383)

* feat: Create `TypedArray` class for Frame Processor Plugins

* Type

* feat: Pass `VisionCameraProxy` along (BREAKING)

* feat: Finish implementation

* Log a bit

* feat: Successfully convert JSI <> JNI buffers

* Wrap buffer

* fix: Fix using wrong Runtime

* feat: Add docs

* add zero copy example

* Format C++

* Create iOS base

* feat: Finish iOS implementation

* chore: Format

* fix: Use `NSData` instead of `NSMutableData`

* Format

* fix: Fix build when Frame Processors are disabled

* chore: Rename `TypedArray` to `SharedArray`

* fix: Fix Swift typings for Array

* Remove a few default inits

* fix: Fix Android build

* fix: Use `NSInteger`

* Update SharedArray.mm

* fix: Expose bytes directly on iOS (NSData was immutable)
This commit is contained in:
Marc Rousavy
2024-01-12 16:00:36 +01:00
committed by GitHub
parent 56cecaa814
commit 29fe98cc44
35 changed files with 491 additions and 65 deletions

View File

@@ -19,5 +19,6 @@
#if VISION_CAMERA_ENABLE_FRAME_PROCESSORS
#import "Frame.h"
#import "FrameProcessor.h"
#import "SharedArray.h"
#import "VisionCameraProxy.h"
#endif

View File

@@ -16,6 +16,8 @@
- (instancetype _Nonnull)initWithBuffer:(CMSampleBufferRef _Nonnull)buffer orientation:(UIImageOrientation)orientation;
- (instancetype)init NS_UNAVAILABLE;
@property(nonatomic, readonly) CMSampleBufferRef _Nonnull buffer;
@property(nonatomic, readonly) UIImageOrientation orientation;

View File

@@ -21,6 +21,8 @@
@interface FrameProcessor : NSObject
- (instancetype)init NS_UNAVAILABLE;
#ifdef __cplusplus
- (instancetype _Nonnull)initWithWorklet:(std::shared_ptr<RNWorklet::JsiWorklet>)worklet
context:(std::shared_ptr<RNWorklet::JsiWorkletContext>)context;

View File

@@ -9,6 +9,7 @@
#pragma once
#import "Frame.h"
#import "VisionCameraProxy.h"
#import <Foundation/Foundation.h>
/**
@@ -28,9 +29,13 @@
* This is called everytime this Frame Processor Plugin is loaded from the JS side (`initFrameProcessorPlugin(..)`).
* Optionally override this method to implement custom initialization logic.
* - Parameters:
* - proxy: The VisionCameraProxy instance for using the Frame Processor Context, e.g. to initialize SharedArrays.
* - options: An options dictionary passed from the JS side, or `nil` if none.
*/
- (instancetype _Nonnull)initWithOptions:(NSDictionary* _Nullable)options;
- (instancetype _Nonnull)initWithProxy:(VisionCameraProxyHolder* _Nonnull)proxy
withOptions:(NSDictionary* _Nullable)options NS_SWIFT_NAME(init(proxy:options:));
- (instancetype)init NS_UNAVAILABLE;
/**
* The actual Frame Processor Plugin's implementation that runs when `plugin.call(..)` is called in the JS Frame Processor.
@@ -52,10 +57,11 @@
#define VISION_EXPORT_FRAME_PROCESSOR(frame_processor_class, frame_processor_plugin_name) \
+(void)load { \
[FrameProcessorPluginRegistry addFrameProcessorPlugin:@ #frame_processor_plugin_name \
withInitializer:^FrameProcessorPlugin*(NSDictionary* _Nullable options) { \
return [[frame_processor_class alloc] initWithOptions:options]; \
}]; \
[FrameProcessorPluginRegistry \
addFrameProcessorPlugin:@ #frame_processor_plugin_name \
withInitializer:^FrameProcessorPlugin*(VisionCameraProxyHolder* _Nonnull proxy, NSDictionary* _Nullable options) { \
return [[frame_processor_class alloc] initWithProxy:proxy withOptions:options]; \
}]; \
}
#define VISION_EXPORT_SWIFT_FRAME_PROCESSOR(frame_processor_class, frame_processor_plugin_name) \
@@ -67,8 +73,9 @@
\
__attribute__((constructor)) static void VISION_CONCAT(initialize_, frame_processor_plugin_name)(void) { \
[FrameProcessorPluginRegistry addFrameProcessorPlugin:@ #frame_processor_plugin_name \
withInitializer:^FrameProcessorPlugin* _Nonnull(NSDictionary* _Nullable options) { \
return [[frame_processor_class alloc] initWithOptions:options]; \
withInitializer:^FrameProcessorPlugin* _Nonnull(VisionCameraProxyHolder* _Nonnull proxy, \
NSDictionary* _Nullable options) { \
return [[frame_processor_class alloc] initWithProxy:proxy withOptions:options]; \
}]; \
} \
\

View File

@@ -11,7 +11,7 @@
// Base implementation (empty)
@implementation FrameProcessorPlugin
- (instancetype)initWithOptions:(NSDictionary* _Nullable)options {
- (instancetype)initWithProxy:(VisionCameraProxyHolder* _Nonnull)proxy withOptions:(NSDictionary* _Nullable)options {
self = [super init];
return self;
}

View File

@@ -10,14 +10,18 @@
#import "Frame.h"
#import "FrameProcessorPlugin.h"
#import "VisionCameraProxy.h"
#import <Foundation/Foundation.h>
@interface FrameProcessorPluginRegistry : NSObject
typedef FrameProcessorPlugin* _Nonnull (^PluginInitializerFunction)(NSDictionary* _Nullable options);
typedef FrameProcessorPlugin* _Nonnull (^PluginInitializerFunction)(VisionCameraProxyHolder* _Nonnull proxy,
NSDictionary* _Nullable options);
+ (void)addFrameProcessorPlugin:(NSString* _Nonnull)name withInitializer:(PluginInitializerFunction _Nonnull)pluginInitializer;
+ (FrameProcessorPlugin* _Nullable)getPlugin:(NSString* _Nonnull)name withOptions:(NSDictionary* _Nullable)options;
+ (FrameProcessorPlugin* _Nullable)getPlugin:(NSString* _Nonnull)name
withProxy:(VisionCameraProxyHolder* _Nonnull)proxy
withOptions:(NSDictionary* _Nullable)options;
@end

View File

@@ -31,7 +31,9 @@
NSLog(@"Successfully registered Frame Processor Plugin \"%@\"!", name);
}
+ (FrameProcessorPlugin*)getPlugin:(NSString* _Nonnull)name withOptions:(NSDictionary* _Nullable)options {
+ (FrameProcessorPlugin*)getPlugin:(NSString* _Nonnull)name
withProxy:(VisionCameraProxyHolder* _Nonnull)proxy
withOptions:(NSDictionary* _Nullable)options {
NSLog(@"Looking up Frame Processor Plugin \"%@\"...", name);
PluginInitializerFunction initializer = [[FrameProcessorPluginRegistry frameProcessorPlugins] objectForKey:name];
if (initializer == nil) {
@@ -40,7 +42,7 @@
}
NSLog(@"Frame Processor Plugin \"%@\" found! Initializing...", name);
return initializer(options);
return initializer(proxy, options);
}
@end

View File

@@ -8,6 +8,7 @@
#pragma once
#import "../Frame Processor/SharedArray.h"
#import <React/RCTBridgeModule.h>
#import <ReactCommon/CallInvoker.h>
#import <jsi/jsi.h>
@@ -32,6 +33,9 @@ jsi::Object convertNSDictionaryToJSIObject(jsi::Runtime& runtime, NSDictionary*
// NSArray -> []
jsi::Array convertNSArrayToJSIArray(jsi::Runtime& runtime, NSArray* value);
// SharedArray -> ArrayBuffer
jsi::Object convertSharedArrayToJSIArrayBuffer(jsi::Runtime& runtime, SharedArray* sharedArray);
// id -> ???
jsi::Value convertObjCObjectToJSIValue(jsi::Runtime& runtime, id value);

View File

@@ -18,6 +18,8 @@
#import "JSINSObjectConversion.h"
#import "../Frame Processor/Frame.h"
#import "../Frame Processor/FrameHostObject.h"
#import "../Frame Processor/SharedArray.h"
#import "JSITypedArray.h"
#import <Foundation/Foundation.h>
#import <React/RCTBridge.h>
#import <ReactCommon/CallInvoker.h>
@@ -58,6 +60,11 @@ jsi::Array convertNSArrayToJSIArray(jsi::Runtime& runtime, NSArray* value) {
return result;
}
jsi::Object convertSharedArrayToJSIArrayBuffer(jsi::Runtime& runtime, SharedArray* sharedArray) {
std::shared_ptr<vision::TypedArrayBase> array = sharedArray.typedArray;
return array->getBuffer(runtime);
}
jsi::Value convertObjCObjectToJSIValue(jsi::Runtime& runtime, id value) {
if (value == nil) {
return jsi::Value::undefined();
@@ -77,6 +84,8 @@ jsi::Value convertObjCObjectToJSIValue(jsi::Runtime& runtime, id value) {
} else if ([value isKindOfClass:[Frame class]]) {
auto frameHostObject = std::make_shared<FrameHostObject>((Frame*)value);
return jsi::Object::createFromHostObject(runtime, frameHostObject);
} else if ([value isKindOfClass:[SharedArray class]]) {
return convertSharedArrayToJSIArrayBuffer(runtime, (SharedArray*)value);
}
return jsi::Value::undefined();
}
@@ -132,36 +141,46 @@ NSDictionary* convertJSIObjectToNSDictionary(jsi::Runtime& runtime, const jsi::O
id convertJSIValueToObjCObject(jsi::Runtime& runtime, const jsi::Value& value, std::shared_ptr<CallInvoker> jsInvoker) {
if (value.isUndefined() || value.isNull()) {
// undefined/null
return nil;
}
if (value.isBool()) {
} else if (value.isBool()) {
// bool
return @(value.getBool());
}
if (value.isNumber()) {
} else if (value.isNumber()) {
// number
return @(value.getNumber());
}
if (value.isString()) {
} else if (value.isString()) {
// string
return convertJSIStringToNSString(runtime, value.getString(runtime));
}
if (value.isObject()) {
} else if (value.isObject()) {
// object
jsi::Object o = value.getObject(runtime);
if (o.isArray(runtime)) {
// array[]
return convertJSIArrayToNSArray(runtime, o.getArray(runtime), jsInvoker);
}
if (o.isFunction(runtime)) {
} else if (o.isFunction(runtime)) {
// function () => {}
return convertJSIFunctionToCallback(runtime, std::move(o.getFunction(runtime)), jsInvoker);
}
if (o.isHostObject(runtime)) {
auto hostObject = o.asHostObject(runtime);
auto frame = dynamic_cast<FrameHostObject*>(hostObject.get());
if (frame != nullptr) {
return frame->frame;
} else if (o.isHostObject(runtime)) {
if (o.isHostObject<FrameHostObject>(runtime)) {
// Frame
auto hostObject = o.getHostObject<FrameHostObject>(runtime);
return hostObject->frame;
} else {
throw std::runtime_error("The given HostObject is not supported by a Frame Processor Plugin!");
}
} else if (o.isArrayBuffer(runtime)) {
// ArrayBuffer
auto typedArray = std::make_shared<vision::TypedArrayBase>(vision::getTypedArray(runtime, o));
return [[SharedArray alloc] initWithRuntime:runtime typedArray:typedArray];
} else {
// object
return convertJSIObjectToNSDictionary(runtime, o, jsInvoker);
}
return convertJSIObjectToNSDictionary(runtime, o, jsInvoker);
}
throw std::runtime_error("Unsupported jsi::jsi::Value kind");
auto stringRepresentation = value.toString(runtime).utf8(runtime);
throw std::runtime_error("Failed to convert jsi::Value to JNI value - unsupported type! " + stringRepresentation);
}
RCTResponseSenderBlock convertJSIFunctionToCallback(jsi::Runtime& runtime, const jsi::Function& value,

View File

@@ -0,0 +1,48 @@
//
// SharedArray.h
// VisionCamera
//
// Created by Marc Rousavy on 12.01.24.
// Copyright © 2024 mrousavy. All rights reserved.
//
#pragma once
#import "VisionCameraProxy.h"
#import <Foundation/Foundation.h>
#ifdef __cplusplus
#import "JSITypedArray.h"
#import <jsi/jsi.h>
using namespace facebook;
#endif
// Needs to be in sync with JSITypedArray.h as the index is used
typedef NS_ENUM(NSInteger, SharedArrayType) {
Int8Array,
Int16Array,
Int32Array,
Uint8Array,
Uint8ClampedArray,
Uint16Array,
Uint32Array,
Float32Array,
Float64Array,
};
@interface SharedArray : NSObject
- (instancetype _Nonnull)init NS_UNAVAILABLE;
- (instancetype _Nonnull)initWithProxy:(VisionCameraProxyHolder* _Nonnull)proxy type:(SharedArrayType)type size:(NSInteger)size;
#ifdef __cplusplus
- (instancetype _Nonnull)initWithRuntime:(jsi::Runtime&)runtime typedArray:(std::shared_ptr<vision::TypedArrayBase>)typedArray;
- (std::shared_ptr<vision::TypedArrayBase>)typedArray;
#endif
@property(nonatomic, readonly, nonnull) uint8_t* data;
@property(nonatomic, readonly) NSInteger count;
@end

View File

@@ -0,0 +1,58 @@
//
// SharedArray.mm
// VisionCamera
//
// Created by Marc Rousavy on 12.01.24.
// Copyright © 2024 mrousavy. All rights reserved.
//
#import "SharedArray.h"
#import "JSITypedArray.h"
#import <Foundation/Foundation.h>
#import <jsi/jsi.h>
using namespace facebook;
@implementation SharedArray {
uint8_t* _data;
NSInteger _count;
std::shared_ptr<vision::TypedArrayBase> _array;
}
vision::TypedArrayKind getTypedArrayKind(int unsafeEnumValue) {
return static_cast<vision::TypedArrayKind>(unsafeEnumValue);
}
- (instancetype)initWithProxy:(VisionCameraProxyHolder*)proxy type:(SharedArrayType)type size:(NSInteger)size {
if (self = [super init]) {
jsi::Runtime& runtime = proxy.proxy->getWorkletRuntime();
vision::TypedArrayKind kind = getTypedArrayKind((int)type);
_array = std::make_shared<vision::TypedArrayBase>(vision::TypedArrayBase(runtime, size, kind));
_data = _array->getBuffer(runtime).data(runtime);
_count = size;
}
return self;
}
- (instancetype)initWithRuntime:(jsi::Runtime&)runtime typedArray:(std::shared_ptr<vision::TypedArrayBase>)typedArray {
if (self = [super init]) {
_array = typedArray;
_data = _array->getBuffer(runtime).data(runtime);
_count = _array->getBuffer(runtime).size(runtime);
}
return self;
}
- (std::shared_ptr<vision::TypedArrayBase>)typedArray {
return _array;
}
- (uint8_t*)data {
return _data;
}
- (NSInteger)count {
return _count;
}
@end

View File

@@ -27,6 +27,10 @@ public:
std::vector<jsi::PropNameID> getPropertyNames(jsi::Runtime& runtime) override;
jsi::Value get(jsi::Runtime& runtime, const jsi::PropNameID& name) override;
jsi::Runtime& getWorkletRuntime() {
return _workletContext->getWorkletRuntime();
}
private:
void setFrameProcessor(jsi::Runtime& runtime, int viewTag, const jsi::Object& frameProcessor);
void removeFrameProcessor(jsi::Runtime& runtime, int viewTag);
@@ -38,6 +42,18 @@ private:
};
#endif
@interface VisionCameraInstaller : NSObject
+ (BOOL)installToBridge:(RCTBridge* _Nonnull)bridge;
@interface VisionCameraProxyHolder : NSObject
- (_Nonnull instancetype)initWithProxy:(void* _Nonnull)proxy;
#ifdef __cplusplus
- (VisionCameraProxy* _Nonnull)proxy;
#endif
@end
@interface VisionCameraInstaller : NSObject
+ (BOOL)installToBridge:(RCTBridge* _Nonnull)bridge;
@end

View File

@@ -97,7 +97,8 @@ void VisionCameraProxy::removeFrameProcessor(jsi::Runtime& runtime, int viewTag)
jsi::Value VisionCameraProxy::initFrameProcessorPlugin(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];
VisionCameraProxyHolder* proxy = [[VisionCameraProxyHolder alloc] initWithProxy:this];
FrameProcessorPlugin* plugin = [FrameProcessorPluginRegistry getPlugin:key withProxy:proxy withOptions:optionsObjc];
if (plugin == nil) {
return jsi::Value::undefined();
}
@@ -145,7 +146,25 @@ jsi::Value VisionCameraProxy::get(jsi::Runtime& runtime, const jsi::PropNameID&
return jsi::Value::undefined();
}
@implementation VisionCameraProxyHolder {
VisionCameraProxy* _proxy;
}
- (instancetype)initWithProxy:(void*)proxy {
if (self = [super init]) {
_proxy = (VisionCameraProxy*)proxy;
}
return self;
}
- (VisionCameraProxy*)proxy {
return _proxy;
}
@end
@implementation VisionCameraInstaller
+ (BOOL)installToBridge:(RCTBridge* _Nonnull)bridge {
RCTCxxBridge* cxxBridge = (RCTCxxBridge*)[RCTBridge currentBridge];
if (!cxxBridge.runtime) {
@@ -160,4 +179,5 @@ jsi::Value VisionCameraProxy::get(jsi::Runtime& runtime, const jsi::PropNameID&
return YES;
}
@end

View File

@@ -105,6 +105,8 @@
B81D41EF263C86F900B041FD /* JSINSObjectConversion.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JSINSObjectConversion.h; sourceTree = "<group>"; };
B8207AAC2B0E5DD70002990F /* AVCaptureSession+synchronizeBuffer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVCaptureSession+synchronizeBuffer.swift"; sourceTree = "<group>"; };
B8207AAE2B0E67460002990F /* AVCaptureVideoDataOutput+recommendedVideoSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVCaptureVideoDataOutput+recommendedVideoSettings.swift"; sourceTree = "<group>"; };
B82186C72B514B5F00CE68CE /* SharedArray.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SharedArray.h; sourceTree = "<group>"; };
B82186C82B514B6D00CE68CE /* SharedArray.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = SharedArray.mm; sourceTree = "<group>"; };
B83D5EE629377117000AFD2F /* PreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewView.swift; sourceTree = "<group>"; };
B8446E4C2ABA147C00E56077 /* CameraDevicesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraDevicesManager.swift; sourceTree = "<group>"; };
B8446E4F2ABA14C900E56077 /* CameraDevicesManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraDevicesManager.m; sourceTree = "<group>"; };
@@ -336,6 +338,8 @@
B8994E6B263F03E100069589 /* JSINSObjectConversion.mm */,
B85F7AE82A77BB680089C539 /* FrameProcessorPlugin.m */,
B89A79692B3EF60F005E0357 /* UIImageOrientation+descriptor.h */,
B82186C72B514B5F00CE68CE /* SharedArray.h */,
B82186C82B514B6D00CE68CE /* SharedArray.mm */,
);
path = "Frame Processor";
sourceTree = "<group>";