feat: Draw onto Frame as if it was a Skia Canvas (#1479)
* Create Shaders.ts * Add `previewType` and `enableFpsGraph` * Add RN Skia native dependency * Add Skia Preview View on iOS * Pass 1 * Update FrameHostObject.mm * Wrap Canvas * Lockfiles * fix: Fix stuff * chore: Upgrade RNWorklets * Add `previewType` to set the Preview * feat: Add Example * Update project.pbxproj * `enableFpsGraph` * Cache the `std::shared_ptr<FrameHostObject>` * Update CameraView+RecordVideo.swift * Update SkiaMetalCanvasProvider.mm * Android: Integrate Skia Dependency * fix: Use new Prefix * Add example for rendering shader * chore: Upgrade CameraX * Remove KTX * Enable `viewBinding` * Revert "Enable `viewBinding`" This reverts commit f2a603f53b33ea4311a296422ffd1a910ce03f9e. * Revert "chore: Upgrade CameraX" This reverts commit 8dc832cf8754490d31a6192e6c1a1f11cdcd94fe. * Remove unneeded `ProcessCameraProvider.getInstance()` call * fix: Add REA hotfix patch * fix: Fix FrameHostObject dead in runAsync * fix: Make `runAsync` run truly async by dropping new Frames while executing * chore: Upgrade RN Worklets to latest * chore: Upgrade RN Skia * Revert "Remove KTX" This reverts commit 253f586633f7af2da992d2279fc206dc62597129. * Make Skia optional in CMake * Fix import * Update CMakeLists.txt * Update build.gradle * Update CameraView.kt * Update CameraView.kt * Update CameraView.kt * Update Shaders.ts * Center Blur * chore: Upgrade RN Worklets * feat: Add `toByteArray()`, `orientation`, `isMirrored` and `timestamp` to `Frame` (#1487) * feat: Implement `orientation` and `isMirrored` on Frame * feat: Add `toArrayBuffer()` func * perf: Do faster buffer copy * feat: Implement `toArrayBuffer()` on Android * feat: Add `orientation` and `isMirrored` to Android * feat: Add `timestamp` to Frame * Update Frame.ts * Update JImageProxy.h * Update FrameHostObject.cpp * Update FrameHostObject.cpp * Update CameraPage.tsx * fix: Format Swift
This commit is contained in:
@@ -10,13 +10,20 @@
|
||||
|
||||
#import <jsi/jsi.h>
|
||||
#import <CoreMedia/CMSampleBuffer.h>
|
||||
|
||||
#import "Frame.h"
|
||||
|
||||
#import "SkCanvas.h"
|
||||
#import "JsiSkCanvas.h"
|
||||
|
||||
using namespace facebook;
|
||||
|
||||
class JSI_EXPORT FrameHostObject: public jsi::HostObject {
|
||||
public:
|
||||
explicit FrameHostObject(Frame* frame): frame(frame) { }
|
||||
explicit FrameHostObject(Frame* frame): frame(frame) {}
|
||||
explicit FrameHostObject(Frame* frame,
|
||||
std::shared_ptr<RNSkia::JsiSkCanvas> canvas):
|
||||
frame(frame), canvas(canvas) {}
|
||||
|
||||
public:
|
||||
jsi::Value get(jsi::Runtime&, const jsi::PropNameID& name) override;
|
||||
@@ -24,4 +31,5 @@ public:
|
||||
|
||||
public:
|
||||
Frame* frame;
|
||||
std::shared_ptr<RNSkia::JsiSkCanvas> canvas;
|
||||
};
|
||||
|
||||
@@ -9,8 +9,11 @@
|
||||
#import "FrameHostObject.h"
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <jsi/jsi.h>
|
||||
#import "JsiHostObject.h"
|
||||
#import "JsiSharedValue.h"
|
||||
#import "WKTJsiHostObject.h"
|
||||
|
||||
#import "SkCanvas.h"
|
||||
#import "../Skia Render Layer/SkImageHelpers.h"
|
||||
#import "../../cpp/JSITypedArray.h"
|
||||
|
||||
std::vector<jsi::PropNameID> FrameHostObject::getPropertyNames(jsi::Runtime& rt) {
|
||||
std::vector<jsi::PropNameID> result;
|
||||
@@ -18,15 +21,37 @@ std::vector<jsi::PropNameID> FrameHostObject::getPropertyNames(jsi::Runtime& rt)
|
||||
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("height")));
|
||||
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("bytesPerRow")));
|
||||
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("planesCount")));
|
||||
// Debugging
|
||||
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("orientation")));
|
||||
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("isMirrored")));
|
||||
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("timestamp")));
|
||||
// Conversion
|
||||
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("toString")));
|
||||
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("toArrayBuffer")));
|
||||
// Ref Management
|
||||
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("isValid")));
|
||||
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("incrementRefCount")));
|
||||
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("decrementRefCount")));
|
||||
// Skia
|
||||
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("render")));
|
||||
|
||||
if (canvas != nullptr) {
|
||||
auto canvasPropNames = canvas->getPropertyNames(rt);
|
||||
for (auto& prop : canvasPropNames) {
|
||||
result.push_back(std::move(prop));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
SkRect inscribe(SkSize size, SkRect rect) {
|
||||
auto halfWidthDelta = (rect.width() - size.width()) / 2.0;
|
||||
auto halfHeightDelta = (rect.height() - size.height()) / 2.0;
|
||||
return SkRect::MakeXYWH(rect.x() + halfWidthDelta,
|
||||
rect.y() + halfHeightDelta, size.width(),
|
||||
size.height());
|
||||
}
|
||||
|
||||
jsi::Value FrameHostObject::get(jsi::Runtime& runtime, const jsi::PropNameID& propName) {
|
||||
auto name = propName.utf8(runtime);
|
||||
|
||||
@@ -55,7 +80,7 @@ jsi::Value FrameHostObject::get(jsi::Runtime& runtime, const jsi::PropNameID& pr
|
||||
0,
|
||||
incrementRefCount);
|
||||
}
|
||||
|
||||
|
||||
if (name == "decrementRefCount") {
|
||||
auto decrementRefCount = JSI_HOST_FUNCTION_LAMBDA {
|
||||
// Decrement retain count by one. If the retain count is zero, ARC will destroy the Frame Buffer.
|
||||
@@ -67,6 +92,59 @@ jsi::Value FrameHostObject::get(jsi::Runtime& runtime, const jsi::PropNameID& pr
|
||||
0,
|
||||
decrementRefCount);
|
||||
}
|
||||
if (name == "render") {
|
||||
auto render = JSI_HOST_FUNCTION_LAMBDA {
|
||||
if (canvas == nullptr) {
|
||||
throw jsi::JSError(runtime, "Trying to render a Frame without a Skia Canvas! Did you install Skia?");
|
||||
}
|
||||
|
||||
// convert CMSampleBuffer to SkImage
|
||||
auto context = canvas->getCanvas()->recordingContext();
|
||||
auto image = SkImageHelpers::convertCMSampleBufferToSkImage(context, frame.buffer);
|
||||
|
||||
// draw SkImage
|
||||
if (count > 0) {
|
||||
// ..with paint/shader
|
||||
auto paintHostObject = arguments[0].asObject(runtime).asHostObject<RNSkia::JsiSkPaint>(runtime);
|
||||
auto paint = paintHostObject->getObject();
|
||||
canvas->getCanvas()->drawImage(image, 0, 0, SkSamplingOptions(), paint.get());
|
||||
} else {
|
||||
// ..without paint/shader
|
||||
canvas->getCanvas()->drawImage(image, 0, 0);
|
||||
}
|
||||
|
||||
return jsi::Value::undefined();
|
||||
};
|
||||
return jsi::Function::createFromHostFunction(runtime, jsi::PropNameID::forUtf8(runtime, "render"), 1, render);
|
||||
}
|
||||
if (name == "toArrayBuffer") {
|
||||
auto toArrayBuffer = JSI_HOST_FUNCTION_LAMBDA {
|
||||
auto pixelBuffer = CMSampleBufferGetImageBuffer(frame.buffer);
|
||||
auto bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer);
|
||||
auto height = CVPixelBufferGetHeight(pixelBuffer);
|
||||
auto buffer = (uint8_t*) CVPixelBufferGetBaseAddress(pixelBuffer);
|
||||
auto arraySize = bytesPerRow * height;
|
||||
|
||||
static constexpr auto ARRAYBUFFER_CACHE_PROP_NAME = "__frameArrayBufferCache";
|
||||
if (!runtime.global().hasProperty(runtime, ARRAYBUFFER_CACHE_PROP_NAME)) {
|
||||
vision::TypedArray<vision::TypedArrayKind::Uint8ClampedArray> arrayBuffer(runtime, arraySize);
|
||||
runtime.global().setProperty(runtime, ARRAYBUFFER_CACHE_PROP_NAME, arrayBuffer);
|
||||
}
|
||||
|
||||
auto arrayBufferCache = runtime.global().getPropertyAsObject(runtime, ARRAYBUFFER_CACHE_PROP_NAME);
|
||||
auto arrayBuffer = vision::getTypedArray(runtime, arrayBufferCache).get<vision::TypedArrayKind::Uint8ClampedArray>(runtime);
|
||||
|
||||
if (arrayBuffer.size(runtime) != arraySize) {
|
||||
arrayBuffer = vision::TypedArray<vision::TypedArrayKind::Uint8ClampedArray>(runtime, arraySize);
|
||||
runtime.global().setProperty(runtime, ARRAYBUFFER_CACHE_PROP_NAME, arrayBuffer);
|
||||
}
|
||||
|
||||
arrayBuffer.updateUnsafe(runtime, buffer, arraySize);
|
||||
|
||||
return arrayBuffer;
|
||||
};
|
||||
return jsi::Function::createFromHostFunction(runtime, jsi::PropNameID::forUtf8(runtime, "toArrayBuffer"), 0, toArrayBuffer);
|
||||
}
|
||||
|
||||
if (name == "isValid") {
|
||||
auto isValid = frame != nil && frame.buffer != nil && CFGetRetainCount(frame.buffer) > 0 && CMSampleBufferIsValid(frame.buffer);
|
||||
@@ -82,6 +160,41 @@ jsi::Value FrameHostObject::get(jsi::Runtime& runtime, const jsi::PropNameID& pr
|
||||
auto height = CVPixelBufferGetHeight(imageBuffer);
|
||||
return jsi::Value((double) height);
|
||||
}
|
||||
if (name == "orientation") {
|
||||
switch (frame.orientation) {
|
||||
case UIImageOrientationUp:
|
||||
case UIImageOrientationUpMirrored:
|
||||
return jsi::String::createFromUtf8(runtime, "portrait");
|
||||
case UIImageOrientationDown:
|
||||
case UIImageOrientationDownMirrored:
|
||||
return jsi::String::createFromUtf8(runtime, "portraitUpsideDown");
|
||||
case UIImageOrientationLeft:
|
||||
case UIImageOrientationLeftMirrored:
|
||||
return jsi::String::createFromUtf8(runtime, "landscapeLeft");
|
||||
case UIImageOrientationRight:
|
||||
case UIImageOrientationRightMirrored:
|
||||
return jsi::String::createFromUtf8(runtime, "landscapeRight");
|
||||
}
|
||||
}
|
||||
if (name == "isMirrored") {
|
||||
switch (frame.orientation) {
|
||||
case UIImageOrientationUp:
|
||||
case UIImageOrientationDown:
|
||||
case UIImageOrientationLeft:
|
||||
case UIImageOrientationRight:
|
||||
return jsi::Value(false);
|
||||
case UIImageOrientationDownMirrored:
|
||||
case UIImageOrientationUpMirrored:
|
||||
case UIImageOrientationLeftMirrored:
|
||||
case UIImageOrientationRightMirrored:
|
||||
return jsi::Value(true);
|
||||
}
|
||||
}
|
||||
if (name == "timestamp") {
|
||||
auto timestamp = CMSampleBufferGetPresentationTimeStamp(frame.buffer);
|
||||
auto seconds = static_cast<double>(CMTimeGetSeconds(timestamp));
|
||||
return jsi::Value(seconds * 1000.0);
|
||||
}
|
||||
if (name == "bytesPerRow") {
|
||||
auto imageBuffer = CMSampleBufferGetImageBuffer(frame.buffer);
|
||||
auto bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer);
|
||||
@@ -93,6 +206,11 @@ jsi::Value FrameHostObject::get(jsi::Runtime& runtime, const jsi::PropNameID& pr
|
||||
return jsi::Value((double) planesCount);
|
||||
}
|
||||
|
||||
if (canvas != nullptr) {
|
||||
// If we have a Canvas, try to access the property on there.
|
||||
return canvas->get(runtime, propName);
|
||||
}
|
||||
|
||||
// fallback to base implementation
|
||||
return HostObject::get(runtime, propName);
|
||||
}
|
||||
|
||||
@@ -11,4 +11,4 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "Frame.h"
|
||||
|
||||
typedef void (^FrameProcessorCallback) (Frame* frame);
|
||||
typedef void (^FrameProcessorCallback) (Frame* frame, void* skCanvas);
|
||||
|
||||
@@ -19,10 +19,10 @@
|
||||
#import <React/RCTUIManager.h>
|
||||
#import <ReactCommon/RCTTurboModuleManager.h>
|
||||
|
||||
#import "JsiWorkletContext.h"
|
||||
#import "JsiWorkletApi.h"
|
||||
#import "JsiWorklet.h"
|
||||
#import "JsiHostObject.h"
|
||||
#import "WKTJsiWorkletContext.h"
|
||||
#import "WKTJsiWorkletApi.h"
|
||||
#import "WKTJsiWorklet.h"
|
||||
#import "WKTJsiHostObject.h"
|
||||
|
||||
#import "FrameProcessorUtils.h"
|
||||
#import "FrameProcessorCallback.h"
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
#endif
|
||||
|
||||
#import <jsi/jsi.h>
|
||||
#import "JsiWorklet.h"
|
||||
#import "WKTJsiWorklet.h"
|
||||
#import <memory>
|
||||
|
||||
using namespace facebook;
|
||||
|
||||
@@ -19,22 +19,41 @@
|
||||
#import "JSConsoleHelper.h"
|
||||
#import <ReactCommon/RCTTurboModule.h>
|
||||
|
||||
#import "JsiWorklet.h"
|
||||
#import "WKTJsiWorklet.h"
|
||||
|
||||
#import "RNSkPlatformContext.h"
|
||||
#import "RNSkiOSPlatformContext.h"
|
||||
#import "JsiSkCanvas.h"
|
||||
|
||||
FrameProcessorCallback convertWorkletToFrameProcessorCallback(jsi::Runtime& runtime, std::shared_ptr<RNWorklet::JsiWorklet> worklet) {
|
||||
|
||||
// Wrap Worklet call in invoker
|
||||
auto workletInvoker = std::make_shared<RNWorklet::WorkletInvoker>(worklet);
|
||||
|
||||
// Create cached Skia Canvas object
|
||||
auto callInvoker = RCTBridge.currentBridge.jsCallInvoker;
|
||||
auto skiaPlatformContext = std::make_shared<RNSkia::RNSkiOSPlatformContext>(&runtime, callInvoker);
|
||||
auto canvasHostObject = std::make_shared<RNSkia::JsiSkCanvas>(skiaPlatformContext);
|
||||
|
||||
// Converts a Worklet to a callable Objective-C block function
|
||||
return ^(Frame* frame) {
|
||||
return ^(Frame* frame, void* skiaCanvas) {
|
||||
|
||||
try {
|
||||
// Box the Frame to a JS Host Object
|
||||
// Create cached Frame object
|
||||
auto frameHostObject = std::make_shared<FrameHostObject>(frame);
|
||||
// Update cached Canvas object
|
||||
if (skiaCanvas != nullptr) {
|
||||
canvasHostObject->setCanvas((SkCanvas*)skiaCanvas);
|
||||
frameHostObject->canvas = canvasHostObject;
|
||||
} else {
|
||||
frameHostObject->canvas = nullptr;
|
||||
}
|
||||
|
||||
auto argument = jsi::Object::createFromHostObject(runtime, frameHostObject);
|
||||
jsi::Value jsValue(std::move(argument));
|
||||
// Call the Worklet with the Frame JS Host Object as an argument
|
||||
workletInvoker->call(runtime, jsi::Value::undefined(), &jsValue, 1);
|
||||
|
||||
// After the sync Frame Processor finished executing, remove the Canvas on that Frame instance. It can no longer draw.
|
||||
frameHostObject->canvas = nullptr;
|
||||
} catch (jsi::JSError& jsError) {
|
||||
// JS Error occured, print it to console.
|
||||
auto stack = std::regex_replace(jsError.getStack(), std::regex("\n"), "\n ");
|
||||
|
||||
Reference in New Issue
Block a user