diff --git a/App.js b/App.js index 744633e..332a3ab 100644 --- a/App.js +++ b/App.js @@ -1,4 +1,8 @@ import React, {Component} from 'react'; + +// Use to access video files https://github.com/react-native-image-picker/react-native-image-picker/tree/main +import {launchImageLibrary} from 'react-native-image-picker'; + import { StyleSheet, Text, @@ -6,6 +10,7 @@ import { View, Platform, NativeModules, + PermissionsAndroid, } from 'react-native'; const {SdkEditorModule} = NativeModules; @@ -23,12 +28,68 @@ async function openVideoEditor() { async function openVideoEditorPIP() { initSDK(); - return await SdkEditorModule.openVideoEditorPIP(); + + // PLEASE GRANT ALL PERMISSIONS TO PROCEED + // The implementation below is for demonstration purposes to show how to use vide for PIP mode + await grantMediaPermissions() + + const videoOptions: ImageLibraryOptions = { + mediaType: 'video', + videoQuality: 'high', + formatAsMp4: false, + quality: 1, + includeBase64: false, + selectionLimit: 1, + durationLimit: 0, + }; + + const result = await launchImageLibrary(videoOptions); + + const videoPath = result.assets[0].originalPath + const videoUri = result.assets[0].uri + console.log('Open video editor in pip mode with video: path = ' + videoPath + ', uri = ' + videoUri); + + if (Platform.OS === 'android') { + // IMPORTANT requirements + // There are 2 types of videoPath - external(/storage/emulated/0/Movies/sample.mp4) and internal (/data/data/$applicationId/...) + // Access to media is required for external - please grant all permissions. + return await SdkEditorModule.openVideoEditorPIP(videoPath); + } else { + return await SdkEditorModule.openVideoEditorPIP(videoUri); + } } async function openVideoEditorTrimmer() { - initSDK(); - return await SdkEditorModule.openVideoEditorTrimmer(); + initSDK(); + + // PLEASE GRANT ALL PERMISSIONS TO PROCEED + // The implementation below is for demonstration purposes to show how to use vide for PIP mode + await grantMediaPermissions() + + const videoOptions: ImageLibraryOptions = { + mediaType: 'video', + videoQuality: 'high', + formatAsMp4: false, + quality: 1, + includeBase64: false, + selectionLimit: 1, + durationLimit: 0, + }; + + const result = await launchImageLibrary(videoOptions); + + const videoPath = result.assets[0].originalPath + const videoUri = result.assets[0].uri + console.log('Open video editor in Trimmer mode with video: path = ' + videoPath + ', uri = ' + videoUri); + + if (Platform.OS === 'android') { + // IMPORTANT requirements + // There are 2 types of videoPath - external(/storage/emulated/0/Movies/sample.mp4) and internal (/data/data/$applicationId/...) + // Access to media is required for external - please grant all permissions. + return await SdkEditorModule.openVideoEditorTrimmer(videoPath); + } else { + return await SdkEditorModule.openVideoEditorTrimmer(videoUri); + } } async function openIosPhotoEditor() { @@ -41,6 +102,22 @@ async function openAndroidPhotoEditor() { return await SdkEditorModule.openPhotoEditor(); } +// It is expected that the user grants all permissions. +// We do not check status here for simplicity +const grantMediaPermissions = async () => { + if (Platform.OS === 'ios') { + return + } + + const status = await PermissionsAndroid.requestMultiple([ + PermissionsAndroid.PERMISSIONS.ACCESS_MEDIA_LOCATION, + PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE, + PermissionsAndroid.PERMISSIONS.READ_MEDIA_VIDEO + ]); + + return status +}; + export default class App extends Component { constructor() { diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 46ef1f7..1511ce9 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -20,6 +20,8 @@ + + if (isValid) { // ✅ The license is active @@ -192,21 +192,23 @@ class SdkEditorModule(reactContext: ReactApplicationContext) : ReactContextBaseJ if (hostActivity == null) { promise.reject(ERR_CODE_NO_HOST_CONTROLLER, "") } else { - // sample_video.mp4 file is hardcoded for demonstrating how to open video editor sdk in the simplest case. - // Please provide valid video URL to open Video Editor in PIP. - val sampleVideoFileName = "sample_video.mp4" - val filesStorage: File = hostActivity.applicationContext.filesDir - val assets: AssetManager = hostActivity.applicationContext.assets - val sampleVideoFile = prepareMediaFile(assets, filesStorage, sampleVideoFileName) - this.resultPromise = promise - val intent = VideoCreationActivity.startFromCamera( + + MediaScannerConnection.scanFile( hostActivity, - PipConfig(video = sampleVideoFile.toUri(), openPipSettings = false), - null, - null - ) - hostActivity.startActivityForResult(intent, OPEN_VIDEO_EDITOR_REQUEST_CODE) + arrayOf(File(pipVideoPath).absolutePath), + arrayOf() + ) { path, uri -> + Log.d(TAG, "Found path = $path, uri = $uri") + + val intent = VideoCreationActivity.startFromCamera( + hostActivity, + PipConfig(video = uri, openPipSettings = false), + null, + null + ) + hostActivity.startActivityForResult(intent, OPEN_VIDEO_EDITOR_REQUEST_CODE) + } } } else { // ❌ Use of SDK is restricted: the license is revoked or expired @@ -216,7 +218,7 @@ class SdkEditorModule(reactContext: ReactApplicationContext) : ReactContextBaseJ } @ReactMethod - fun openVideoEditorTrimmer(promise: Promise) { + fun openVideoEditorTrimmer(videoPath: String, promise: Promise) { checkLicense(callback = { isValid -> if (isValid) { // ✅ The license is active @@ -224,21 +226,23 @@ class SdkEditorModule(reactContext: ReactApplicationContext) : ReactContextBaseJ if (hostActivity == null) { promise.reject(ERR_CODE_NO_HOST_CONTROLLER, "") } else { - // sample_video.mp4 file is hardcoded for demonstrating how to open video editor sdk in the simplest case. - // Please provide valid video URL to open Video Editor in trimmer. - val sampleVideoFileName = "sample_video.mp4" - val filesStorage: File = hostActivity.applicationContext.filesDir - val assets: AssetManager = hostActivity.applicationContext.assets - val sampleVideoFile = prepareMediaFile(assets, filesStorage, sampleVideoFileName) - this.resultPromise = promise - val intent = VideoCreationActivity.startFromTrimmer( + + MediaScannerConnection.scanFile( hostActivity, - arrayOf(sampleVideoFile.toUri()), - null, - null - ) - hostActivity.startActivityForResult(intent, OPEN_VIDEO_EDITOR_REQUEST_CODE) + arrayOf(File(videoPath).absolutePath), + arrayOf() + ) { path, uri -> + Log.d(TAG, "Found path = $path, uri = $uri") + + val intent = VideoCreationActivity.startFromTrimmer( + hostActivity, + arrayOf(uri), + null, + null + ) + hostActivity.startActivityForResult(intent, OPEN_VIDEO_EDITOR_REQUEST_CODE) + } } } else { // ❌ Use of SDK is restricted: the license is revoked or expired diff --git a/ios/Podfile.lock b/ios/Podfile.lock index d290772..92fd326 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -958,6 +958,10 @@ PODS: - React-Mapbuffer (0.73.0): - glog - React-debug + - react-native-image-picker (7.1.1): + - glog + - RCT-Folly (= 2022.05.16.00) + - React-Core - React-nativeconfig (0.73.0) - React-NativeModulesApple (0.73.0): - glog @@ -1174,6 +1178,7 @@ DEPENDENCIES: - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector-modern`) - React-logger (from `../node_modules/react-native/ReactCommon/logger`) - React-Mapbuffer (from `../node_modules/react-native/ReactCommon`) + - react-native-image-picker (from `../node_modules/react-native-image-picker`) - React-nativeconfig (from `../node_modules/react-native/ReactCommon`) - React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`) - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) @@ -1291,6 +1296,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/logger" React-Mapbuffer: :path: "../node_modules/react-native/ReactCommon" + react-native-image-picker: + :path: "../node_modules/react-native-image-picker" React-nativeconfig: :path: "../node_modules/react-native/ReactCommon" React-NativeModulesApple: @@ -1387,6 +1394,7 @@ SPEC CHECKSUMS: React-jsinspector: 9f6fb9ed9f03a0fb961ab8dc2e0e0ee0dc729e77 React-logger: 008caec0d6a587abc1e71be21bfac5ba1662fe6a React-Mapbuffer: 58fe558faf52ecde6705376700f848d0293d1cef + react-native-image-picker: 1a7cd3224036e080fe46bcb955f2eb42fcbf7acc React-nativeconfig: a063483672b8add47a4875b0281e202908ff6747 React-NativeModulesApple: 169506a5fd708ab22811f76ee06a976595c367a1 React-perflogger: b61e5db8e5167f5e70366e820766c492847c082e diff --git a/ios/SdkEditorModule.swift b/ios/SdkEditorModule.swift index 24581a0..bfcb8bf 100644 --- a/ios/SdkEditorModule.swift +++ b/ios/SdkEditorModule.swift @@ -80,19 +80,21 @@ class SdkEditorModule: NSObject, RCTBridgeModule { } } - @objc func openVideoEditorPIP(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) { + @objc func openVideoEditorPIP(_ pipVideoPath: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { self.currentResolve = resolve self.currentReject = reject prepareAudioBrowser() DispatchQueue.main.async { guard let presentedVC = RCTPresentedViewController() else { + reject("RCTPresentedViewController returned nil", nil, nil) return } - // sample_pip_video.mp4 file is hardcoded for demonstrating how to open video editor sdk in the simplest case. - // Please provide valid video URL to open Video Editor in PIP. - let pipVideoURL = Bundle.main.url(forResource: "sample_video", withExtension: "mp4") + guard let pipVideoURL = URL(string: pipVideoPath) else { + reject("Failed to instantiate URL from String", nil, nil) + return + } let pipLaunchConfig = VideoEditorLaunchConfig( entryPoint: .pip, @@ -106,7 +108,7 @@ class SdkEditorModule: NSObject, RCTBridgeModule { } } - @objc func openVideoEditorTrimmer(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) { + @objc func openVideoEditorTrimmer(_ trimmerVideoPath: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { self.currentResolve = resolve self.currentReject = reject @@ -114,21 +116,19 @@ class SdkEditorModule: NSObject, RCTBridgeModule { DispatchQueue.main.async { guard let presentedVC = RCTPresentedViewController() else { + reject("RCTPresentedViewController returned nil", nil, nil) return } - // sample_video.mp4 file is hardcoded for demonstrating how to open video editor sdk in the simplest case. - // Please provide valid video URL to open Video Editor in Trimmer. - let trimmerVideoURL = Bundle.main.url(forResource: "sample_video", withExtension: "mp4")! - let fileManager = FileManager.default - let tmpURL = fileManager.temporaryDirectory.appendingPathComponent("sample_video.mp4") - try? fileManager.removeItem(at: tmpURL) - try? fileManager.copyItem(at: trimmerVideoURL, to: tmpURL) + guard let trimmerVideoURL = URL(string: trimmerVideoPath) else { + reject("Failed to instantiate URL from String", nil, nil) + return + } let trimmerLaunchConfig = VideoEditorLaunchConfig( entryPoint: .trimmer, hostController: presentedVC, - videoItems: [tmpURL], + videoItems: [trimmerVideoURL], musicTrack: nil, animated: true ) diff --git a/ios/SdkEditorModuleBridge.m b/ios/SdkEditorModuleBridge.m index 6f6796b..89befc7 100644 --- a/ios/SdkEditorModuleBridge.m +++ b/ios/SdkEditorModuleBridge.m @@ -18,9 +18,9 @@ @interface RCT_EXTERN_MODULE(SdkEditorModule, NSObject) RCT_EXTERN_METHOD(closeAudioBrowser: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) -RCT_EXTERN_METHOD(openVideoEditorPIP: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +RCT_EXTERN_METHOD(openVideoEditorPIP:(NSString *)pipVideoPath resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) -RCT_EXTERN_METHOD(openVideoEditorTrimmer: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +RCT_EXTERN_METHOD(openVideoEditorTrimmer:(NSString *)trimmerVideoPath resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) RCT_EXTERN_METHOD(initPhotoEditor:(NSString *) token resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) diff --git a/ios/vesdkreactnativecliintegrationsample.xcodeproj/project.pbxproj b/ios/vesdkreactnativecliintegrationsample.xcodeproj/project.pbxproj index 462161b..4d462fa 100644 --- a/ios/vesdkreactnativecliintegrationsample.xcodeproj/project.pbxproj +++ b/ios/vesdkreactnativecliintegrationsample.xcodeproj/project.pbxproj @@ -21,7 +21,6 @@ C2A6917D294C9CE6005DFA75 /* AudioBrowserModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2A6917C294C9CE6005DFA75 /* AudioBrowserModule.swift */; }; C2A69180294C9D1C005DFA75 /* CustomViewControllerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2A6917F294C9D1C005DFA75 /* CustomViewControllerFactory.swift */; }; C2A69183294CA0AB005DFA75 /* sample_audio.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = C2A69182294CA0AB005DFA75 /* sample_audio.mp3 */; }; - C2B80BDF294DFBFA006D11FA /* sample_video.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = C2B80BDE294DFBFA006D11FA /* sample_video.mp4 */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -46,7 +45,6 @@ C2A6917C294C9CE6005DFA75 /* AudioBrowserModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioBrowserModule.swift; sourceTree = ""; }; C2A6917F294C9D1C005DFA75 /* CustomViewControllerFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomViewControllerFactory.swift; sourceTree = ""; }; C2A69182294CA0AB005DFA75 /* sample_audio.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; name = sample_audio.mp3; path = ../android/app/src/main/assets/sample_audio.mp3; sourceTree = ""; }; - C2B80BDE294DFBFA006D11FA /* sample_video.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; name = sample_video.mp4; path = ../android/app/src/main/assets/sample_video.mp4; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; F3D506A8DB72071BDDDE58E8 /* libPods-vesdkreactnativecliintegrationsample-vesdkreactnativecliintegrationsampleTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-vesdkreactnativecliintegrationsample-vesdkreactnativecliintegrationsampleTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -104,7 +102,6 @@ 83CBB9F61A601CBA00E9B192 = { isa = PBXGroup; children = ( - C2B80BDE294DFBFA006D11FA /* sample_video.mp4 */, 7837EBDA28AF8E3C00174DDB /* Localizable.strings */, 13B07FAE1A68108700A75B9A /* vesdkreactnativecliintegrationsample */, 832341AE1AAA6A7D00B99B32 /* Libraries */, @@ -199,7 +196,6 @@ buildActionMask = 2147483647; files = ( 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */, - C2B80BDF294DFBFA006D11FA /* sample_video.mp4 in Resources */, C2A69183294CA0AB005DFA75 /* sample_audio.mp3 in Resources */, 7837EBDB28AF8E3C00174DDB /* Localizable.strings in Resources */, 7837EBDD28AF91FD00174DDB /* Music in Resources */, diff --git a/package.json b/package.json index d5d7c32..b1e50c5 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "eslint": "^7.32.0", "jest": "^29.6.3", "metro-react-native-babel-preset": "^0.76.9", + "react-native-image-picker": "7.1.1", "react-test-renderer": "18.0.0", "typescript": "5.0.4" }, diff --git a/yarn.lock b/yarn.lock index a87353d..5226191 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7107,6 +7107,16 @@ __metadata: languageName: node linkType: hard +"react-native-image-picker@npm:7.1.1": + version: 7.1.1 + resolution: "react-native-image-picker@npm:7.1.1" + peerDependencies: + react: "*" + react-native: "*" + checksum: 10/85a8d946da5c7e3187a1229bb1910e0b3d0a8664d591fcf322e1c7fa38997d2f182f0ea26c68bc7a0cb17ad78a6cb2987a2d54d1f8268e94c5a0745c65b4281f + languageName: node + linkType: hard + "react-native@npm:0.73.0": version: 0.73.0 resolution: "react-native@npm:0.73.0" @@ -8501,6 +8511,7 @@ __metadata: react: "npm:18.2.6" react-native: "npm:0.73.0" react-native-gradle-plugin: "npm:^0.71.19" + react-native-image-picker: "npm:7.1.1" react-test-renderer: "npm:18.0.0" typescript: "npm:5.0.4" languageName: unknown