diff --git a/CHANGELOG.md b/CHANGELOG.md index d2709ab..2bbb40b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,141 @@ +## [3.4.0] + +### Changed + +* [react-native-photoeditorsdk] Raised minimum PhotoEditor Build SDK for Android version to 35 +* [react-native-photoeditorsdk] Raised Kotlin version to 2.1 and KSP to 2.1.0-1.0.28 +* [react-native-videoeditorsdk] Raised minimum PhotoEditor Build SDK for Android version to 35 +* [react-native-videoeditorsdk] Raised Kotlin version to 2.1 and KSP to 2.1.0-1.0.28 + + +## [3.3.0] + +### Changed + +* [react-native-imglysdk] Removed `@expo/config-plugins` dependency. + +### Fixed + +* Fixed potential crash from `unlockWithLicense` if new architecture is enabled. + +## [3.2.0] + +### Changed + +* [react-native-imglysdk] Updated `@expo/config-plugins` dependency to `7.2`. +* [react-native-photoeditorsdk] Raised minimum PhotoEditor SDK for iOS version to 11.9.0. +* [react-native-photoeditorsdk] Raised minimum PhotoEditor SDK for Android version to 10.9.0. See the [migration guide](https://img.ly/docs/pesdk/react-native/getting-started/migration-guides/3-2-0/) for more information. +* [react-native-videoeditorsdk] Raised minimum VideoEditor SDK for iOS version to 11.9.0. +* [react-native-videoeditorsdk] Raised minimum VideoEditor SDK for Android version to 10.9.0. See the [migration guide](https://img.ly/docs/vesdk/react-native/getting-started/migration-guides/3-2-0/) for more information. + +### Added + +* [react-native-imglysdk] Added `kspVersion` parameter. +* [react-native-videoeditorsdk] Added `VideoEditorResult` to types export. +* [react-native-photoeditorsdk] Added `PhotoEditorResult` to types export. + +### Fixed + +* Fixed compiling issues when using native customizations on iOS. +* Fixed potential crash on Android: `IllegalStateException "You need to use a Theme.AppCompat theme (or descendant) with this activity."`. + +## [3.1.0] + +### Added + +* Added support for Expo 49. (#1811) + +## [3.0.0] + +### Changed + +* Changed and aligned the error codes for the modules. +* [react-native-videoeditorsdk] Unlocking the SDK via `VESDK.unlockWithLicense` now returns a `Promise`. +* [react-native-photoeditorsdk] Unlocking the SDK via `PESDK.unlockWithLicense` now returns a `Promise`. + +### Fixed + +* [react-native-videoeditorsdk] Fixed unused types exports. + +## [2.17.1] + +### Fixed + +* [react-native-imglysdk] Fixed wrong default SDK version for Android. + +## [2.17.0] + +### Added + +* Added `configuration.singleToolMode` that skips main menu if only one tool is used. +* [react-native-videoeditorsdk] Added `RNVideoEditorSDKModule.editorWillOpenClosure` and `RNVideoEditorSDKModule.editorWillExportClosure` which allow further native configuration on Android. +* [react-native-photoeditorsdk] Added `RNPhotoEditorSDKModule.editorWillOpenClosure` and `RNPhotoEditorSDKModule.editorWillExportClosure` which allow further native configuration on Android. + +### Fixed + +* [react-native-videoeditorsdk] Fixed `VideoEditorResult.videoSize` would always be zero. +* [react-native-videoeditorsdk] Fixed error when not setting `Configuration.export.video.segments`. + +## [2.16.1] + +### Fixed + +* [react-native-videoeditorsdk] Fixed error when cancelling the editor. +* [react-native-videoeditorsdk] Fixed missing export of `VideoSegment` type. +* [react-native-videoeditorsdk] Fixed wrong types for `VESDK.openEditor`. + +## [2.16.0] + +### Added + +* [react-native-videoeditorsdk] Added duration action for text and stickers. +* [react-native-videoeditorsdk] Added `VideoEditorResult.segments`, `VideoEditorResult.videoSize`, and `VideoEditorResult.release()` which enable serialization of the individual video composition components if `configuration.export.video.segments` is enabled. + +## [2.15.0] + +### Changed + +* 🚨 Bumped iOS deployment target to 13.0. +* [react-native-videoeditorsdk] Raised minimum VideoEditor SDK for iOS version to 11.1.0. See the [changelog](https://github.com/imgly/vesdk-ios-build/blob/master/CHANGELOG.md) for more information. +* [react-native-photoeditorsdk] Raised minimum PhotoEditor SDK for iOS version to 11.1.0. See the [changelog](https://github.com/imgly/pesdk-ios-build/blob/master/CHANGELOG.md) for more information. + +### Added + +* Added implementation and documentation for background removal. + +## [2.14.0] + +### Added + +* [react-native-videoeditorsdk] Added implementation and documentation for GIPHY sticker integration. + +### Fixed + +* [react-native-videoeditorsdk] Fixed `VESDK.openEditor` return type declaration and API documentation to return `Promise` instead of just `Promise`. +* [react-native-videoeditorsdk] Fixed height and width of specified composition size would be flipped on Android. +* [react-native-photoeditorsdk] Fixed `PESDK.openEditor` return type declaration and API documentation to return `Promise` instead of just `Promise`. +* [react-native-photoeditorsdk] Fixed deprecation warning for `RCTBridge.imageLoader` on iOS. + + +## [2.13.1] + +### Fixed + +* Fixed enabling serialization would crash the application on Android when exporting. + +## [2.13.0] + +### Changed + +* 🚨 With this version you might need to create symlinks when using Android Gradle Plugin version `4.x`. Please refer to the new [known issues](https://github.com/imgly/vesdk-react-native#known-issues) section of the README for details. +* 🚨 This version requires `minSdkVersion` `21` for Android. Please refer to the new step 3 in the [getting started](https://github.com/imgly/vesdk-react-native#android) section of the README for instructions on how to adjust it. +* [react-native-videoeditorsdk] Raised minimum VideoEditor SDK for Android version to 10.0.1. See the [changelog](https://github.com/imgly/vesdk-android-demo/blob/master/CHANGELOG.md) for more information. +* [react-native-photoeditorsdk] Raised minimum PhotoEditor SDK for Android version to 10.0.1. See the [changelog](https://github.com/imgly/pesdk-android-demo/blob/master/CHANGELOG.md) for more information. + +### Added + +* [react-native-imglysdk] Added support to specify a custom `buildToolsVersion`, `minSdkVersion`, `compileSdkVersion`, `targetSdkVersion`, and `kotlinGradlePluginVersion` for Android with the Expo config plugin. + ## [2.12.0] ### Changed @@ -20,7 +158,7 @@ ### Changed -* 🚨 The img.ly maven repository is no longer automatically added to your project by the plugin for Android. Please refer to the new step 3 in the [getting started](https://github.com/imgly/vesdk-react-native#android) section of the README for instructions on how to add it. +* 🚨 The IMG.LY maven repository is no longer automatically added to your project by the plugin for Android. Please refer to the new step 3 in the [getting started](https://github.com/imgly/vesdk-react-native#android) section of the README for instructions on how to add it. * [react-native-videoeditorsdk] Added support for VideoEditor SDK for Android version 9. * [react-native-photoeditorsdk] Added support for PhotoEditor SDK for Android version 9. diff --git a/LICENSE.md b/LICENSE.md index d86fc14..8e7cc2c 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -4,7 +4,7 @@ In order to run any samples or use any wrapper without a watermark, you'll have to purchase a commercial PhotoEditor SDK or VideoEditor SDK license. Visit https://img.ly for more details. -Copyright (c) 2014-2022, img.ly GmbH +Copyright (c) 2014-2024, IMG.LY GmbH All rights reserved. Redistribution and use in source and binary forms, with or without @@ -17,7 +17,7 @@ modification, are permitted provided that the following conditions are met: notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -3. Neither the name img.ly GmbH, img.ly, PhotoEditor SDK, VideoEditor SDK +3. Neither the name IMG.LY GmbH, IMG.LY, PhotoEditor SDK, VideoEditor SDK nor the names of its developers may be used to endorse or promote products derived from this software without specific prior written permission. diff --git a/README.md b/README.md index 7ddf294..35527f5 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,12 @@ Check out our [video tutorial](https://img.ly/blog/a-photo-and-video-editor-for-your-react-native-apps/) for a step-by-step integration guide which also details advanced SDK features, such as serializing and reusing previously applied editing operations. +## System requirements + +- React Native: 0.60 +- iOS: 13 +- Android: 5 (SDK 21) + ## Getting started ### Expo CLI @@ -36,7 +42,7 @@ In order to use this module with the Expo CLI you can make use of our integrated ```sh expo install react-native-videoeditorsdk ``` - + This will automatically install [`react-native-imglysdk`](https://npmjs.org/package/react-native-imglysdk) which you can use to configure your application with our Expo config plugin. 2. Inside your app's `app.json` or `app.config.js` add our config plugin: @@ -47,7 +53,9 @@ In order to use this module with the Expo CLI you can make use of our integrated } ``` - If needed, you can also use a specific version of our native library for Android as well as define explicitly the included modules. By default, all modules for both PhotoEditor SDK and VideoEditor SDK are included. + If needed, you can also use a specific version of our native library for Android as well as define explicitly the included modules. By default, all modules for both PhotoEditor SDK and VideoEditor SDK are included. Further, you can alternate the KSP version with the `kspVersion` parameter based on the Kotlin version you are using. Please take a look [here](#android) on further details. + + For Expo version < 45, you can configure the `buildToolsVersion`, `minSdkVersion`, `compileSdkVersion`, `targetSdkVersion`, and `kotlinGradlePluginVersion`. From version 45+ we recommend setting these properties using the `expo-build-properties` config plugin directly. ```json { @@ -56,13 +64,19 @@ In order to use this module with the Expo CLI you can make use of our integrated "react-native-imglysdk", { "android": { - "version": "9.2.0", + "version": "10.9.0", + "kspVersion": "1.8.0-1.0.9", "modules": [ "ui:core", "ui:transform", "ui:filter", "assets:filter-basic" - ] + ], + "buildToolsVersion": "35.0.0", + "minSdkVersion": "21", + "compileSdkVersion": "35", + "targetSdkVersion": "35", + "kotlinGradlePluginVersion": "2.1.0" } } ] @@ -71,10 +85,12 @@ In order to use this module with the Expo CLI you can make use of our integrated ``` For further information on the available modules, please refer to step 4 of the React Native CLI [Android](#android) guide below. - + **Please note that the `react-native-imglysdk` module manages both VideoEditor SDK as well as PhotoEditor SDK so you only need to add the Expo config plugin once even when using both SDKs.** -3. The changes will be applied on `expo prebuild` or during the prebuild phase of `eas build`. +3. From version `2.15.0` the iOS deployment target needs to be set to at least iOS 13. You can use the `expo-build-properties` config plugin for this. Please refer to the [official Expo docs](https://docs.expo.dev/versions/v45.0.0/sdk/build-properties/). + +4. The changes will be applied on `expo prebuild` or during the prebuild phase of `eas build`. For further information on how to integrate Expo config plugins please also refer to the official [docs](https://docs.expo.dev/guides/config-plugins/#using-a-plugin-in-your-app). @@ -114,25 +130,8 @@ For older React Native versions autolinking is not available and VideoEditor SDK #### Android -1. Because VideoEditor SDK for Android is quite large, there is a high chance that you will need to enable [Multidex](https://developer.android.com/studio/build/multidex) for your project as follows: - - 1. Open the `android/app/build.gradle` file (**not** `android/build.gradle`) and add these lines at the end: - ```groovy - android { - defaultConfig { - multiDexEnabled true - } - } - dependencies { - implementation 'androidx.multidex:multidex:2.0.1' - } - ``` - 2. Open the `android/app/src/main/java/.../MainApplication.java` file and change the superclass of your `MainApplication` class from `Application` to `androidx.multidex.MultiDexApplication`, e.g.: - ```java - public class MainApplication extends androidx.multidex.MultiDexApplication implements ReactApplication { - ``` - -2. Add the img.ly repository and plugin by opening the `android/build.gradle` file (**not** `android/app/build.gradle`) and adding these lines at the top: +1. Add the IMG.LY repository and plugin by opening the `android/build.gradle` file (**not** `android/app/build.gradle`) and adding these lines at the top: + ```groovy buildscript { repositories { @@ -140,14 +139,18 @@ For older React Native versions autolinking is not available and VideoEditor SDK maven { url "https://artifactory.img.ly/artifactory/imgly" } } dependencies { - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.10" - classpath 'ly.img.android.sdk:plugin:9.2.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.0" + classpath 'com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:2.1.0-1.0.28' // KSP version is depending on your Kotlin version. + classpath 'ly.img.android.sdk:plugin:10.9.0' } } ``` - In order to update VideoEditor SDK for Android replace the version string `9.2.0` with a [newer release](https://github.com/imgly/vesdk-android-demo/releases). -3. Still in the `android/build.gradle` file (**not** `android/app/build.gradle`), add these lines at the bottom: + The KSP version depends on the Kotlin version that you are using. In order to find the correct version, please visit the [official KSP release page](https://github.com/google/ksp/releases?page=1). + + In order to update VideoEditor SDK for Android replace the version string `10.9.0` with a [newer release](https://github.com/imgly/vesdk-android-demo/releases). + +2. Still in the `android/build.gradle` file (**not** `android/app/build.gradle`), add these lines at the bottom: ```groovy allprojects { @@ -157,13 +160,30 @@ For older React Native versions autolinking is not available and VideoEditor SDK } ``` -4. Configure VideoEditor SDK for Android by opening the `android/app/build.gradle` file (**not** `android/build.gradle`) and adding the following lines under `apply plugin: "com.android.application"`: +3. In the same file, you will need to modify the `minSdkVersion` to at least `21`. We also recommend to update the `buildToolsVersion` to `35.0.0` or higher as well as the `compileSdkVersion` to `35` or higher but this is not mandatory: + + ```diff + buildscript { + ext { + - buildToolsVersion = "30.0.2" + + buildToolsVersion = "35.0.0" + - minSdkVersion = 19 + + minSdkVersion = 21 + - compileSdkVersion = 30 + + compileSdkVersion = 35 + targetSdkVersion = 35 + } + } + ``` + +4. Configure VideoEditor SDK for Android by opening the `android/app/build.gradle` file (**not** `android/build.gradle`) and adding the following lines under `apply plugin: "com.android.application"`: + ```groovy apply plugin: 'ly.img.android.sdk' apply plugin: 'kotlin-android' // Comment out the modules you don't need, to save size. - imglyConfig { + IMGLY.configure { modules { include 'ui:text' include 'ui:focus' @@ -179,6 +199,7 @@ For older React Native versions autolinking is not available and VideoEditor SDK include 'ui:video-library' include 'ui:video-composition' include 'ui:audio-composition' + include 'ui:giphy-sticker' // This module is big, remove the serializer if you don't need that feature. include 'backend:serializer' @@ -194,6 +215,7 @@ For older React Native versions autolinking is not available and VideoEditor SDK include 'backend:sticker-animated' include 'backend:sticker-smart' + include 'backend:background-removal' } } ``` @@ -203,25 +225,29 @@ For older React Native versions autolinking is not available and VideoEditor SDK Import the module in your `App.js`: ```js -import {VESDK, VideoEditorModal, Configuration} from 'react-native-videoeditorsdk'; +import { + VESDK, + VideoEditorModal, + Configuration, +} from "react-native-videoeditorsdk"; ``` Each platform requires a separate license file. [Unlock VideoEditor SDK](./index.d.ts#L41-L53) automatically for both platforms with a single line of code via [platform-specific file extensions](https://reactnative.dev/docs/platform-specific-code#platform-specific-extensions): ```js -VESDK.unlockWithLicense(require('./vesdk_license')); +VESDK.unlockWithLicense(require("./vesdk_license")); ``` Open the editor with a video: ```js -VESDK.openEditor(require('./video.mp4')); +VESDK.openEditor(require("./video.mp4")); ``` Or use the component to open the editor: ```jsx - + ``` Please see the [code documentation](./index.d.ts) for more details and additional [customization and configuration options](./configuration.ts). diff --git a/RNVideoEditorSDK.podspec b/RNVideoEditorSDK.podspec index b27608c..e6f1a3c 100644 --- a/RNVideoEditorSDK.podspec +++ b/RNVideoEditorSDK.podspec @@ -10,7 +10,7 @@ Pod::Spec.new do |s| s.homepage = package['homepage'] s.license = { :type => package['license'], :file => package['licenseFilename'] } s.author = { package['author']['name'] => package['author']['email'] } - s.platform = :ios, '9.0' + s.platform = :ios, '13.0' s.source = { :git => package['repository']['url'], :tag => "#{s.version}" } s.source_files = 'ios/**/*.{h,m,swift}' s.public_header_files = ['ios/RNVideoEditorSDK.h', 'ios/RNImglyKit.h'] @@ -18,5 +18,5 @@ Pod::Spec.new do |s| s.dependency 'React' s.dependency 'React-RCTImage' - s.dependency 'VideoEditorSDK', '~> 10.29' + s.dependency 'VideoEditorSDK', '~> 11.9' end diff --git a/android/build.gradle b/android/build.gradle index 3db2ac5..5987786 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -7,7 +7,8 @@ apply plugin: 'com.android.library' // Apply the ImgLy-Plugin apply plugin: 'ly.img.android.sdk' apply plugin: 'kotlin-android' -imglyConfig { + +IMGLY.configure { vesdk { enabled true } @@ -20,7 +21,7 @@ imglyConfig { } } -def MIN_LY_IMG_ANDROID_SDK_PLUGIN_VERSION = "9.2.0" +def MIN_LY_IMG_ANDROID_SDK_PLUGIN_VERSION = "10.9.0" task checkVersion { if (imglyConfig.convertToVersionNumber(imglyConfig.getVersion()) < imglyConfig.convertToVersionNumber(MIN_LY_IMG_ANDROID_SDK_PLUGIN_VERSION)) { @@ -39,12 +40,12 @@ task checkVersion { preBuild.dependsOn checkVersion android { - compileSdkVersion safeExtGet('compileSdkVersion', 30) - buildToolsVersion safeExtGet('buildToolsVersion', '30.0.3') + compileSdkVersion safeExtGet('compileSdkVersion', 35) + buildToolsVersion safeExtGet('buildToolsVersion', '35.0.0') defaultConfig { - minSdkVersion safeExtGet('minSdkVersion', 19) - targetSdkVersion safeExtGet('targetSdkVersion', 30) + minSdkVersion safeExtGet('minSdkVersion', 21) + targetSdkVersion safeExtGet('targetSdkVersion', 35) versionCode 1 versionName "1.0" } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 361d45c..6105f47 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 333a25c..d43ec93 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -8,6 +8,9 @@ android:exported="false" android:enabled="false" android:name="ly.img.android.IMGLYAutoInit" /> + \ No newline at end of file diff --git a/android/src/main/java/ly/img/react_native/vesdk/RNVideoEditorSDKModule.kt b/android/src/main/java/ly/img/react_native/vesdk/RNVideoEditorSDKModule.kt index 9085751..d8ab7ee 100644 --- a/android/src/main/java/ly/img/react_native/vesdk/RNVideoEditorSDKModule.kt +++ b/android/src/main/java/ly/img/react_native/vesdk/RNVideoEditorSDKModule.kt @@ -2,14 +2,15 @@ package ly.img.react_native.vesdk import android.app.Activity import android.content.Intent -import android.content.pm.PackageManager import android.net.Uri -import android.os.Build import android.util.Log +import androidx.annotation.WorkerThread import com.facebook.react.bridge.* +import ly.img.android.AuthorizationException import ly.img.android.IMGLY import ly.img.android.VESDK import ly.img.android.pesdk.VideoEditorSettingsList +import ly.img.android.pesdk.backend.decoder.VideoSource import ly.img.android.pesdk.backend.model.state.LoadSettings import ly.img.android.pesdk.backend.model.state.manager.SettingsList import ly.img.android.pesdk.kotlin_extension.continueWithExceptions @@ -24,7 +25,10 @@ import org.json.JSONObject import java.io.File import ly.img.android.pesdk.backend.encoder.Encoder import ly.img.android.pesdk.backend.model.EditorSDKResult +import ly.img.android.pesdk.backend.model.VideoPart import ly.img.android.pesdk.backend.model.state.VideoCompositionSettings +import ly.img.android.pesdk.backend.model.state.manager.StateHandler +import ly.img.android.pesdk.ui.activity.VideoEditorActivity import ly.img.android.serializer._3.IMGLYFileReader import ly.img.android.serializer._3.IMGLYFileWriter import java.util.UUID @@ -32,28 +36,57 @@ import java.util.UUID class RNVideoEditorSDKModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext), ActivityEventListener { companion object { // This number must be unique. It is public to allow client code to change it if the same value is used elsewhere. - var EDITOR_RESULT_ID = 29065 + @JvmField var EDITOR_RESULT_ID = 29065 + + /** A closure to modify a *VideoEditorSettingsList* before the editor is opened. */ + @JvmField var editorWillOpenClosure: ((settingsList: VideoEditorSettingsList) -> Unit)? = null + + /** A closure allowing access to the *StateHandler* before the editor is exporting. */ + @JvmField var editorWillExportClosure: ((stateHandler: StateHandler) -> Unit)? = null } init { reactContext.addActivityEventListener(this) } - private var currentSettingsList: VideoEditorSettingsList? = null + /** IMGLY constants for the plugin use. */ + object IMGLYConstants { + const val K_ERROR_UNABLE_TO_UNLOCK = "E_UNABLE_TO_UNLOCK" + const val K_ERROR_UNABLE_TO_LOAD = "E_UNABLE_TO_LOAD" + } + private var currentPromise: Promise? = null private var currentConfig: Configuration? = null + private var resolveManually: Boolean = false + private var currentEditorUID: String = UUID.randomUUID().toString() + private var settingsLists: MutableMap = mutableMapOf() + + @ReactMethod + fun unlockWithLicense(license: String, promise: Promise) { + try { + VESDK.initSDKWithLicenseData(license) + IMGLY.authorize() + promise.resolve(null) + } catch (e: AuthorizationException) { + promise.reject(IMGLYConstants.K_ERROR_UNABLE_TO_UNLOCK, "Unlocking the SDK failed due to: ${e.message}.") + } + + } @ReactMethod - fun unlockWithLicense(license: String) { - VESDK.initSDKWithLicenseData(license) - IMGLY.authorize() + fun releaseTemporaryData(identifier: String) { + val settingsList = settingsLists[identifier] + if (settingsList != null) { + settingsList.release() + settingsLists.remove(identifier) + } } override fun onActivityResult(activity: Activity, requestCode: Int, resultCode: Int, intent: Intent?) { val data = try { - intent?.let { EditorSDKResult(it) } + intent?.let { EditorSDKResult(it) } } catch (e: EditorSDKResult.NotAnImglyResultException) { - null + null } ?: return // If data is null the result is not from us. when (requestCode) { @@ -68,48 +101,80 @@ class RNVideoEditorSDKModule(reactContext: ReactApplicationContext) : ReactConte val resultPath = data.resultUri val serializationConfig = currentConfig?.export?.serialization + + var serialization: Any? = null val settingsList = data.settingsList - val serialization: Any? = if (serializationConfig?.enabled == true) { + if (serializationConfig?.enabled == true) { skipIfNotExists { settingsList.let { settingsList -> if (serializationConfig.embedSourceImage == true) { - Log.i("ImgLySdk", "EmbedSourceImage is currently not supported by the Android SDK") + Log.i( + "ImgLySdk", + "EmbedSourceImage is currently not supported by the Android SDK" + ) } - when (serializationConfig.exportType) { + serialization = when (serializationConfig.exportType) { SerializationExportType.FILE_URL -> { val uri = serializationConfig.filename?.let { Uri.parse("$it.json") - } ?: Uri.fromFile(File.createTempFile("serialization-" + UUID.randomUUID().toString(), ".json")) - Encoder.createOutputStream(uri).use { outputStream -> - IMGLYFileWriter(settingsList).writeJson(outputStream) - } + } ?: Uri.fromFile( + File.createTempFile( + "serialization-" + UUID.randomUUID() + .toString(), ".json" + ) + ) + Encoder.createOutputStream(uri) + .use { outputStream -> + IMGLYFileWriter(settingsList).writeJson( + outputStream + ) + } uri.toString() } SerializationExportType.OBJECT -> { ReactJSON.convertJsonToMap( - JSONObject( - IMGLYFileWriter(settingsList).writeJsonAsString() - ) + JSONObject( + IMGLYFileWriter(settingsList).writeJsonAsString() + ) ) } } } } ?: run { - Log.i("ImgLySdk", "You need to include 'backend:serializer' Module, to use serialisation!") - null + Log.i( + "ImgLySdk", + "You need to include 'backend:serializer' Module, to use serialisation!" + ) } - } else { - null + } + + var segments: ReadableArray? = null + val canvasSize = sourcePath?.let { VideoSource.create(it).fetchFormatInfo()?.size } + val serializedSize = reactMap( + "height" to canvasSize?.height, + "width" to canvasSize?.width + ) + + if (resolveManually) { + settingsLists[currentEditorUID] = settingsList + segments = serializeVideoSegments(settingsList) } currentPromise?.resolve( - reactMap( - "video" to resultPath?.toString(), - "hasChanges" to (sourcePath?.path != resultPath?.path), - "serialization" to serialization - ) + reactMap( + "video" to resultPath?.toString(), + "hasChanges" to (sourcePath?.path != resultPath?.path), + "serialization" to serialization, + "segments" to segments, + "identifier" to currentEditorUID, + "videoSize" to serializedSize + ) ) + if (!resolveManually) { + settingsList.release() + } + resolveManually = false }() } } @@ -117,81 +182,125 @@ class RNVideoEditorSDKModule(reactContext: ReactApplicationContext) : ReactConte } } - - override fun onNewIntent(intent: Intent?) { + override fun onNewIntent(intent: Intent) { } @ReactMethod fun present(video: String, config: ReadableMap?, serialization: String?, promise: Promise) { + val rawConfig: Map = config?.toHashMap() ?: emptyMap() + val cleanConfig: Map = rawConfig.filterValues { it != null }.mapValues { it.value as Any } + val configuration = ConfigLoader.readFrom(cleanConfig) + val serializationEnabled = configuration.export?.serialization?.enabled == true + val exportVideoSegments = configuration.export?.video?.segments == true + val createTemporaryFiles = serializationEnabled || exportVideoSegments + resolveManually = exportVideoSegments + + val settingsList = VideoEditorSettingsList(createTemporaryFiles) + configuration.applyOn(settingsList) + currentConfig = configuration + currentPromise = promise + + settingsList.configure { loadSettings -> + loadSettings.source = retrieveURI(video) + } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { - val settingsList = VideoEditorSettingsList() - - currentSettingsList = settingsList - currentConfig = ConfigLoader.readFrom(config?.toHashMap() ?: mapOf()).also { - it.applyOn(settingsList) - } - currentPromise = promise + readSerialisation(settingsList, serialization, false) + startEditor(settingsList) + } + @ReactMethod + fun presentComposition(videos: ReadableArray, config: ReadableMap?, serialization: String?, size: ReadableMap?, promise: Promise) { + val videoArray = deserializeVideoParts(videos) + var source = resolveSize(size) - settingsList.configure { loadSettings -> - loadSettings.source = retrieveURI(video) + val rawConfig: Map = config?.toHashMap() ?: emptyMap() + val cleanConfig: MutableMap = mutableMapOf() + for ((key, value) in rawConfig) { + if (value != null) cleanConfig[key] = value + } + val configuration = ConfigLoader.readFrom(cleanConfig) + val serializationEnabled = configuration.export?.serialization?.enabled == true + val exportVideoSegments = configuration.export?.video?.segments == true + val createTemporaryFiles = serializationEnabled || exportVideoSegments + resolveManually = exportVideoSegments + + val settingsList = VideoEditorSettingsList(createTemporaryFiles) + configuration.applyOn(settingsList) + currentConfig = configuration + currentPromise = promise + + if (videoArray.isNotEmpty()) { + if (source == null) { + if (size != null) { + promise.reject(IMGLYConstants.K_ERROR_UNABLE_TO_LOAD, "Invalid video size: width and height must be greater than zero.") + return + } + val video = videoArray.first() + source = video.videoSource.getSourceAsUri() } - readSerialisation(settingsList, serialization, false) - - startEditor(settingsList) + settingsList.configure { loadSettings -> + videoArray.forEach { + loadSettings.addCompositionPart(it) + } + } } else { - promise.reject("VESDK", "The video editor is only available in Android 4.3 and later.") + if (source == null) { + promise.reject(IMGLYConstants.K_ERROR_UNABLE_TO_LOAD, "The editor requires a valid size when initialized without a video.") + return + } + } + + settingsList.configure { + it.source = source } + + readSerialisation(settingsList, serialization, false) + startEditor(settingsList) } - @ReactMethod - fun presentComposition(videos: ReadableArray, config: ReadableMap?, serialization: String?, size: ReadableMap?, promise: Promise) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { - val array = videos.toArrayList() - val videoArray = array.filterIsInstance().takeIf { it.size == array.size } ?: arrayListOf() - val settingsList = VideoEditorSettingsList() - var source = resolveSize(size) - - currentSettingsList = settingsList - currentConfig = ConfigLoader.readFrom(config?.toHashMap() ?: mapOf()).also { - it.applyOn(settingsList) - } - currentPromise = promise + private fun serializeVideoSegments(settingsList: SettingsList): ReadableArray { + val compositionParts = WritableNativeArray() + settingsList[VideoCompositionSettings::class].videos.forEach { + val source = it.videoSource.getSourceAsUri().toString() + val trimStart = it.trimStartInNano / 1000000000.0f + val trimEnd = it.trimEndInNano / 1000000000.0f + + val videoPart = reactMap( + "videoURI" to source, + "startTime" to trimStart.toDouble(), + "endTime" to trimEnd.toDouble() + ) + compositionParts.pushMap(videoPart) + } + return compositionParts + } - if (videoArray.count() > 0) { - if (source == null) { - if (size != null) { - promise.reject("VESDK", "Invalid video size: width and height must be greater than zero.") - return + private fun deserializeVideoParts(videos: ReadableArray) : List { + val parts = emptyList().toMutableList() + + videos.toArrayList().forEach { + if (it is String) { + val videoPart = VideoPart(retrieveURI(it)) + parts.add(videoPart) + } else if (it is Map<*, *>) { + val uri = it["videoURI"] as String? + val trimStart = it["startTime"] as Double? + val trimEnd = it["endTime"] as Double? + + if (uri != null) { + val videoPart = VideoPart(retrieveURI(uri)) + if (trimStart != null) { + videoPart.trimStartInNanoseconds = (trimStart * 1000000000.0f).toLong() } - val video = videoArray.first() - source = retrieveURI(video) - } - - settingsList.configure { loadSettings -> - videoArray.forEach { - val resolvedSource = retrieveURI(it) - loadSettings.addCompositionPart(VideoCompositionSettings.VideoPart(resolvedSource)) + if (trimEnd != null) { + videoPart.trimEndInNanoseconds = (trimEnd * 1000000000.0f).toLong() } - } - } else { - if (source == null) { - promise.reject("VESDK", "The editor requires a valid size when initialized without a video.") - return + parts.add(videoPart) } } - - settingsList.configure { - it.source = source - } - - readSerialisation(settingsList, serialization, false) - startEditor(settingsList) - } else { - promise.reject("VESDK", "The video editor is only available in Android 4.3 and later.") } + return parts } private fun retrieveURI(source: String) : Uri { @@ -214,10 +323,12 @@ class RNVideoEditorSDKModule(reactContext: ReactApplicationContext) : ReactConte if (height == 0.0 || width == 0.0) { return null } - return LoadSettings.compositionSource(height.toInt(), width.toInt(), 60) + return LoadSettings.compositionSource(width.toInt(), height.toInt(), 60) } - private fun readSerialisation(settingsList: SettingsList, serialization: String?, readImage: Boolean) { + private fun readSerialisation(settingsList: VideoEditorSettingsList, serialization: String?, readImage: Boolean) { + editorWillOpenClosure?.invoke(settingsList) + if (serialization != null) { skipIfNotExists { IMGLYFileReader(settingsList).also { @@ -230,10 +341,13 @@ class RNVideoEditorSDKModule(reactContext: ReactApplicationContext) : ReactConte private fun startEditor(settingsList: VideoEditorSettingsList?) { val currentActivity = this.currentActivity ?: throw RuntimeException("Can't start the Editor because there is no current activity") if (settingsList != null) { + currentEditorUID = UUID.randomUUID().toString() + MainThreadRunnable { - VideoEditorBuilder(currentActivity) - .setSettingsList(settingsList) - .startActivityForResult(currentActivity, EDITOR_RESULT_ID) + VideoEditorBuilder(currentActivity, RNVideoEditorSDKActivity::class.java) + .setSettingsList(settingsList) + .startActivityForResult(currentActivity, EDITOR_RESULT_ID) + settingsList.release() }() } } @@ -336,4 +450,14 @@ class RNVideoEditorSDKModule(reactContext: ReactApplicationContext) : ReactConte } override fun getName() = "RNVideoEditorSDK" -} \ No newline at end of file +} + +/** A *VideoEditorActivity* used for the native interfaces. */ +class RNVideoEditorSDKActivity: VideoEditorActivity() { + @WorkerThread + override fun onExportStart(stateHandler: StateHandler) { + RNVideoEditorSDKModule.editorWillExportClosure?.invoke(stateHandler) + + super.onExportStart(stateHandler) + } +} diff --git a/configuration.ts b/configuration.ts index f49db10..dda7297 100644 --- a/configuration.ts +++ b/configuration.ts @@ -24,14 +24,25 @@ export interface Configuration { */ forceCrop?: boolean; + /** + * Controls if the editor is used in single tool mode. + * Prerequisite is that only one tool is in `tools`. + * + * @example // Defaults to: + * true + */ + singleToolMode?: boolean; + /** * Defines all allowed actions for the main screen that are displayed as overlay buttons on the canvas. * Only buttons for allowed actions are visible. + * @note The `CanvasAction.REMOVE_BACKGROUND` action is only shown when editing photos where a person could be detected. This feature is only supported on devices running iOS 15+. * @note The `CanvasAction.SOUND_ON_OFF` and `CanvasAction.PLAY_PAUSE` action is only shown when editing videos. * @example // Defaults to: * [CanvasAction.SOUND_ON_OFF, CanvasAction.PLAY_PAUSE, CanvasAction.UNDO, CanvasAction.REDO] */ mainCanvasActions?: Array< + CanvasAction.REMOVE_BACKGROUND | CanvasAction.SOUND_ON_OFF | CanvasAction.PLAY_PAUSE | CanvasAction.UNDO | @@ -576,7 +587,7 @@ export interface Configuration { * ]}, * ] */ - categories?: (StickerCategory | ExistingStickerCategory)[]; + categories?: (StickerCategory | ExistingStickerCategory | ExistingStickerProviderCategory)[]; /** * Defines all available colors that can be applied to stickers with a `tintMode` other than `TintMode.NONE`. * The color pipette is always added. @@ -603,8 +614,10 @@ export interface Configuration { colors?: ColorPalette; /** * Defines all allowed actions for the sticker tool menu. Only buttons for allowed actions are visible and shown in the given order. + * @note The `StickerAction.REMOVE_BACKGROUND` action is only shown for personal and external (non-animated) stickers where a person could be detected. This feature is only supported on devices running iOS 15+. + * @note The `StickerAction.DURATION` action is only shown when editing videos. * @example // Defaults to: - * [StickerAction.REPLACE, StickerAction.OPACITY, StickerAction.COLOR] + * [StickerAction.DURATION, StickerAction.REPLACE, StickerAction.OPACITY, StickerAction.COLOR, StickerAction.REMOVE_BACKGROUND] */ actions?: StickerAction[]; /** @@ -682,8 +695,9 @@ export interface Configuration { fonts?: (Font | ExistingItem)[]; /** * Defines all allowed actions for the text tool menu. Only buttons for allowed actions are visible and shown in the given order. + * @note The `TextAction.DURATION` action is only shown when editing videos. * @example // Defaults to: - * [TextAction.FONT, TextAction.COLOR, TextAction.BACKGROUND_COLOR, TextAction.ALIGNMENT] + * [TextAction.DURATION, TextAction.FONT, TextAction.COLOR, TextAction.BACKGROUND_COLOR, TextAction.ALIGNMENT] */ actions?: TextAction[]; /** @@ -1027,6 +1041,16 @@ export interface Configuration { * null */ bitRate?: number | null; + /** + * Whether the video editor should include the video segments of the composition + * in the `VideoEditorResult`. + * @note If enabled, you need to release the result via `VideoEditorResult.release()` + * after processing the video segments in order to prevent memory leaks. + * + * @example // Defaults to: + * false + */ + segments?: boolean; } /** * The filename for the exported data if the `exportType` is not `ImageExportType.DATA_URL`. @@ -1297,6 +1321,7 @@ export enum CanvasAction { INVERT = "invert", SOUND_ON_OFF = "soundonoff", PLAY_PAUSE = "playpause", + REMOVE_BACKGROUND = "removebackground", } /** A sticker action. */ @@ -1308,6 +1333,8 @@ export enum StickerAction { SATURATION = "saturation", REPLACE = "replace", OPACITY = "opacity", + REMOVE_BACKGROUND = "removebackground", + DURATION = "duration" } /** A text action. */ @@ -1316,6 +1343,7 @@ export enum TextAction { COLOR = "color", BACKGROUND_COLOR = "backgroundcolor", ALIGNMENT = "alignment", + DURATION = "duration" } /** A frame action. */ @@ -1571,6 +1599,38 @@ export interface StickerCategory extends NamedItem { items?: (Sticker | ExistingItem)[]; } +/** An existing sticker provider category. */ +export interface ExistingStickerProviderCategory extends ExistingItem { + /** + * The used sticker provider that must match the category's identifier. + */ + provider: GiphyStickerProvider; +} + +/** + * A GIPHY sticker provider. + * @note This sticker provider requires to use the identifier `imgly_sticker_category_giphy` for its `ExistingStickerProviderCategory`. + */ +export interface GiphyStickerProvider { + /** + * The key used to authorize API requests, obtained from GIPHY. + */ + apiKey: string; + /** + * The default language for regional content in 2-letter ISO 639-1 language code. + * If `null` the language setting of the current locale is used. + * @example // Defaults to: + * null + */ + language?: string; + /** + * The audience category used for content filtering. Available values are `"g"`, `"pg"`, `"pg-13"`, `"r"`. + * @example // Defaults to: + * "g" + */ + rating?: string; +} + /** A sticker. */ export interface Sticker extends NamedItem { /** diff --git a/index.d.ts b/index.d.ts index 6d4620a..134f226 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,5 +1,5 @@ -import { Component } from 'react'; -import { AssetURI, Configuration } from './configuration'; +import { Component } from "react"; +import { AssetURI, Configuration } from "./configuration"; /** * The result of an export. @@ -11,6 +11,12 @@ interface VideoEditorResult { hasChanges: boolean; /** All modifications applied to the input video if `export.serialization.enabled` of the `Configuration` was set to `true`. */ serialization?: string | object; + /** The used input video segments that compose the edited `video`. Returned if `export.video.segments` of the `Configuration` was set to `true`. */ + segments?: [VideoSegment]; + /** The size of the **untransformed** video. */ + videoSize: Size; + /** Releases the result. Needed if `export.video.segments` of the `Configuration` was set to `true`. */ + release?(): void; } /** An object that contains width and height values. */ @@ -21,18 +27,45 @@ interface Size { height: number; } +/** + * A video segment with optional trimming. + * + * It is used to initialize the editor with a video composition and to retrieve the edited video composition on export. + * A video composition (edited asset) in combination with a serialization (edit model) can be used to restore the state of the editor. + */ +interface VideoSegment { + /** + * A URI for the video segment. + * @note Remote resources are not optimized and therefore should be downloaded + * in advance and then passed to the editor as local resources. + */ + videoURI: AssetURI; + /** + * The start time in seconds. + * @example // Defaults to: + * null + */ + startTime?: number; + /** + * The end time in seconds. + * @example // Defaults to: + * null + */ + endTime?: number; +} + declare class VESDK { /** * Modally present a video editor. * @note Remote resources are not optimized and therefore should be downloaded * in advance and then passed to the editor as local resources. * - * @param {AssetURI | [AssetURI] | {uri: string}} video The source of the video to be edited. - * Can be either an URI (local only), an object with a member `uri`, or an asset reference + * @param {AssetURI | AssetURI[] | VideoSegment[] | {uri: string}} video The source of the video to be edited. + * Can be either a URI, an object with a member `uri`, or an asset reference * which can be optained by, e.g., `require('./video.mp4')` as `number`. * * For video compositions an array of video sources is accepted as input. If an empty array is - * passed to the editor `videoSize` must be set. You need to obtain a **valid license** for this + * passed to the editor `videoSize` must be set. You need to obtain a **valid license** for this * feature to work. * @param {Configuration} configuration The configuration used to initialize the editor. * @param {object} serialization The serialization used to initialize the editor. This @@ -41,16 +74,15 @@ declare class VESDK { * @param {Size} videoSize **Video composition only:** The size of the video in pixels that is about to be edited. * This overrides the natural dimensions of the video(s) passed to the editor. All videos will * be fitted to the `videoSize` aspect by adding black bars on the left and right side or top and bottom. - * - * @return {Promise} Returns a `VideoEditorResult` or `null` if the editor + * @return {Promise} Returns a `VideoEditorResult` or `null` if the editor * is dismissed without exporting the edited video. */ static openEditor( - video: AssetURI | [AssetURI] | {uri: string}, + video: AssetURI | AssetURI[] | VideoSegment[] | { uri: string }, configuration?: Configuration, serialization?: object, videoSize?: Size - ): Promise + ): Promise; /** * Unlock VideoEditor SDK with a license. @@ -62,9 +94,7 @@ declare class VESDK { * and `vesdk_license.android.json` for the Android license file in order to get automatically * resolved by the packager. */ - static unlockWithLicense( - license: string | object - ): void + static unlockWithLicense(license: string | object): Promise; } /** @@ -78,17 +108,15 @@ interface VideoEditorModalProps { /** * This prop determines the source of the video to be edited. - * Can be either an URI (local only), an object with a member `uri`, or an asset reference + * Can be either a URI, an object with a member `uri`, or an asset reference * which can be optained by, e.g., `require('./video.mp4')` as `number`. * For video compositions an array of video sources is accepted as input. If an empty array is * passed to the editor `videoSize` must be set. * - * @note Edited videos from remote resources can be previewed in the editor but their export will - * fail! Remote video resources are currently supported for debugging purposes only, e.g., when - * loading videos with `require('./video.mp4')` for debug builds static video assets will be - * resolved to remote URLs served by the development packager. + * @note Remote resources are not optimized and therefore should be downloaded + * in advance and then passed to the editor as local resources. */ - video: AssetURI | [AssetURI] | {uri: string}; + video: AssetURI | [AssetURI] | [VideoSegment] | { uri: string }; /** * This prop determines the configuration used to initialize the editor. @@ -139,7 +167,11 @@ interface VideoEditorModalState { /** * A component that wraps the `VESDK.openEditor` function to modally present a video editor. */ -declare class VideoEditorModal extends Component {} +declare class VideoEditorModal extends Component< + VideoEditorModalProps, + VideoEditorModalState +> {} + +export * from "./configuration"; +export { VESDK, VideoEditorModal, VideoEditorResult, VideoSegment }; -export { VESDK, VideoEditorModal }; -export * from './configuration'; diff --git a/index.js b/index.js index 58e1d8a..718096b 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,5 @@ import { Component } from 'react'; -import { NativeModules, Image, Platform } from 'react-native'; +import { Image, NativeModules, Platform } from 'react-native'; import { Configuration } from './configuration'; const { RNVideoEditorSDK } = NativeModules; @@ -123,8 +123,8 @@ class VESDK { * @note Remote resources are not optimized and therefore should be downloaded * in advance and then passed to the editor as local resources. * - * @param {AssetURI | [AssetURI] | {uri: string}} video The source of the video to be edited. - * Can be either an URI (local only), an object with a member `uri`, or an asset reference + * @param {AssetURI | AssetURI[] | VideoSegment[] | {uri: string}} video The source of the video to be edited. + * Can be either a URI, an object with a member `uri`, or an asset reference * which can be optained by, e.g., `require('./video.mp4')` as `number`. * * For video compositions an array of video sources is accepted as input. If an empty array is @@ -137,29 +137,59 @@ class VESDK { * @param {Size} videoSize **Video composition only:** The size of the video in pixels that is about to be edited. * This overrides the natural dimensions of the video(s) passed to the editor. All videos will * be fitted to the `videoSize` aspect by adding black bars on the left and right side or top and bottom. - * - * @return {Promise} Returns a `VideoEditorResult` or `null` if the editor + * @return {Promise} Returns a `VideoEditorResult` or `null` if the editor * is dismissed without exporting the edited video. */ - static openEditor(video, configuration = null, serialization = null, videoSize = null) { + static async openEditor(video, configuration = null, serialization = null, videoSize = null) { resolveStaticAssets(configuration) - const videoDimensions = videoSize == null ? (Platform.OS == 'android' ? null : {height: 0, width: 0}) : videoSize; - const resolvedSerialization = Platform.OS == 'android' ? (serialization != null ? JSON.stringify(serialization) : null) : serialization; - + const isAndroid = Platform.OS == 'android'; + const videoDimensions = videoSize == null ? (isAndroid ? null : {height: 0, width: 0}) : videoSize; + const resolvedSerialization = isAndroid ? (serialization != null ? JSON.stringify(serialization) : null) : serialization; + var result = null; + var source = null; + if (Array.isArray(video)) { - var source = []; + source = []; video.forEach((videoClip) => { - source.push(resolveStaticAsset(videoClip, Platform.OS == 'android')); + if (videoClip.videoURI != null) { + const resolvedSource = resolveStaticAsset(videoClip.videoURI, isAndroid); + const resolvedVideo = {...videoClip}; + resolvedVideo.videoURI = resolvedSource + source.push(resolvedVideo); + } else { + const resolvedSource = resolveStaticAsset(videoClip, isAndroid); + source.push(resolvedSource); + } }); - return RNVideoEditorSDK.presentComposition(source, configuration, resolvedSerialization, videoDimensions); + if (isAndroid) { + result = await RNVideoEditorSDK.presentComposition(source, configuration, resolvedSerialization, videoDimensions); + } else { + result = await RNVideoEditorSDK.presentVideoSegments(source, configuration, resolvedSerialization, videoDimensions); + } } else { if (videoSize != null) { console.warn("Ignoring the video size. This parameter can only be used in combination with video compositions. If your license includes the video composition feature please wrap your video source into an array instead.") } - const resolvedVideo = resolveStaticAsset(video, Platform.OS == 'android'); - return RNVideoEditorSDK.present(resolvedVideo, configuration, resolvedSerialization); + source = resolveStaticAsset(video, isAndroid); + result = await RNVideoEditorSDK.present(source, configuration, resolvedSerialization); } + if (result == null) return null; + const resolvedResult = {...result}; + if (configuration?.export?.video?.segments == true) { + const release = () => { + if (isAndroid) { + return RNVideoEditorSDK.releaseTemporaryData(result.identifier); + } + return; + } + resolvedResult.release = release; + } else { + delete resolvedResult.segments; + } + + delete resolvedResult.identifier; + return resolvedResult; } /** @@ -173,11 +203,7 @@ class VESDK { * resolved by the packager. */ static unlockWithLicense(license) { - if (Platform.OS == 'android') { - RNVideoEditorSDK.unlockWithLicense(JSON.stringify(license)); - } else { - RNVideoEditorSDK.unlockWithLicense(license); - } + return RNVideoEditorSDK.unlockWithLicense(JSON.stringify(license)); } } @@ -212,5 +238,6 @@ class VideoEditorModal extends Component { } } -export { VESDK, VideoEditorModal }; export * from './configuration'; +export { VESDK, VideoEditorModal }; + diff --git a/ios/RNImglyKit.h b/ios/RNImglyKit.h index d8f9cc7..68e8d6c 100644 --- a/ios/RNImglyKit.h +++ b/ios/RNImglyKit.h @@ -1,4 +1,4 @@ -@import Foundation; +#import @interface RNVESDKImglyKit : NSObject diff --git a/ios/RNImglyKit.m b/ios/RNImglyKit.m index 510f3f1..114d46e 100644 --- a/ios/RNImglyKit.m +++ b/ios/RNImglyKit.m @@ -63,11 +63,6 @@ - (void)present:(nonnull IMGLYMediaEditViewControllerBlock)createMediaEditViewCo } dispatch_async(dispatch_get_main_queue(), ^{ - if (self.licenseError != nil) { - reject(RN_IMGLY.kErrorUnableToUnlock, [NSString RN_IMGLY_string:@"Unable to unlock with license." withError:self.licenseError], self.licenseError); - return; - } - PESDKAssetCatalog *assetCatalog = PESDKAssetCatalog.defaultItems; PESDKConfiguration *configuration = [[PESDKConfiguration alloc] initWithBuilder:^(PESDKConfigurationBuilder * _Nonnull builder) { builder.assetCatalog = assetCatalog; @@ -86,6 +81,7 @@ - (void)present:(nonnull IMGLYMediaEditViewControllerBlock)createMediaEditViewCo id valueSerializationType = [NSDictionary RN_IMGLY_dictionary:dictionary valueForKeyPath:@"export.serialization.exportType" default:RN_IMGLY.kExportTypeFileURL]; id valueSerializationFile = [NSDictionary RN_IMGLY_dictionary:dictionary valueForKeyPath:@"export.serialization.filename" default:valueExportFile]; id valueSerializationEmbedImage = [NSDictionary RN_IMGLY_dictionary:dictionary valueForKeyPath:@"export.serialization.embedSourceImage" default:@(NO)]; + id valueExportVideoSegments = [NSDictionary RN_IMGLY_dictionary:dictionary valueForKeyPath:@"export.video.segments" default:@(NO)]; NSString *exportType = [RCTConvert NSString:valueExportType]; NSURL *exportFile = [RCTConvert RN_IMGLY_ExportFileURL:valueExportFile withExpectedUTI:getUTI(configuration)]; @@ -93,6 +89,7 @@ - (void)present:(nonnull IMGLYMediaEditViewControllerBlock)createMediaEditViewCo NSString *serializationType = [RCTConvert NSString:valueSerializationType]; NSURL *serializationFile = [RCTConvert RN_IMGLY_ExportFileURL:valueSerializationFile withExpectedUTI:kUTTypeJSON]; BOOL serializationEmbedImage = [RCTConvert BOOL:valueSerializationEmbedImage]; + BOOL exportVideoSegments = [RCTConvert BOOL:valueExportVideoSegments]; // Make sure that the export settings are valid if ((exportType == nil) || @@ -138,6 +135,7 @@ - (void)present:(nonnull IMGLYMediaEditViewControllerBlock)createMediaEditViewCo self.resolve = resolve; self.reject = reject; self.mediaEditViewController = mediaEditViewController; + self.exportVideoSegments = exportVideoSegments; UIViewController *currentViewController = RCTPresentedViewController(); [currentViewController presentViewController:self.mediaEditViewController animated:YES completion:NULL]; @@ -159,40 +157,31 @@ - (void)dismiss:(nullable PESDKMediaEditViewController *)mediaEditViewController self.resolve = nil; self.reject = nil; self.mediaEditViewController = nil; + self.exportVideoSegments = nil; dispatch_async(dispatch_get_main_queue(), ^{ [mediaEditViewController.presentingViewController dismissViewControllerAnimated:animated completion:completion]; }); } -- (void)handleLicenseError:(nullable NSError *)error +- (void)handleLicenseError:(nullable NSError *)error resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject { - self.licenseError = nil; if (error != nil) { - if ([error.domain isEqualToString:@"ImglyKit.IMGLY.Error"]) { - switch (error.code) { - case 3: - RCTLogWarn(@"%@: %@", NSStringFromClass(self.class), error.localizedDescription); - break; - default: - self.licenseError = error; - RCTLogError(@"%@: %@", NSStringFromClass(self.class), error.localizedDescription); - break; - } - } else { - self.licenseError = error; - RCTLogError(@"Error while unlocking with license: %@", error); - } + reject(RN_IMGLY.kErrorUnableToUnlock, [NSString RN_IMGLY_string:@"Unable to unlock with license." withError:error], error); + return; + } else { + resolve(nil); + return; } } -- (void)unlockWithLicenseURL:(nonnull NSURL *)url {} +- (void)unlockWithLicenseURL:(nonnull NSURL *)url resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {} -- (void)unlockWithLicenseString:(nonnull NSString *)string {} +- (void)unlockWithLicenseString:(nonnull NSString *)string resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {} -- (void)unlockWithLicenseObject:(nonnull NSDictionary *)dictionary {} +- (void)unlockWithLicenseObject:(nonnull NSDictionary *)dictionary resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {} -- (void)unlockWithLicense:(nonnull id)json +- (void)unlockWithLicense:(nonnull id)json resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject { NSString *string = nil; NSURL *url = nil; @@ -218,14 +207,14 @@ - (void)unlockWithLicense:(nonnull id)json } if (url != nil) { - [self unlockWithLicenseURL:url]; + [self unlockWithLicenseURL:url resolve:resolve reject:reject]; } else if (string != nil) { - [self unlockWithLicenseString:string]; + [self unlockWithLicenseString:string resolve:resolve reject:reject]; } else if ([json isKindOfClass:[NSDictionary class]]) { NSDictionary *dictionary = json; - [self unlockWithLicenseObject:dictionary]; + [self unlockWithLicenseObject:dictionary resolve:resolve reject:reject]; } else if (json) { RCTLogConvertError(json, @"a valid license format"); @@ -351,22 +340,22 @@ + (nullable RN_IMGLY_ExportFileURL *)RN_IMGLY_ExportFileURL:(nullable id)json wi + (nullable RN_IMGLY_URLRequestArray *)RN_IMGLY_URLRequestArray:(nullable id)json { - NSArray *array = [RCTConvert NSArray:json]; - NSMutableArray *requests = [NSMutableArray new]; - if (array.count == 0) { return [requests copy]; } - for (id value in array) { - if (value == (id)[NSNull null]) { - RCTLogConvertError(json, @"a valid NSArray"); - return nil; - } - NSURLRequest *request = [RCTConvert NSURLRequest:value]; - if (request == nil) { - RCTLogConvertError(value, @"a valid NSURLRequest"); - return nil; - } - [requests addObject:request]; + NSArray *array = [RCTConvert NSArray:json]; + NSMutableArray *requests = [NSMutableArray new]; + if (array.count == 0) { return [requests copy]; } + for (id value in array) { + if (value == (id)[NSNull null]) { + RCTLogConvertError(json, @"a valid NSArray"); + return nil; } - return [requests copy]; + NSURLRequest *request = [RCTConvert NSURLRequest:value]; + if (request == nil) { + RCTLogConvertError(value, @"a valid NSURLRequest"); + return nil; + } + [requests addObject:request]; + } + return [requests copy]; } @end diff --git a/ios/RNImglyKitSubclass.h b/ios/RNImglyKitSubclass.h index 5386312..a85f07e 100644 --- a/ios/RNImglyKitSubclass.h +++ b/ios/RNImglyKitSubclass.h @@ -39,7 +39,8 @@ #define RN_IMGLY_dictionary_HELPER(prefix) RN_IMGLY_CONCATENATE(prefix, _dictionary) #define RN_IMGLY_dictionary RN_IMGLY_dictionary_HELPER(RN_IMGLY) -@import ImglyKit; +#import +#import @interface RN_IMGLY_ImglyKit () @@ -50,7 +51,6 @@ typedef void (^IMGLYCompletionBlock)(void); @property (class, strong, atomic, nullable) IMGLYConfigurationBlock configureWithBuilder; -@property (strong, atomic, nullable) NSError* licenseError; @property (strong, atomic, nullable) NSString* exportType; @property (strong, atomic, nullable) NSURL* exportFile; @property (atomic) BOOL serializationEnabled; @@ -60,17 +60,19 @@ typedef void (^IMGLYCompletionBlock)(void); @property (strong, atomic, nullable) RCTPromiseResolveBlock resolve; @property (strong, atomic, nullable) RCTPromiseRejectBlock reject; @property (strong, atomic, nullable) PESDKMediaEditViewController* mediaEditViewController; +@property (atomic) BOOL exportVideoSegments; +@property (strong, atomic, nullable) NSString* uuid; - (void)present:(nonnull IMGLYMediaEditViewControllerBlock)createMediaEditViewController withUTI:(nonnull IMGLYUTIBlock)getUTI configuration:(nullable NSDictionary *)dictionary serialization:(nullable NSDictionary *)state resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject; - (void)dismiss:(nullable PESDKMediaEditViewController *)mediaEditViewController animated:(BOOL)animated completion:(nullable IMGLYCompletionBlock)completion; -- (void)handleLicenseError:(nullable NSError *)error; -- (void)unlockWithLicenseURL:(nonnull NSURL *)url; -- (void)unlockWithLicenseString:(nonnull NSString *)string; -- (void)unlockWithLicenseObject:(nonnull NSDictionary *)dictionary; -- (void)unlockWithLicense:(nonnull id)json; +- (void)handleLicenseError:(nullable NSError *)error resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject; +- (void)unlockWithLicenseURL:(nonnull NSURL *)url resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject; +- (void)unlockWithLicenseString:(nonnull NSString *)string resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject; +- (void)unlockWithLicenseObject:(nonnull NSDictionary *)dictionary resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject; +- (void)unlockWithLicense:(nonnull id)json resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject; extern const struct RN_IMGLY_Constants { diff --git a/ios/RNVideoEditorSDK.h b/ios/RNVideoEditorSDK.h index 45a5c1c..bf57cc8 100644 --- a/ios/RNVideoEditorSDK.h +++ b/ios/RNVideoEditorSDK.h @@ -1,7 +1,7 @@ #import #import "RNImglyKit.h" -@import VideoEditorSDK; +#import /// The React Native module for VideoEditor SDK @interface RNVideoEditorSDK : RNVESDKImglyKit diff --git a/ios/RNVideoEditorSDK.m b/ios/RNVideoEditorSDK.m index 2afe432..6e96763 100644 --- a/ios/RNVideoEditorSDK.m +++ b/ios/RNVideoEditorSDK.m @@ -29,10 +29,18 @@ + (void)setWillPresentVideoEditViewController:(RNVESDKWillPresentBlock)willPrese _willPresentVideoEditViewController = willPresentBlock; } +- (void)handleError:(nonnull PESDKVideoEditViewController *)videoEditViewController code:(nullable NSString *)code message:(nullable NSString *)message error:(nullable NSError *)error { + RCTPromiseRejectBlock reject = self.reject; + [self dismiss:videoEditViewController animated:YES completion:^{ + reject(code, message, error); + }]; +} + - (void)present:(nonnull PESDKVideo *)video withConfiguration:(nullable NSDictionary *)dictionary andSerialization:(nullable NSDictionary *)state resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { [self present:^PESDKMediaEditViewController * _Nullable(PESDKConfiguration * _Nonnull configuration, NSData * _Nullable serializationData) { + self.uuid = [[NSUUID new] UUIDString]; PESDKPhotoEditModel *photoEditModel = [[PESDKPhotoEditModel alloc] init]; @@ -57,36 +65,36 @@ - (void)present:(nonnull PESDKVideo *)video withConfiguration:(nullable NSDictio } configuration:dictionary serialization:state resolve:resolve reject:reject]; } -RCT_EXPORT_METHOD(unlockWithLicenseURL:(nonnull NSURL *)url) +RCT_EXPORT_METHOD(unlockWithLicenseURL:(nonnull NSURL *)url resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject) { dispatch_async(dispatch_get_main_queue(), ^{ NSError *error = nil; [VESDK unlockWithLicenseFromURL:url error:&error]; - [self handleLicenseError:error]; + [self handleLicenseError:error resolve:resolve reject:reject]; }); } -RCT_EXPORT_METHOD(unlockWithLicenseString:(nonnull NSString *)string) +RCT_EXPORT_METHOD(unlockWithLicenseString:(nonnull NSString *)string resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject) { dispatch_async(dispatch_get_main_queue(), ^{ NSError *error = nil; [VESDK unlockWithLicenseFromString:string error:&error]; - [self handleLicenseError:error]; + [self handleLicenseError:error resolve:resolve reject:reject]; }); } -RCT_EXPORT_METHOD(unlockWithLicenseObject:(nonnull NSDictionary *)dictionary) +RCT_EXPORT_METHOD(unlockWithLicenseObject:(nonnull NSDictionary *)dictionary resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject) { dispatch_async(dispatch_get_main_queue(), ^{ NSError *error = nil; [VESDK unlockWithLicenseFromDictionary:dictionary error:&error]; - [self handleLicenseError:error]; + [self handleLicenseError:error resolve:resolve reject:reject]; }); } -RCT_EXPORT_METHOD(unlockWithLicense:(nonnull id)json) +RCT_EXPORT_METHOD(unlockWithLicense:(nonnull id)json resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject) { - [super unlockWithLicense:json]; + [super unlockWithLicense:json resolve:resolve reject:reject]; } RCT_EXPORT_METHOD(present:(nonnull NSURLRequest *)request @@ -96,66 +104,131 @@ - (void)present:(nonnull PESDKVideo *)video withConfiguration:(nullable NSDictio reject:(RCTPromiseRejectBlock)reject) { // TODO: Handle React Native URLs from camera roll. - if (request.URL.isFileURL) { - if (![[NSFileManager defaultManager] fileExistsAtPath:request.URL.path]) { - reject(RN_IMGLY.kErrorUnableToLoad, @"File does not exist", nil); - return; - } + if (![self isValidURL:request.URL]) { + reject(RN_IMGLY.kErrorUnableToLoad, @"File does not exist", nil); + return; } PESDKVideo *video = [[PESDKVideo alloc] initWithURL:request.URL]; [self present:video withConfiguration:configuration andSerialization:state resolve:resolve reject:reject]; } -RCT_EXPORT_METHOD(presentComposition:(nonnull RN_IMGLY_URLRequestArray *)requests +RCT_EXPORT_METHOD(presentVideoSegments:(nonnull NSArray *)requests configuration:(nullable NSDictionary *)configuration serialization:(nullable NSDictionary *)state videoSize:(CGSize)videoSize resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { - NSMutableArray *assets = [NSMutableArray new]; - - if (requests.count > 0) { - for (NSURLRequest *request in requests) { - if (request.URL.isFileURL) { - if (![[NSFileManager defaultManager] fileExistsAtPath:request.URL.path]) { - reject(RN_IMGLY.kErrorUnableToLoad, @"File does not exist", nil); - return; - } - } - - AVAsset *asset = [AVAsset assetWithURL:request.URL]; - [assets addObject:asset]; - } - } + [self openEditorFromVideos:requests videoSize:videoSize configuration:configuration serialization:state resolve:resolve reject:reject]; +} - PESDKVideo *video; +RCT_EXPORT_METHOD(presentComposition:(nonnull RN_IMGLY_URLRequestArray *)requests + configuration:(nullable NSDictionary *)configuration + serialization:(nullable NSDictionary *)state + videoSize:(CGSize)videoSize + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + [self openEditorFromVideos:requests videoSize:videoSize configuration:configuration serialization:state resolve:resolve reject:reject]; +} - if (CGSizeEqualToSize(videoSize, CGSizeZero)) { - if (assets.count == 0) { - RCTLogError(@"A video without assets must have a specific size."); - reject(RN_IMGLY.kErrorUnableToLoad, @"The editor requires a valid size when initialized without a video.", nil); - return; - } - video = [[PESDKVideo alloc] initWithAssets:assets]; +- (void)openEditorFromVideos:(nonnull NSArray *)videos videoSize:(CGSize)videoSize configuration:(nullable NSDictionary *)configuration serialization:(nullable NSDictionary *)state resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock) reject +{ + NSMutableArray *segments = [NSMutableArray new]; + + for (id video in videos) { + if ([video isKindOfClass:[NSURLRequest class]]) { + NSURLRequest *request = video; + if (![self isValidURL:request.URL]) { + reject(RN_IMGLY.kErrorUnableToLoad, @"File does not exist", nil); + return; + } + PESDKVideoSegment *videoSegment = [[PESDKVideoSegment alloc] initWithURL:request.URL]; + [segments addObject: videoSegment]; } else { - if (videoSize.height <= 0 || videoSize.width <= 0) { - RCTLogError(@"Invalid video size: width and height must be greater than zero"); - reject(RN_IMGLY.kErrorUnableToLoad, @"Invalid video size: width and height must be greater than zero", nil); - return; + NSURLRequest *request = [RCTConvert NSURLRequest:video]; + if (request == nil) { + // Segment. + NSURLRequest *segmentRequest = [RCTConvert NSURLRequest:[video valueForKey:@"videoURI"]]; + NSNumber *startTime; + NSNumber *endTime; + + id start = [video valueForKey:@"startTime"]; + id end = [video valueForKey:@"endTime"]; + + if (![start isKindOfClass:[NSNull class]]) { + startTime = [RCTConvert NSNumber:start]; + } + if (![end isKindOfClass:[NSNull class]]) { + endTime = [RCTConvert NSNumber:end]; + } + + if (![self isValidURL:segmentRequest.URL]) { + reject(RN_IMGLY.kErrorUnableToLoad, @"File does not exist", nil); + return; } - if (assets.count == 0) { - video = [[PESDKVideo alloc] initWithSize:videoSize]; + PESDKVideoSegment *videoSegment = [[PESDKVideoSegment alloc] initWithURL:segmentRequest.URL startTime:startTime endTime:endTime]; + [segments addObject: videoSegment]; + } else { + if (![self isValidURL:request.URL]) { + reject(RN_IMGLY.kErrorUnableToLoad, @"File does not exist", nil); + return; } - video = [[PESDKVideo alloc] initWithAssets:assets size:videoSize]; + PESDKVideoSegment *videoSegment = [[PESDKVideoSegment alloc] initWithURL:request.URL]; + [segments addObject: videoSegment]; + } + } + } + + PESDKVideo *video; + + if (CGSizeEqualToSize(videoSize, CGSizeZero)) { + if (segments.count == 0) { + RCTLogError(@"A video without assets must have a specific size."); + reject(RN_IMGLY.kErrorUnableToLoad, @"The editor requires a valid size when initialized without a video.", nil); + return; + } + video = [[PESDKVideo alloc] initWithSegments:segments]; + } else { + if (videoSize.height <= 0 || videoSize.width <= 0) { + RCTLogError(@"Invalid video size: width and height must be greater than zero"); + reject(RN_IMGLY.kErrorUnableToLoad, @"Invalid video size: width and height must be greater than zero", nil); + return; + } + if (segments.count == 0) { + video = [[PESDKVideo alloc] initWithSize:videoSize]; } + video = [[PESDKVideo alloc] initWithSegments:segments size:videoSize]; + } - [self present:video withConfiguration:configuration andSerialization:state resolve:resolve reject:reject]; + [self present:video withConfiguration:configuration andSerialization:state resolve:resolve reject:reject]; +} + +- (BOOL)isValidURL:(nonnull NSURL*)url { + if (url.isFileURL) { + if (![[NSFileManager defaultManager] fileExistsAtPath:url.path]) { + return false; + } + } + return true; +} + +- (NSArray *)serializeVideoSegments:(nonnull NSArray *)segments { + NSMutableArray *videoSegments = [NSMutableArray new]; + for (PESDKVideoSegment *segment in segments) { + NSDictionary *videoSegment = @{ + @"videoURI": segment.url.absoluteString, + @"startTime": (segment.startTime != nil) ? segment.startTime : [NSNull null], + @"endTime": (segment.endTime != nil) ? segment.endTime : [NSNull null] + }; + [videoSegments addObject: videoSegment]; + } + return [videoSegments copy]; } #pragma mark - PESDKVideoEditViewControllerDelegate -- (void)videoEditViewController:(nonnull PESDKVideoEditViewController *)videoEditViewController didFinishWithVideoAtURL:(nullable NSURL *)url { +- (void)videoEditViewControllerDidFinish:(nonnull PESDKVideoEditViewController *)videoEditViewController result:(nonnull PESDKVideoEditorResult *)result { NSError *error = nil; id serialization = nil; @@ -171,17 +244,26 @@ - (void)videoEditViewController:(nonnull PESDKVideoEditViewController *)videoEdi } } - RCTPromiseResolveBlock resolve = self.resolve; - RCTPromiseRejectBlock reject = self.reject; - [self dismiss:videoEditViewController animated:YES completion:^{ - if (error == nil) { - resolve(@{ @"video": (url != nil) ? url.absoluteString : [NSNull null], - @"hasChanges": @(videoEditViewController.hasChanges), - @"serialization": (serialization != nil) ? serialization : [NSNull null] }); - } else { - reject(RN_IMGLY.kErrorUnableToExport, [NSString RN_IMGLY_string:@"Unable to export video or serialization." withError:error], error); + if (error == nil) { + RCTPromiseResolveBlock resolve = self.resolve; + NSArray *segments; + + if (self.exportVideoSegments) { + segments = [self serializeVideoSegments:result.task.video.segments]; } - }]; + + [self dismiss:videoEditViewController animated:YES completion:^{ + resolve(@{ @"video": (result.output.url != nil) ? result.output.url.absoluteString : [NSNull null], + @"hasChanges": @(result.status == VESDKVideoEditorStatusRenderedWithChanges), + @"serialization": (serialization != nil) ? serialization : [NSNull null], + @"segments": (segments != nil) ? segments : [NSNull null], + @"videoSize": @{@"height": @(result.task.video.size.height), @"width": @(result.task.video.size.height)}, + @"identifier": self.uuid + }); + }]; + } else { + [self handleError:videoEditViewController code:RN_IMGLY.kErrorUnableToExport message:[NSString RN_IMGLY_string:@"Unable to export video or serialization." withError:error] error:error]; + } } - (void)videoEditViewControllerDidCancel:(nonnull PESDKVideoEditViewController *)videoEditViewController { @@ -191,11 +273,8 @@ - (void)videoEditViewControllerDidCancel:(nonnull PESDKVideoEditViewController * }]; } -- (void)videoEditViewControllerDidFailToGenerateVideo:(nonnull PESDKVideoEditViewController *)videoEditViewController { - RCTPromiseRejectBlock reject = self.reject; - [self dismiss:videoEditViewController animated:YES completion:^{ - reject(RN_IMGLY.kErrorUnableToExport, @"Unable to generate video", nil); - }]; +- (void)videoEditViewControllerDidFail:(nonnull PESDKVideoEditViewController *)videoEditViewController error:(PESDKVideoEditorError *)error { + [self handleError:videoEditViewController code:RN_IMGLY.kErrorUnableToExport message:@"Unable to generate video" error:error]; } @end diff --git a/package.json b/package.json index b14553f..827f0b3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "react-native-videoeditorsdk", "title": "React Native module for VideoEditor SDK", - "version": "2.12.0", + "version": "3.4.0", "description": "A React Native module for VideoEditor SDK. Integrate the video editor into your own HTML5, iOS or Android app - in minutes!", "main": "index.js", "typings": "index.d.ts", @@ -25,7 +25,7 @@ "android" ], "author": { - "name": "img.ly GmbH", + "name": "IMG.LY GmbH", "email": "contact@img.ly" }, "license": "BSD-3-Clause", @@ -35,6 +35,6 @@ "react-native": ">=0.60.0 <1.0.x" }, "dependencies": { - "react-native-imglysdk": "2.12.0" + "react-native-imglysdk": "3.4.0" } }