From 77c59cccb9b052bf84040447fb18fb4ddd5df39f Mon Sep 17 00:00:00 2001 From: Gleb Prischepa Date: Tue, 4 Jun 2024 23:37:02 +0300 Subject: [PATCH] Open photo editor from video editor --- android/app/build.gradle | 2 +- .../flutter/flutter_ve_sdk/MainActivity.kt | 31 ++++++++-- .../flutter_ve_sdk/VideoEditorModule.kt | 62 +++++++++++++++++++ ios/Runner/PhotoEditorModule.swift | 8 +++ ios/Runner/VideoEditorModule.swift | 60 ++++++++++++++++++ 5 files changed, 158 insertions(+), 5 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 5eb31df..96e17e8 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -101,6 +101,6 @@ dependencies { // Photo Editor SDK dependency // WARNING! // Remove this dependency if you only use Video Editor SDK - def banubaPESdkVersion = '1.1.0' + def banubaPESdkVersion = '1.2.0' implementation "com.banuba.sdk:pe-sdk:${banubaPESdkVersion}" } diff --git a/android/app/src/main/kotlin/com/banuba/flutter/flutter_ve_sdk/MainActivity.kt b/android/app/src/main/kotlin/com/banuba/flutter/flutter_ve_sdk/MainActivity.kt index 267b624..518f2d0 100644 --- a/android/app/src/main/kotlin/com/banuba/flutter/flutter_ve_sdk/MainActivity.kt +++ b/android/app/src/main/kotlin/com/banuba/flutter/flutter_ve_sdk/MainActivity.kt @@ -41,6 +41,7 @@ class MainActivity : FlutterActivity() { // For Photo Editor private const val PHOTO_EDITOR_REQUEST_CODE = 8888 + const val EXTRA_EXPORTED_IMAGE = "EXTRA_EXPORTED_IMAGE" private const val METHOD_START_PHOTO_EDITOR = "startPhotoEditor" private const val ARG_EXPORTED_PHOTO_FILE = "argExportedPhotoFilePath" @@ -210,6 +211,20 @@ class MainActivity : FlutterActivity() { override fun onActivityResult(requestCode: Int, result: Int, intent: Intent?) { super.onActivityResult(requestCode, result, intent) if (requestCode == VIDEO_EDITOR_REQUEST_CODE && result == RESULT_OK) { + handleVideoEditorExportResult(intent) + } else if (requestCode == PHOTO_EDITOR_REQUEST_CODE && result == RESULT_OK) { + val data = preparePhotoExportData(intent) + exportResult?.success(data) + } + } + + private fun handleVideoEditorExportResult(intent: Intent?) { + // The image taken on camera screen in Video Editor SDK + val exportImageResult = + intent?.getParcelableExtra("EXTRA_EXPORTED_IMAGE") as? ExportResult.Success + + if (exportImageResult == null) { + // Handle export result from val exportResult = intent?.getParcelableExtra(EXTRA_EXPORTED_SUCCESS) as? ExportResult.Success if (exportResult == null) { @@ -222,10 +237,18 @@ class MainActivity : FlutterActivity() { val data = prepareVideoExportData(exportResult) this.exportResult?.success(data) } - - } else if (requestCode == PHOTO_EDITOR_REQUEST_CODE && result == RESULT_OK) { - val data = preparePhotoExportData(intent) - exportResult?.success(data) + } else { + // IMPORTANT! Release Video Editor SDK + videoEditorModule?.release() + videoEditorModule = null + + activity.startActivityForResult( + PhotoCreationActivity.startFromEditor( + activity.applicationContext, + imageUri = exportImageResult.preview + ), + PHOTO_EDITOR_REQUEST_CODE + ) } } diff --git a/android/app/src/main/kotlin/com/banuba/flutter/flutter_ve_sdk/VideoEditorModule.kt b/android/app/src/main/kotlin/com/banuba/flutter/flutter_ve_sdk/VideoEditorModule.kt index 9f317ca..001509c 100644 --- a/android/app/src/main/kotlin/com/banuba/flutter/flutter_ve_sdk/VideoEditorModule.kt +++ b/android/app/src/main/kotlin/com/banuba/flutter/flutter_ve_sdk/VideoEditorModule.kt @@ -1,22 +1,34 @@ package com.banuba.flutter.flutter_ve_sdk +import android.app.Activity import android.app.Application +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.util.Log import androidx.fragment.app.Fragment +import com.banuba.flutter.flutter_ve_sdk.MainActivity.Companion.EXTRA_EXPORTED_IMAGE import com.banuba.sdk.arcloud.data.source.ArEffectsRepositoryProvider import com.banuba.sdk.arcloud.di.ArCloudKoinModule import com.banuba.sdk.audiobrowser.di.AudioBrowserKoinModule import com.banuba.sdk.audiobrowser.domain.AudioBrowserMusicProvider +import com.banuba.sdk.core.EditorUtilityManager import com.banuba.sdk.core.data.TrackData +import com.banuba.sdk.core.domain.MediaNavigationProcessor import com.banuba.sdk.core.ui.ContentFeatureProvider import com.banuba.sdk.effectplayer.adapter.BanubaEffectPlayerKoinModule +import com.banuba.sdk.export.data.ExportResult import com.banuba.sdk.export.di.VeExportKoinModule import com.banuba.sdk.gallery.di.GalleryKoinModule import com.banuba.sdk.playback.di.VePlaybackSdkKoinModule import com.banuba.sdk.ve.di.VeSdkKoinModule +import com.banuba.sdk.ve.ext.VideoEditorUtils.getKoin import com.banuba.sdk.ve.flow.di.VeFlowKoinModule import com.banuba.sdk.veui.di.VeUiSdkKoinModule import org.koin.android.ext.koin.androidContext import org.koin.core.context.startKoin +import org.koin.core.context.stopKoin +import org.koin.core.error.InstanceCreationException import org.koin.core.qualifier.named import org.koin.dsl.module @@ -49,6 +61,19 @@ class VideoEditorModule { ) } } + + fun release() { + val utilityManager = try { + // EditorUtilityManager is NULL when the token is expired or revoked. + // This dependency is not explicitly created in DI. + getKoin().getOrNull() + } catch (e: InstanceCreationException) { + Log.w("CustomNavigation", "EditorUtilityManager was not initialized!", e) + null + } + utilityManager?.release() + stopKoin() + } } /** @@ -79,5 +104,42 @@ private class SampleIntegrationVeKoinModule { AudioBrowserMusicProvider() } } + + // Provide custom implementation to process media received from Camera screen + single { + OpenPhotoEditorFromVideoEditor() + } + } + + private class OpenPhotoEditorFromVideoEditor() : MediaNavigationProcessor { + + override fun process(activity: Activity, mediaList: List): Boolean { + // Provide custom implementation to process mediaList received from Camera screen. + // Keep in mind that mediaList contains slideshow video as well. + Log.d("CustomMediaNavigation", "Process mediaList = $mediaList") + val imageUri = mediaList.find { it.path?.contains(".png") == true } + + val stayInVideoEditorSDK = if (imageUri == null) { + true + } else { + val exportImageIntent = Intent().apply { + putExtra(EXTRA_EXPORTED_IMAGE , + ExportResult.Success( + emptyList(), + imageUri, + Uri.EMPTY, + Bundle() + ) + ) + } + + // Finish Video Editor SDK + activity.setResult(Activity.RESULT_OK, exportImageIntent) + activity.finish() + false // false - close and open app screen + } + + return stayInVideoEditorSDK + } } } \ No newline at end of file diff --git a/ios/Runner/PhotoEditorModule.swift b/ios/Runner/PhotoEditorModule.swift index af16377..00037c0 100644 --- a/ios/Runner/PhotoEditorModule.swift +++ b/ios/Runner/PhotoEditorModule.swift @@ -47,6 +47,13 @@ class PhotoEditorModule: BanubaPhotoEditorDelegate { }) } + func presentPhotoEditor(with launchConfig: PhotoEditorLaunchConfig) { + photoEditorSDK?.presentPhotoEditor( + withLaunchConfiguration: launchConfig, + completion: nil + ) + } + // MARK: - PhotoEditorSDKDelegate func photoEditorDidCancel(_ photoEditor: BanubaPhotoEditor) { photoEditor.dismissPhotoEditor(animated: true) { [unowned self] in @@ -66,6 +73,7 @@ class PhotoEditorModule: BanubaPhotoEditorDelegate { let data = [ AppDelegate.argExportedPhotoFile: exportedPhotoFileUrl.path, ] + debugPrint("Photo exported = \(exportedPhotoFileUrl.path)") photoEditor.dismissPhotoEditor(animated: true) { [unowned self] in self.flutterResult?(data) self.flutterResult = nil diff --git a/ios/Runner/VideoEditorModule.swift b/ios/Runner/VideoEditorModule.swift index 51893d8..2b5e881 100644 --- a/ios/Runner/VideoEditorModule.swift +++ b/ios/Runner/VideoEditorModule.swift @@ -1,6 +1,7 @@ import Foundation import BanubaVideoEditorSDK import BanubaAudioBrowserSDK +import BanubaPhotoEditorSDK import VideoEditor import VEExportSDK import Flutter @@ -18,11 +19,16 @@ protocol VideoEditor { class VideoEditorModule: VideoEditor { private var videoEditorSDK: BanubaVideoEditor? + // MARK: - PhotoEditorSDK + private var photoEditorModule: PhotoEditorModule? + private var flutterResult: FlutterResult? // Use “true” if you want users could restore the last video editing session. private let restoreLastVideoEditingSession: Bool = false + private var licenseToken: String = "" + func initVideoEditor( token: String?, flutterResult: @escaping FlutterResult @@ -47,6 +53,8 @@ class VideoEditorModule: VideoEditor { return } + licenseToken = token! + videoEditorSDK?.delegate = self flutterResult(nil) } @@ -249,6 +257,58 @@ extension VideoEditorModule: BanubaVideoEditorDelegate { } } + func videoEditor(_ videoEditor: BanubaVideoEditor, shouldProcessMediaUrls urls: [URL]) -> Bool { + // Implement your custom filter to take image + guard let jpegURL = urls.first(where: { $0.pathExtension.lowercased() == "jpeg" }), + let imageData = try? Data(contentsOf: jpegURL), + !imageData.isEmpty, + let resultImage = UIImage(data: imageData) else { + return true + } + + videoEditor.dismissVideoEditor(animated: true) { + DispatchQueue.main.async { [weak self] in + guard let self else { return } + // Calling clearSessionData() also removes any files stored in urls array + videoEditorSDK?.clearSessionData() + + let launchConfig = PhotoEditorLaunchConfig( + hostController: getTopViewController()!, + entryPoint: .editorWithImage(resultImage) + ) + checkLicenseAndOpenPhotoEditor(with: launchConfig) + } + } + + return false + } + + private func checkLicenseAndOpenPhotoEditor(with launchConfig: PhotoEditorLaunchConfig) { + // Deallocate any active instances of both editors to free used resources + // and to prevent "You are trying to create the second instance of the singleton." crash + photoEditorModule = nil + videoEditorSDK = nil + + photoEditorModule = PhotoEditorModule(token: licenseToken) + + guard let photoEditorSDK = photoEditorModule?.photoEditorSDK else { + print("Banuba Photo Editor SDK is not initialized: license token is unknown or incorrect.\nPlease check your license token or contact Banuba") + return + } + + photoEditorSDK.delegate = photoEditorModule + + photoEditorSDK.getLicenseState(completion: { [weak self] isValid in + guard let self else { return } + if isValid { + print("✅ License is active, all good") + photoEditorModule?.presentPhotoEditor(with: launchConfig) + } else { + print("❌ License is either revoked or expired") + } + }) + } + func videoEditorDone(_ videoEditor: BanubaVideoEditor) { exportVideo() }