diff --git a/android/CMakeLists.txt b/android/CMakeLists.txt index c9dbc16..2279125 100644 --- a/android/CMakeLists.txt +++ b/android/CMakeLists.txt @@ -13,22 +13,6 @@ file(GLOB libfbjni_include_DIRS "${build_DIR}/fbjni-*-headers.jar/") # Consume shared libraries and headers from prefabs find_package(fbjni REQUIRED CONFIG) find_package(ReactAndroid REQUIRED CONFIG) -find_package(shopify_react-native-skia REQUIRED CONFIG) -find_package(react-native-reanimated REQUIRED CONFIG) - -set(JSI_LIB ReactAndroid::jsi) -message("-- JSI : " ${JSI_LIB}) -set(FBJNI_LIBRARY fbjni::fbjni) -message("-- FBJNI : " ${FBJNI_LIBRARY}) -set(REACT_LIB ReactAndroid::react_nativemodule_core) -message("-- REACT : " ${REACT_LIB}) -set(TURBOMODULES_LIB "ReactAndroid::turbomodulejsijni") -message("-- TURBO : " ${TURBOMODULES_LIB}) -set(RNSKIA_LIB shopify_react-native-skia::rnskia) -message("-- RNSKIA : " ${RNSKIA_LIB}) -set(REANIMATED_LIB react-native-reanimated::reanimated) -message("-- REANIMATED : " ${REANIMATED_LIB}) - add_library(${PACKAGE_NAME} SHARED @@ -65,88 +49,56 @@ include_directories( # React native "${NODE_MODULES_DIR}/react-native/ReactCommon/callinvoker" "${NODE_MODULES_DIR}/react-native/ReactCommon/jsi" - "${NODE_MODULES_DIR}/react-native/ReactCommon/runtimeexecutor" "${NODE_MODULES_DIR}/react-native/ReactCommon" "${NODE_MODULES_DIR}/react-native/ReactCommon/react/nativemodule/core" "${NODE_MODULES_DIR}/react-native/ReactAndroid/src/main/java/com/facebook/react/turbomodule/core/jni" - # include skia headers - "${NODE_MODULES_DIR}/@shopify/react-native-skia/cpp/rnskia" - "${NODE_MODULES_DIR}/@shopify/react-native-skia/cpp/skia/include/config/" - "${NODE_MODULES_DIR}/@shopify/react-native-skia/cpp/skia/include/core/" - "${NODE_MODULES_DIR}/@shopify/react-native-skia/cpp/skia/include/effects/" - "${NODE_MODULES_DIR}/@shopify/react-native-skia/cpp/skia/include/utils/" - "${NODE_MODULES_DIR}/@shopify/react-native-skia/cpp/skia/include/pathops/" - "${NODE_MODULES_DIR}/@shopify/react-native-skia/cpp/skia/modules/" - "${NODE_MODULES_DIR}/@shopify/react-native-skia/cpp/skia/modules/skparagraph/include/" - "${NODE_MODULES_DIR}/@shopify/react-native-skia/cpp/skia/modules/skresources/include/" - "${NODE_MODULES_DIR}/@shopify/react-native-skia/cpp/skia/include/" - "${NODE_MODULES_DIR}/@shopify/react-native-skia/cpp/skia" - - "${NODE_MODULES_DIR}/@shopify/react-native-skia/cpp/api" - "${NODE_MODULES_DIR}/@shopify/react-native-skia/cpp/jsi" - "${NODE_MODULES_DIR}/@shopify/react-native-skia/cpp/rnskia" - "${NODE_MODULES_DIR}/@shopify/react-native-skia/cpp/rnskia/values" - "${NODE_MODULES_DIR}/@shopify/react-native-skia/cpp/rnskia/dom" - "${NODE_MODULES_DIR}/@shopify/react-native-skia/cpp/rnskia/dom/base" - "${NODE_MODULES_DIR}/@shopify/react-native-skia/cpp/rnskia/dom/nodes" - "${NODE_MODULES_DIR}/@shopify/react-native-skia/cpp/rnskia/dom/props" - "${NODE_MODULES_DIR}/@shopify/react-native-skia/cpp/utils" - "${NODE_MODULES_DIR}/@shopify/react-native-skia/android/cpp/jni/include" - "${NODE_MODULES_DIR}/@shopify/react-native-skia/android/cpp/rnskia-android" - ${libfbjni_include_DIRS} ) -# Import prebuilt skia library -set(SKIA_LIBS_PATH "${NODE_MODULES_DIR}/@shopify/react-native-skia/libs/android/${ANDROID_ABI}") - -set(SKIA_LIB "skia") -add_library(skia STATIC IMPORTED) -set_property(TARGET skia PROPERTY IMPORTED_LOCATION "${SKIA_LIBS_PATH}/libskia.a") - -set(SKIA_SVG "svg") -add_library(svg STATIC IMPORTED) -set_property(TARGET svg PROPERTY IMPORTED_LOCATION "${SKIA_LIBS_PATH}/libsvg.a") - -set(SKIA_SKSHAPER_LIB "skshaper") -add_library(skshaper STATIC IMPORTED) -set_property(TARGET skshaper PROPERTY IMPORTED_LOCATION "${SKIA_LIBS_PATH}/libskshaper.a") - -set(SKIA_MODULE_SKSG_LIB "sksg") -add_library(sksg STATIC IMPORTED) -set_property(TARGET sksg PROPERTY IMPORTED_LOCATION "${SKIA_LIBS_PATH}/libsksg.a") - -set(SKIA_MODULE_SKUNICODE_LIB "skunicode") -add_library(skunicode STATIC IMPORTED) -set_property(TARGET skunicode PROPERTY IMPORTED_LOCATION "${SKIA_LIBS_PATH}/libskunicode.a") - - -# Android Log lib -find_library(LOG_LIB log) - +find_library( + LOG_LIB + log +) add_definitions(-DGL_GLEXT_PROTOTYPES) add_definitions(-DEGL_EGLEXT_PROTOTYPES) -# Link target_link_libraries( ${PACKAGE_NAME} ${LOG_LIB} - ${REANIMATED_LIB} - ${FBJNI_LIBRARY} - ${REACT_LIB} - ${JSI_LIB} - ${TURBOMODULES_LIB} - ${SKIA_SKSHAPER_LIB} - ${SKIA_SVG} - ${SKIA_MODULE_SKSG_LIB} - ${SKIA_MODULE_SKUNICODE_LIB} - ${SKIA_LIB} - ${RNSKIA_LIB} + android + fbjni::fbjni + ReactAndroid::react_nativemodule_core + ReactAndroid::jsi + ReactAndroid::reactnativejni + ReactAndroid::runtimeexecutor + ReactAndroid::turbomodulejsijni -ljnigraphics -lGLESv2 -lEGL -landroid ) + +find_package(shopify_react-native-skia REQUIRED CONFIG) +target_link_libraries( + ${PACKAGE_NAME} + shopify_react-native-skia::rnskia +) + +get_target_property(RN_SKIA_INCLUDE_DIR shopify_react-native-skia::rnskia INTERFACE_INCLUDE_DIRECTORIES) +include_directories("${RN_SKIA_INCLUDE_DIR}/react-native-skia") + + +set(RN_SKIA_DIR "${NODE_MODULES_DIR}/@shopify/react-native-skia") +file(GLOB skiaLibraries "${RN_SKIA_DIR}/libs/android/${ANDROID_ABI}/*.a") +foreach(skiaLibrary ${skiaLibraries}) + target_link_libraries(${PACKAGE_NAME} "${skiaLibrary}") +endforeach() + +find_package(react-native-reanimated REQUIRED CONFIG) +target_link_libraries( + ${PACKAGE_NAME} + react-native-reanimated::reanimated +) \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index ee8b783..c5c2e89 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,5 +1,8 @@ buildscript { repositories { + maven { + url "https://plugins.gradle.org/m2/" + } google() mavenCentral() } @@ -111,12 +114,20 @@ android { excludes = [ "META-INF", "META-INF/**", - "**/librnskia.so", + "**/libc++_shared.so", + "**/libfbjni.so", "**/libjsi.so", - "**/libreact_nativemodule_core.so", + "**/libfolly_json.so", + "**/libfolly_runtime.so", + "**/libglog.so", + "**/libhermes.so", + "**/libhermes-executor-debug.so", + "**/libhermes_executor.so", + "**/libreactnativejni.so", "**/libturbomodulejsijni.so", - "**/libc++_shared.so", - "**/libfbjni.so" + "**/libreact_nativemodule_core.so", + "**/libjscexecutor.so", + "**/libruntimeexecutor.so" ] } @@ -161,10 +172,7 @@ repositories { def MEDIA3_VERSION = "1.2.1" dependencies { - // For < 0.71, this will be from the local maven repo - // For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin - //noinspection GradleDynamicVersion - implementation "com.facebook.react:react-native:+" + implementation 'com.facebook.react:react-android:+' implementation(project(":shopify_react-native-skia")) { exclude group: "com.facebook.react", module: "react-native" } @@ -175,6 +183,20 @@ dependencies { implementation "androidx.media3:media3-exoplayer-hls:${MEDIA3_VERSION}" } +tasks.whenTaskAdded { task -> + if (task.name.contains("configureCMakeDebug")) { + rootProject.getTasksByName("packageReactNdkDebugLibs", true).forEach { + task.dependsOn(it) + } + } + + if (task.name.contains("configureCMakeRel")) { + rootProject.getTasksByName("packageReactNdkReleaseLibs", true).forEach { + task.dependsOn(it) + } + } +} + if (isNewArchitectureEnabled()) { react { jsRootDir = file("../src/") diff --git a/android/cpp/JNIHelpers.h b/android/cpp/JNIHelpers.h index e0a9126..4b7d911 100644 --- a/android/cpp/JNIHelpers.h +++ b/android/cpp/JNIHelpers.h @@ -1,8 +1,8 @@ #pragma once -#include -#include #include +#include +#include namespace RNSkiaVideo { diff --git a/android/cpp/VideoComposition.h b/android/cpp/VideoComposition.h index 5044526..abfa93b 100644 --- a/android/cpp/VideoComposition.h +++ b/android/cpp/VideoComposition.h @@ -1,9 +1,9 @@ #pragma once -#include #include #include #include +#include namespace RNSkiaVideo { diff --git a/android/cpp/VideoCompositionExporter.cpp b/android/cpp/VideoCompositionExporter.cpp index 38c9369..5cdec31 100644 --- a/android/cpp/VideoCompositionExporter.cpp +++ b/android/cpp/VideoCompositionExporter.cpp @@ -4,9 +4,9 @@ #include "VideoCompositionExporter.h" #include -#include -#include -#include +#include +#include +#include #include "JNIHelpers.h" @@ -18,7 +18,8 @@ jsi::Value VideoCompositionExporter::exportVideoComposition( jsi::Runtime& runtime, jsi::Object jsComposition, jsi::Object options, std::shared_ptr workletRuntime, std::shared_ptr drawFrame, - jsi::Function onSuccess, jsi::Function onError) { + jsi::Function onSuccess, jsi::Function onError, + std::shared_ptr jsiSurface) { auto composition = VideoComposition::fromJSIObject(runtime, jsComposition); auto outPath = @@ -38,7 +39,7 @@ jsi::Value VideoCompositionExporter::exportVideoComposition( global_ref exporter = VideoCompositionExporter::create(composition, outPath, width, height, frameRate, bitRate, encoderName, - workletRuntime, drawFrame); + workletRuntime, drawFrame, jsiSurface); auto sharedSuccessCallback = std::make_shared(std::move(onSuccess)); @@ -80,10 +81,11 @@ global_ref VideoCompositionExporter::create( int width, int height, int frameRate, int bitRate, std::optional encoderName, std::shared_ptr workletRuntime, - std::shared_ptr drawFrame) { + std::shared_ptr drawFrame, + std::shared_ptr jsiSurface) { auto hybridData = makeHybridData(std::make_unique( - width, height, workletRuntime, drawFrame)); + width, height, workletRuntime, drawFrame, jsiSurface)); auto jExporter = newObjectJavaArgs( hybridData, composition, outPath, width, height, frameRate, bitRate, encoderName.has_value() ? encoderName.value() : nullptr); @@ -96,11 +98,13 @@ global_ref VideoCompositionExporter::create( VideoCompositionExporter::VideoCompositionExporter( int width, int height, std::shared_ptr workletRuntime, - std::shared_ptr drawFrame) { + std::shared_ptr drawFrame, + std::shared_ptr jsiSurface) { this->width = width; this->height = height; this->drawFrameWorklet = drawFrame; this->workletRuntime = workletRuntime; + this->jsiSurface = jsiSurface; } void VideoCompositionExporter::registerNatives() { @@ -124,30 +128,7 @@ void VideoCompositionExporter::start( void VideoCompositionExporter::makeSkiaSharedContextCurrent() { if (surface == nullptr) { surface = SkiaOpenGLSurfaceFactory::makeOffscreenSurface(width, height); - jsiCanvas = std::make_shared( - JNIHelpers::getSkiaPlatformContext(), surface->getCanvas()); - - jsiCanvasProxy = - std::make_shared(workletRuntime->getJSIRuntime()); - auto& jsiCanvasMap = jsiCanvas->getExportedFunctionMap(); - for (auto& entry : jsiCanvasMap) { - auto propName = entry.first; - auto func = std::bind(entry.second, jsiCanvas, std::placeholders::_1, - std::placeholders::_2, std::placeholders::_3, - std::placeholders::_4); - jsiCanvasProxy->setProperty( - workletRuntime->getJSIRuntime(), propName.c_str(), - jsi::Function::createFromHostFunction( - workletRuntime->getJSIRuntime(), - jsi::PropNameID::forUtf8(workletRuntime->getJSIRuntime(), - propName), - 0, - [=](jsi::Runtime& runtime, const jsi::Value& thisValue, - const jsi::Value* arguments, size_t count) -> jsi::Value { - return func(workletRuntime->getJSIRuntime(), thisValue, - arguments, count); - })); - } + jsiSurface->setObject(surface); } SkiaOpenGLHelper::makeCurrent( &ThreadContextHolder::ThreadSkiaOpenGLContext, @@ -160,19 +141,18 @@ int VideoCompositionExporter::renderFrame( &ThreadContextHolder::ThreadSkiaOpenGLContext, ThreadContextHolder::ThreadSkiaOpenGLContext.gl1x1Surface); auto currentTime = jsi::Value(time); - auto result = jsi::Object(workletRuntime->getJSIRuntime()); + auto& runtime = workletRuntime->getJSIRuntime(); + auto result = jsi::Object(runtime); auto platformContext = JNIHelpers::getSkiaPlatformContext(); for (auto& entry : *frames) { auto id = entry.first->toStdString(); auto frame = entry.second; - auto jsFrame = frame->toJS(workletRuntime->getJSIRuntime()); - result.setProperty(workletRuntime->getJSIRuntime(), id.c_str(), - std::move(jsFrame)); + auto jsFrame = frame->toJS(runtime); + result.setProperty(runtime, id.c_str(), std::move(jsFrame)); } surface->getCanvas()->clear(SkColors::kTransparent); - auto canvasProxy = jsiCanvasProxy.get(); - workletRuntime->runGuarded(drawFrameWorklet, *canvasProxy, currentTime, result); + workletRuntime->runGuarded(drawFrameWorklet, currentTime, result); GrAsDirectContext(surface->recordingContext())->flushAndSubmit(); GrBackendTexture texture = SkSurfaces::GetBackendTexture( @@ -189,8 +169,7 @@ int VideoCompositionExporter::renderFrame( void VideoCompositionExporter::release() { jThis = nullptr; surface = nullptr; - jsiCanvas = nullptr; - jsiCanvasProxy = nullptr; + jsiSurface = nullptr; } void VideoCompositionExporter::onComplete() { diff --git a/android/cpp/VideoCompositionExporter.h b/android/cpp/VideoCompositionExporter.h index 0b317cb..1d836fa 100644 --- a/android/cpp/VideoCompositionExporter.h +++ b/android/cpp/VideoCompositionExporter.h @@ -1,10 +1,10 @@ #pragma once #include -#include -#include #include #include +#include +#include #include "VideoComposition.h" #include "VideoFrame.h" @@ -22,12 +22,14 @@ class VideoCompositionExporter : public HybridClass { int width, int height, int frameRate, int bitRate, std::optional encoderName, std::shared_ptr workletRuntime, - std::shared_ptr drawFrame); + std::shared_ptr drawFrame, + std::shared_ptr jsiSurface); explicit VideoCompositionExporter( int width, int height, std::shared_ptr workletRuntime, - std::shared_ptr drawFrame); + std::shared_ptr drawFrame, + std::shared_ptr jsiSurface); static void registerNatives(); @@ -35,7 +37,8 @@ class VideoCompositionExporter : public HybridClass { jsi::Runtime& runtime, jsi::Object composition, jsi::Object options, std::shared_ptr workletRuntime, std::shared_ptr drawFrame, - jsi::Function onSuccess, jsi::Function onError); + jsi::Function onSuccess, jsi::Function onError, + std::shared_ptr jsiSurface); private: friend HybridBase; @@ -47,8 +50,7 @@ class VideoCompositionExporter : public HybridClass { std::function onCompleteCallback; std::function e)> onErrorCallback; sk_sp surface; - std::shared_ptr jsiCanvas; - std::shared_ptr jsiCanvasProxy; + std::shared_ptr jsiSurface; void start(std::function onCompleteCallback, std::function e)> onErrorCallback); diff --git a/android/cpp/VideoFrame.cpp b/android/cpp/VideoFrame.cpp index fabf38c..834e322 100644 --- a/android/cpp/VideoFrame.cpp +++ b/android/cpp/VideoFrame.cpp @@ -45,8 +45,8 @@ jsi::Value VideoFrame::toJS(jsi::Runtime& runtime) { jsObject.setProperty(runtime, "rotation", rotation); jsObject.setProperty( runtime, "buffer", - std::move(jsi::BigInt::fromUint64( - runtime, reinterpret_cast(hardwareBuffer)))); + jsi::BigInt::fromUint64(runtime, + reinterpret_cast(hardwareBuffer))); return jsObject; } diff --git a/android/cpp/cpp-adapter.cpp b/android/cpp/cpp-adapter.cpp index 84170d5..ebb4b3e 100644 --- a/android/cpp/cpp-adapter.cpp +++ b/android/cpp/cpp-adapter.cpp @@ -7,6 +7,7 @@ #include #include #include +#include using namespace facebook; using namespace RNSkiaVideo; @@ -69,11 +70,11 @@ void install(jsi::Runtime& jsiRuntime) { jsi::PropNameID::forAscii(jsiRuntime, "exportVideoComposition"), 6, [](jsi::Runtime& runtime, const jsi::Value& thisValue, const jsi::Value* arguments, size_t count) -> jsi::Value { - if (count != 6) { + if (count != 7) { throw jsi::JSError(runtime, - "SkiaVideo.exportVideoComposition(..) expects 4" + "SkiaVideo.exportVideoComposition(..) expects 6" "arguments (composition, options, workletRuntime, " - "drawFrame, onSuccess, onError)!"); + "drawFrame, onSuccess, onError, SkSurface)!"); } return VideoCompositionExporter::exportVideoComposition( runtime, arguments[0].asObject(runtime), @@ -82,7 +83,9 @@ void install(jsi::Runtime& jsiRuntime) { reanimated::extractShareableOrThrow( runtime, arguments[3]), arguments[4].asObject(runtime).asFunction(runtime), - arguments[5].asObject(runtime).asFunction(runtime)); + arguments[5].asObject(runtime).asFunction(runtime), + std::static_pointer_cast( + arguments[6].asObject(runtime).asHostObject(runtime))); }); RNSVModule.setProperty(jsiRuntime, "exportVideoComposition", std::move(exportVideoComposition)); diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 305ecd7..1c5047b 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - azzapp-react-native-skia-video (0.4.0): + - azzapp-react-native-skia-video (0.4.2): - DoubleConversion - glog - hermes-engine @@ -1477,7 +1477,7 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: - azzapp-react-native-skia-video: ce9e739e4eff89ee9bf8277a8a74f08fce2b2caf + azzapp-react-native-skia-video: 8efa2f751065f509016b3f394265f46920f7edb1 boost: d3f49c53809116a5d38da093a8aa78bf551aed09 DoubleConversion: 76ab83afb40bddeeee456813d9c04f67f78771b5 FBLazyVector: 026c8f4ae67b06e088ae01baa2271ef8a26c0e8c diff --git a/ios/ReactNativeSkiaVideo.mm b/ios/ReactNativeSkiaVideo.mm index d9213fb..f9dc9ea 100644 --- a/ios/ReactNativeSkiaVideo.mm +++ b/ios/ReactNativeSkiaVideo.mm @@ -97,7 +97,7 @@ @implementation ReactNativeSkiaVideo runtime, jsi::PropNameID::forAscii(runtime, "exportVideoComposition"), 6, [](jsi::Runtime& runtime, const jsi::Value& thisValue, const jsi::Value* arguments, size_t count) -> jsi::Value { - if (count != 6) { + if (count < 6) { throw jsi::JSError(runtime, "SkiaVideo.exportVideoComposition(..) expects 4" "arguments (composition, options, workletRuntime, " diff --git a/src/exportVideoComposition.ts b/src/exportVideoComposition.ts index 24a987e..a661bd2 100644 --- a/src/exportVideoComposition.ts +++ b/src/exportVideoComposition.ts @@ -3,7 +3,9 @@ import { makeShareableCloneRecursive, type WorkletRuntime, } from 'react-native-reanimated'; -import type { SkCanvas } from '@shopify/react-native-skia'; +import { Platform } from 'react-native'; +import { Skia } from '@shopify/react-native-skia'; +import type { SkCanvas, SkSurface } from '@shopify/react-native-skia'; import type { ExportOptions, FrameDrawer, @@ -32,21 +34,41 @@ export const exportVideoComposition = async ( options: ExportOptions, drawFrame: FrameDrawer ): Promise => { - const drawFrameInner = ( - canvas: SkCanvas, - time: number, - frames: Record - ) => { - 'worklet'; - drawFrame({ - canvas, - videoComposition, - currentTime: time, - frames, - width: options.width, - height: options.height, - }); - }; + let surface: SkSurface | null = null; + let drawFrameInner: (...args: any[]) => void; + if (Platform.OS === 'android') { + surface = Skia.Surface.Make(1, 1); + if (!surface) { + throw new Error('Failed to create Skia surface'); + } + drawFrameInner = (time: number, frames: Record) => { + 'worklet'; + drawFrame({ + canvas: surface!.getCanvas(), + videoComposition, + currentTime: time, + frames, + width: options.width, + height: options.height, + }); + }; + } else { + drawFrameInner = ( + canvas: SkCanvas, + time: number, + frames: Record + ) => { + 'worklet'; + drawFrame({ + canvas, + videoComposition, + currentTime: time, + frames, + width: options.width, + height: options.height, + }); + }; + } return new Promise((resolve, reject) => RNSkiaVideoModule.exportVideoComposition( @@ -55,7 +77,8 @@ export const exportVideoComposition = async ( getExportRuntime(), makeShareableCloneRecursive(drawFrameInner), () => resolve(), - (e: any) => reject(e ?? new Error('Failed to export video')) + (e: any) => reject(e ?? new Error('Failed to export video')), + surface ) ); }; diff --git a/src/types.ts b/src/types.ts index 741e860..0540199 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,6 @@ // @ts-ignore // eslint-disable-next-line @typescript-eslint/no-unused-vars -import type { SkCanvas, Skia } from '@shopify/react-native-skia'; +import type { SkCanvas, SkSurface, Skia } from '@shopify/react-native-skia'; import type { WorkletRuntime } from 'react-native-reanimated'; /** @@ -333,7 +333,9 @@ export type RNSkiaVideoModule = { workletRuntime: WorkletRuntime, drawFrame: any, // ShareableRef onCompletion: () => void, - onError: (error: any) => void + onError: (error: any) => void, + // android only + surface: SkSurface | null ) => Promise; /**