Jul 10, 2024
How to Add a Video Creation and Editing Feature to Your Flutter App
Unlock the power of video creation in your Flutter app! Dive into our article to learn how the Banuba SDK can transform your app into a one-stop video creation studio.
Author
Vikrant SinghSoftware Engineer III
In today's world, where short-form video content reigns supreme and user-generated creations are king, equipping your Flutter app with video creation and editing features is no longer a fad - it's a strategic move. Imagine empowering your users to become instant video makers within your app. This article dives into the exciting realm of video creator/editor functionalities in Flutter using the Banuba SDK. Get ready to explore how you can transform your app into a one-stop video creation studio, boosting user engagement and unleashing a wave of creative expression.
Adding Banuba's cutting-edge video creation and video editing capabilities to your Flutter projects is easier than you think! While there's no dedicated Flutter SDK, this blog breaks down the integration process for iOS and Android, making it a breeze to add exciting video functionalities to your apps.
Installation
- Android
GitHub Packages is used for downloading Android Video Editor SDK modules.
First, add repositories to gradle file in allprojects section.
allprojects {
repositories {
maven {
name = "GitHubPackages"
url = uri("https://maven.pkg.github.com/Banuba/banuba-ve-sdk")
credentials {
username = "Banuba"
password = "\u0038\u0036\u0032\u0037\u0063\u0035\u0031\u0030\u0033\u0034\u0032\u0063\u0061\u0033\u0065\u0061\u0031\u0032\u0034\u0064\u0065\u0066\u0039\u0062\u0034\u0030\u0063\u0063\u0037\u0039\u0038\u0063\u0038\u0038\u0066\u0034\u0031\u0032\u0061\u0038"
}
}
maven {
name = "ARCloudPackages"
url = uri("https://maven.pkg.github.com/Banuba/banuba-ar")
credentials {
username = "Banuba"
password = "\u0038\u0036\u0032\u0037\u0063\u0035\u0031\u0030\u0033\u0034\u0032\u0063\u0061\u0033\u0065\u0061\u0031\u0032\u0034\u0064\u0065\u0066\u0039\u0062\u0034\u0030\u0063\u0063\u0037\u0039\u0038\u0063\u0038\u0038\u0066\u0034\u0031\u0032\u0061\u0038"
}
}
maven {
name "GitHubPackagesEffectPlayer"
url "https://maven.pkg.github.com/sdk-banuba/banuba-sdk-android"
credentials {
username = "sdk-banuba"
password = "\u0067\u0068\u0070\u005f\u0033\u0057\u006a\u0059\u004a\u0067\u0071\u0054\u0058\u0058\u0068\u0074\u0051\u0033\u0075\u0038\u0051\u0046\u0036\u005a\u0067\u004f\u0041\u0053\u0064\u0046\u0032\u0045\u0046\u006a\u0030\u0036\u006d\u006e\u004a\u004a"
}
}
}
}Next, specify a list of dependencies in gradle file.
def banubaSdkVersion = '1.36.0'
implementation "com.banuba.sdk:ffmpeg:5.1.3"
implementation "com.banuba.sdk:camera-sdk:${banubaSdkVersion}"
implementation "com.banuba.sdk:camera-ui-sdk:${banubaSdkVersion}"
implementation "com.banuba.sdk:core-sdk:${banubaSdkVersion}"
implementation "com.banuba.sdk:core-ui-sdk:${banubaSdkVersion}"
implementation "com.banuba.sdk:ve-flow-sdk:${banubaSdkVersion}"
implementation "com.banuba.sdk:ve-timeline-sdk:${banubaSdkVersion}"
implementation "com.banuba.sdk:ve-sdk:${banubaSdkVersion}"
implementation "com.banuba.sdk:ve-ui-sdk:${banubaSdkVersion}"
implementation "com.banuba.sdk:ve-gallery-sdk:${banubaSdkVersion}"
implementation "com.banuba.sdk:ve-effects-sdk:${banubaSdkVersion}"
implementation "com.banuba.sdk:effect-player-adapter:${banubaSdkVersion}"
implementation "com.banuba.sdk:ar-cloud:${banubaSdkVersion}"
implementation "com.banuba.sdk:ve-audio-browser-sdk:${banubaSdkVersion}"
implementation "com.banuba.sdk:ve-export-sdk:${banubaSdkVersion}"
implementation "com.banuba.sdk:ve-playback-sdk:${banubaSdkVersion}"Additionally, make sure the following plugins are in your app gradle and at the top of the file.
plugins { id "com.android.application" id "kotlin-android" id "dev.flutter.flutter-gradle-plugin" id "kotlin-parcelize" }
- iOS
Add iOS Video Editor SDK dependencies to your Podfile
`sdk_version = '1.36.0'
pod 'BanubaARCloudSDK', sdk_version #optional
pod 'BanubaVideoEditorSDK', sdk_version
pod 'BanubaAudioBrowserSDK', sdk_version #optional
pod 'BanubaSDK', sdk_version #optional
pod 'BanubaSDKSimple', sdk_version
pod 'BanubaSDKServicing', sdk_version
pod 'VideoEditor', sdk_version
pod 'BanubaUtilities', sdk_version
pod 'BanubaVideoEditorGallerySDK', sdk_version #optional
pod 'BanubaLicenseServicingSDK', sdk_version
pod 'BNBLicenseUtils', sdk_version
pod 'VEExportSDK', sdk_version
pod 'VEEffectsSDK', sdk_version
pod 'VEPlaybackSDK', sdk_version`
Configuration
- Android
Next, specify VideoCreationActivity in your AndroidManifest.xml. This Activity combines a number of screens into video editor flow.
<activity
android:name="com.banuba.sdk.ve.flow.VideoCreationActivity"
android:screenOrientation="portrait"
android:theme="@style/CustomIntegrationAppTheme"
android:windowSoftInputMode="adjustResize"
tools:replace="android:theme" />Next, allow Network by adding permissions:
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />and android:usesCleartextTraffic="true" in AndroidManifest.xml.
Please set up correctly network security config and use of android:usesCleartextTraffic by following guide.
Create new Kotlin class VideoEditorModule.kt in your project for initializing and customizing Video Editor SDK features:
package com.banuba.flutter.flutter_ve_sdk
import android.app.Application
import androidx.fragment.app.Fragment
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.data.TrackData
import com.banuba.sdk.core.ui.ContentFeatureProvider
import com.banuba.sdk.effectplayer.adapter.BanubaEffectPlayerKoinModule
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.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.qualifier.named
import org.koin.dsl.module
class VideoEditorModule {
fun initialize(application: Application) {
startKoin {
androidContext(application)
allowOverride(true)
// IMPORTANT! order of modules is required
modules(
VeSdkKoinModule().module,
VeExportKoinModule().module,
VePlaybackSdkKoinModule().module,
// Use AudioBrowserKoinModule ONLY if your contract includes this feature.
AudioBrowserKoinModule().module,
// IMPORTANT! ArCloudKoinModule should be set before TokenStorageKoinModule to get effects from the cloud
ArCloudKoinModule().module,
VeUiSdkKoinModule().module,
VeFlowKoinModule().module,
BanubaEffectPlayerKoinModule().module,
GalleryKoinModule().module,
// Sample integration module
SampleIntegrationVeKoinModule().module,
)
}
}
}
/**
* All dependencies mentioned in this module will override default
* implementations provided in VE UI SDK.
* Some dependencies has no default implementations. It means that
* these classes fully depends on your requirements
*/
private class SampleIntegrationVeKoinModule {
val module = module {
single<ArEffectsRepositoryProvider>(createdAtStart = true) {
ArEffectsRepositoryProvider(
arEffectsRepository = get(named("backendArEffectsRepository")),
ioDispatcher = get(named("ioDispatcher"))
)
}
// Audio Browser provider implementation.
single<ContentFeatureProvider<TrackData, Fragment>>(
named("musicTrackProvider")
) {
if (MainActivity.CONFIG_ENABLE_CUSTOM_AUDIO_BROWSER) {
AudioBrowserContentProvider()
} else {
// Default implementation that supports Soundstripe, Mubert and Local audio stored on the device
AudioBrowserMusicProvider()
}
}
}
}- iOS
Create new Swift class VideoEditorModule.swift in your project for initializing and customizing Video Editor SDK features:
import Foundation
import BanubaVideoEditorSDK
import BanubaAudioBrowserSDK
import VideoEditor
import VEExportSDK
import Flutter
protocol VideoEditor {
func initVideoEditor(token: String?, flutterResult: @escaping FlutterResult)
func openVideoEditorDefault(fromViewController controller: FlutterViewController, flutterResult: @escaping FlutterResult)
func openVideoEditorPIP(fromViewController controller: FlutterViewController, videoURL: URL, flutterResult: @escaping FlutterResult)
func openVideoEditorTrimmer(fromViewController controller: FlutterViewController, videoURL: URL, flutterResult: @escaping FlutterResult)
}
class VideoEditorModule: VideoEditor {
private var videoEditorSDK: BanubaVideoEditor?
private var flutterResult: FlutterResult?
// Use “true” if you want users could restore the last video editing session.
private let restoreLastVideoEditingSession: Bool = false
func initVideoEditor(
token: String?,
flutterResult: @escaping FlutterResult
) {
guard videoEditorSDK == nil else {
flutterResult(nil)
return
}
let config = VideoEditorConfig()
// Make customization here
videoEditorSDK = BanubaVideoEditor(
token: token ?? "",
configuration: config,
externalViewControllerFactory: self.getAppDelegate().provideCustomViewFactory()
)
if videoEditorSDK == nil {
flutterResult(FlutterError(code: AppDelegate.errEditorNotInitialized, message: "", details: nil))
return
}
videoEditorSDK?.delegate = self
flutterResult(nil)
}
func openVideoEditorDefault(
fromViewController controller: FlutterViewController,
flutterResult: @escaping FlutterResult
) {
self.flutterResult = flutterResult
let config = VideoEditorLaunchConfig(
entryPoint: .camera,
hostController: controller,
animated: true
)
checkLicenseAndStartVideoEditor(with: config, flutterResult: flutterResult)
}
func openVideoEditorPIP(
fromViewController controller: FlutterViewController,
videoURL: URL,
flutterResult: @escaping FlutterResult
) {
self.flutterResult = flutterResult
let pipLaunchConfig = VideoEditorLaunchConfig(
entryPoint: .pip,
hostController: controller,
pipVideoItem: videoURL,
musicTrack: nil,
animated: true
)
checkLicenseAndStartVideoEditor(with: pipLaunchConfig, flutterResult: flutterResult)
}
func openVideoEditorTrimmer(
fromViewController controller: FlutterViewController,
videoURL: URL,
flutterResult: @escaping FlutterResult
) {
self.flutterResult = flutterResult
let trimmerLaunchConfig = VideoEditorLaunchConfig(
entryPoint: .trimmer,
hostController: controller,
videoItems: [videoURL],
musicTrack: nil,
animated: true
)
checkLicenseAndStartVideoEditor(with: trimmerLaunchConfig, flutterResult: flutterResult)
}
func checkLicenseAndStartVideoEditor(with config: VideoEditorLaunchConfig, flutterResult: @escaping FlutterResult) {
if videoEditorSDK == nil {
flutterResult(FlutterError(code: AppDelegate.errEditorNotInitialized, message: "", details: nil))
return
}
// Checking the license might take around 1 sec in the worst case.
// Please optimize use if this method in your application for the best user experience
videoEditorSDK?.getLicenseState(completion: { [weak self] isValid in
guard let self else { return }
if isValid {
print("✅ The license is active")
DispatchQueue.main.async {
self.videoEditorSDK?.presentVideoEditor(
withLaunchConfiguration: config,
completion: nil
)
}
} else {
if self.restoreLastVideoEditingSession == false {
self.videoEditorSDK?.clearSessionData()
}
self.videoEditorSDK = nil
print("❌ Use of SDK is restricted: the license is revoked or expired")
flutterResult(FlutterError(code: "ERR_SDK_LICENSE_REVOKED", message: "", details: nil))
}
})
}
private func getAppDelegate() -> AppDelegate {
return UIApplication.shared.delegate as! AppDelegate
}
}
// MARK: - Export flow
extension VideoEditorModule {
func exportVideo() {
let progressView = ProgressViewController.makeViewController()
progressView.cancelHandler = { [weak self] in
self?.videoEditorSDK?.stopExport()
}
getTopViewController()?.present(progressView, animated: true)
let manager = FileManager.default
// File name
let firstFileURL = manager.temporaryDirectory.appendingPathComponent("banuba_demo_ve.mov")
if manager.fileExists(atPath: firstFileURL.path) {
try? manager.removeItem(at: firstFileURL)
}
// Video configuration
let exportVideoConfigurations: [ExportVideoConfiguration] = [
ExportVideoConfiguration(
fileURL: firstFileURL,
quality: .auto,
useHEVCCodecIfPossible: true,
watermarkConfiguration: nil
)
]
// Set up export
let exportConfiguration = ExportConfiguration(
videoConfigurations: exportVideoConfigurations,
isCoverEnabled: true,
gifSettings: nil
)
videoEditorSDK?.export(
using: exportConfiguration,
exportProgress: { [weak progressView] progress in progressView?.updateProgressView(with: Float(progress)) }
) { [weak self] (error, coverImage) in
// Export Callback
DispatchQueue.main.async {
progressView.dismiss(animated: true) {
// if export cancelled just hide progress view
if let error, error as NSError == exportCancelledError {
return
}
self?.completeExport(videoUrl: firstFileURL, error: error, coverImage: coverImage?.coverImage)
}
}
}
}
private func completeExport(videoUrl: URL, error: Error?, coverImage: UIImage?) {
videoEditorSDK?.dismissVideoEditor(animated: true) {
let success = error == nil
if success {
let exportedVideoFilePath = videoUrl.path
print("Video exported successfully = \(exportedVideoFilePath))")
let coverImageData = coverImage?.pngData()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH-mm-ss.SSS"
let coverImageURL = FileManager.default.temporaryDirectory.appendingPathComponent("export_preview-\(dateFormatter.string(from: Date())).png")
try? coverImageData?.write(to: coverImageURL)
let data = [
AppDelegate.argExportedVideoFile: exportedVideoFilePath,
AppDelegate.argExportedVideoCoverPreviewPath: coverImageURL.path
]
self.flutterResult?(data)
} else {
print("Error while exporting video = \(String(describing: error))")
self.flutterResult?(FlutterError(code: "ERR_MISSING_EXPORT_RESULT", message: "", details: nil))
}
// Remove strong reference to video editor sdk instance
if self.restoreLastVideoEditingSession == false {
self.videoEditorSDK?.clearSessionData()
}
self.videoEditorSDK = nil
}
}
func getTopViewController() -> UIViewController? {
let keyWindow = UIApplication
.shared
.connectedScenes
.flatMap { ($0 as? UIWindowScene)?.windows ?? [] }
.last { $0.isKeyWindow }
var topController = keyWindow?.rootViewController
while let newTopController = topController?.presentedViewController {
topController = newTopController
}
return topController
}
}
// MARK: - BanubaVideoEditorSDKDelegate
extension VideoEditorModule: BanubaVideoEditorDelegate {
func videoEditorDidCancel(_ videoEditor: BanubaVideoEditor) {
videoEditor.dismissVideoEditor(animated: true) {
// remove strong reference to video editor sdk instance
if self.restoreLastVideoEditingSession == false {
self.videoEditorSDK?.clearSessionData()
}
self.videoEditorSDK = nil
}
}
func videoEditorDone(_ videoEditor: BanubaVideoEditor) {
exportVideo()
}
}
Launch
- Android
Flutter platform channels approach is used for communication between Flutter and Android.
Set up channel message handler in your MainActivity to listen to calls from Flutter.
class MainActivity : FlutterActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
val appFlutterEngine = requireNotNull(flutterEngine)
GeneratedPluginRegistrant.registerWith(appFlutterEngine)
MethodChannel(
appFlutterEngine.dartExecutor.binaryMessenger,
"banubaSdkChannel"
).setMethodCallHandler { call, result ->
// Handle method calls
}
}
}Send initVideoEditor message from Flutter to Android:
await platformChannel.invokeMethod('initVideoEditor', LICENSE_TOKEN);and add corresponding handler on Android side to initialize Video Editor.
val licenseToken = call.arguments as String
val editorSDK = BanubaVideoEditor.initialize(licenseToken)
if (editorSDK == null) {
// The SDK token is incorrect - empty or truncated
...
} else {
if (videoEditorModule == null) {
// Initialize video editor sdk dependencies
videoEditorModule = VideoEditorModule().apply {
initialize(application)
}
}
...
}
Finally, once the SDK in initialized you can send startVideoEditor message from Flutter to Android:
final result = await platformChannel.invokeMethod('startVideoEditor');and add the corresponding handler on Android side to start Video Editor.
❗ Important
- Instance
editorSDKisnullif the license token is incorrect. In this case you cannot use photo editor. Check your license token. - It is highly recommended to check if the license is active before starting Photo Editor.
- iOS
Flutter platform channels approach is used for communication between Flutter and iOS.
Set up channel message handler in your AppDelegate.swift to listen to calls from Flutter.
let binaryMessenger = controller as? FlutterBinaryMessenger {
let channel = FlutterMethodChannel(
name: AppDelegate.channelName,
binaryMessenger: binaryMessenger
)
channel.setMethodCallHandler { methodCall, result in
...
}Send initVideoEditor message from Flutter to iOS:
await platformChannel.invokeMethod('initVideoEditor', LICENSE_TOKEN);and add corresponding handler on iOS side to initialize Video Editor.
Initialize Video Editor SDK using license token in VideoEditorModule on iOS.
let videoEditor = BanubaVideoEditor(
token: token,
...
)Finally, once the SDK in initialized you can send startVideoEditor message from Flutter to iOS:
final result = await platformChannel.invokeMethod('startVideoEditor');and add the corresponding handler on iOS side to start Video Editor.
❗ Important
- Instance
videoEditorisnilif the license token is incorrect. In this case you cannot use photo editor. Check your license token. - It is highly recommended to check if the license if active before starting Photo Editor.
Conclusion
This comprehensive blog unveils the power of Banuba SDK to bring cutting-edge video creation and editing features to your Flutter apps. You’ve learnt how to integratre it on both iOS and Android platforms, even without a dedicated Flutter SDK. Follow step-by-step instructions on installation, configuration, and launch, complete with code snippets and explanations. Empower your users to become video creators within your app and unlock a new dimension of user engagement and creative expression.
Subscribe to Our Newsletter
Subscribe to RSS
Press & Media Hub RSS FeedRELATED ARTICLES