diff --git a/android/build.gradle b/android/build.gradle index e63b407..e883ef6 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,7 +2,7 @@ group 'me.anharu.video_editor' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.5.10' repositories { google() jcenter() @@ -41,5 +41,5 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'com.github.MasayukiSuda:Mp4Composer-android:v0.3.9' + implementation 'com.github.MasayukiSuda:Mp4Composer-android:v0.4.1' } diff --git a/android/src/main/kotlin/me/anharu/video_editor/SpeedChanger.kt b/android/src/main/kotlin/me/anharu/video_editor/SpeedChanger.kt new file mode 100644 index 0000000..e2e142a --- /dev/null +++ b/android/src/main/kotlin/me/anharu/video_editor/SpeedChanger.kt @@ -0,0 +1,43 @@ +package me.anharu.video_editor + + +import android.app.Activity +import com.daasuu.mp4compose.composer.Mp4Composer +import io.flutter.plugin.common.MethodChannel.Result + +class SpeedChanger(inputVideo: String, outputVideo: String, val result: Result, val activity: Activity) { + var composer: Mp4Composer = Mp4Composer(inputVideo, outputVideo) + + fun speed(speed:Float) { + composer.timeScale(10F) + .listener(object : Mp4Composer.Listener { + override fun onProgress(progress: Double) { + + } + + override fun onCurrentWrittenVideoTime(timeUs: Long) { + TODO("Not yet implemented") + } + + override fun onCompleted() { + activity.runOnUiThread(Runnable { + result.success(null) + }) + } + + override fun onCanceled() { + activity.runOnUiThread(Runnable { + result.error("user_cancelled", "Cancelled by user", null) + }) + } + + override fun onFailed(exception: Exception?) { + exception?.printStackTrace() + activity.runOnUiThread(Runnable { + result.error("video_trim_failed", exception?.localizedMessage, exception?.stackTrace) + }) + } + + }).start(); + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/me/anharu/video_editor/VideoEditorPlugin.kt b/android/src/main/kotlin/me/anharu/video_editor/VideoEditorPlugin.kt index d48b84f..c71720b 100644 --- a/android/src/main/kotlin/me/anharu/video_editor/VideoEditorPlugin.kt +++ b/android/src/main/kotlin/me/anharu/video_editor/VideoEditorPlugin.kt @@ -93,7 +93,22 @@ public class VideoEditorPlugin : FlutterPlugin, MethodCallHandler, PluginRegistr val endTime: Long = call.argument("endTime")?.toLong() ?: -1 VideoTrimmer(srcFilePath, destFilePath, result, getActivity).trimVideo(startTime, endTime) } + "speed_change" -> { + val getActivity = activity ?: return + val srcFilePath: String = call.argument("srcFilePath") ?: run { + result.error("src_file_path_not_found", "the src file path is not found.", null) + return + } + val destFilePath: String = call.argument("destFilePath") ?: run { + result.error("dest_file_path_not_found", "the dest file path is not found.", null) + return + } + val speed: Float = call.argument("speed")?.toFloat() ?: 0F + print("===>$speed"); + SpeedChanger(srcFilePath, destFilePath, result, getActivity).speed(speed) + } else -> { + print("===>sxxxxxxxx"); result.notImplemented() } } diff --git a/android/src/main/kotlin/me/anharu/video_editor/VideoGeneratorService.kt b/android/src/main/kotlin/me/anharu/video_editor/VideoGeneratorService.kt index c8db1d9..6b93c17 100644 --- a/android/src/main/kotlin/me/anharu/video_editor/VideoGeneratorService.kt +++ b/android/src/main/kotlin/me/anharu/video_editor/VideoGeneratorService.kt @@ -55,6 +55,9 @@ class VideoGeneratorService( override fun onProgress(progress: Double) { println("onProgress = " + progress) } + override fun onCurrentWrittenVideoTime(timeUs: Long) { + TODO("Not yet implemented") + } override fun onCompleted() { activity.runOnUiThread(Runnable { diff --git a/android/src/main/kotlin/me/anharu/video_editor/VideoTrimmer.kt b/android/src/main/kotlin/me/anharu/video_editor/VideoTrimmer.kt index 4135f3a..c7ddda1 100644 --- a/android/src/main/kotlin/me/anharu/video_editor/VideoTrimmer.kt +++ b/android/src/main/kotlin/me/anharu/video_editor/VideoTrimmer.kt @@ -14,6 +14,10 @@ class VideoTrimmer(inputVideo: String, outputVideo: String, val result: Result, } + override fun onCurrentWrittenVideoTime(timeUs: Long) { + + } + override fun onCompleted() { activity.runOnUiThread(Runnable { result.success(null) diff --git a/example/android/build.gradle b/example/android/build.gradle index c384adc..d57d8c3 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.4.32' repositories { google() jcenter() diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 4224a21..8c7c282 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -4,12 +4,24 @@ PODS: - Flutter - image_picker (0.0.1): - Flutter + - libwebp (1.2.0): + - libwebp/demux (= 1.2.0) + - libwebp/mux (= 1.2.0) + - libwebp/webp (= 1.2.0) + - libwebp/demux (1.2.0): + - libwebp/webp + - libwebp/mux (1.2.0): + - libwebp/demux + - libwebp/webp (1.2.0) - path_provider (0.0.1): - Flutter - tapioca (0.0.1): - Flutter - video_player (0.0.1): - Flutter + - video_thumbnail (0.0.1): + - Flutter + - libwebp DEPENDENCIES: - Flutter (from `Flutter`) @@ -18,6 +30,11 @@ DEPENDENCIES: - path_provider (from `.symlinks/plugins/path_provider/ios`) - tapioca (from `.symlinks/plugins/tapioca/ios`) - video_player (from `.symlinks/plugins/video_player/ios`) + - video_thumbnail (from `.symlinks/plugins/video_thumbnail/ios`) + +SPEC REPOS: + trunk: + - libwebp EXTERNAL SOURCES: Flutter: @@ -32,14 +49,18 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/tapioca/ios" video_player: :path: ".symlinks/plugins/video_player/ios" + video_thumbnail: + :path: ".symlinks/plugins/video_thumbnail/ios" SPEC CHECKSUMS: Flutter: 434fef37c0980e73bb6479ef766c45957d4b510c gallery_saver: 9fc173c9f4fcc48af53b2a9ebea1b643255be542 image_picker: 50e7c7ff960e5f58faa4d1f4af84a771c671bc4a + libwebp: e90b9c01d99205d03b6bb8f2c8c415e5a4ef66f0 path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c tapioca: 59e9fc89671d49e2cefe6cd59c3efe678e589e89 video_player: 9cc823b1d9da7e8427ee591e8438bfbcde500e6e + video_thumbnail: c4e2a3c539e247d4de13cd545344fd2d26ffafd1 PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index bc8450a..7128b22 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -168,7 +168,7 @@ TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; - DevelopmentTeam = SCAN67V3XX; + DevelopmentTeam = WQZP98NKJH; LastSwiftMigration = 1100; }; }; @@ -229,17 +229,21 @@ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", "${BUILT_PRODUCTS_DIR}/gallery_saver/gallery_saver.framework", "${BUILT_PRODUCTS_DIR}/image_picker/image_picker.framework", + "${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework", "${BUILT_PRODUCTS_DIR}/path_provider/path_provider.framework", "${BUILT_PRODUCTS_DIR}/tapioca/tapioca.framework", "${BUILT_PRODUCTS_DIR}/video_player/video_player.framework", + "${BUILT_PRODUCTS_DIR}/video_thumbnail/video_thumbnail.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/gallery_saver.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_picker.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libwebp.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/tapioca.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/video_player.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/video_thumbnail.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -373,7 +377,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = SCAN67V3XX; + DEVELOPMENT_TEAM = WQZP98NKJH; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -506,7 +510,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = SCAN67V3XX; + DEVELOPMENT_TEAM = WQZP98NKJH; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -534,7 +538,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = SCAN67V3XX; + DEVELOPMENT_TEAM = WQZP98NKJH; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", diff --git a/example/lib/video_player_screen.dart b/example/lib/video_player_screen.dart index b20fa0d..2f41907 100644 --- a/example/lib/video_player_screen.dart +++ b/example/lib/video_player_screen.dart @@ -22,12 +22,16 @@ class _VideoAppState extends State { @override void initState() { super.initState(); - _controller = VideoPlayerController.file(File(path)) - ..initialize().then((_) { - // Ensure the first frame is shown after the video is initialized, even before the play button has been pressed. - setState(() {}); - print("output video ==== ${_controller.value.duration.inSeconds}"); - }); + initialize(); + } + initialize() async { + try { + _controller = VideoPlayerController.file(File(path)); + await _controller.initialize(); + setState(() {}); + } catch(error){ + print(error); + } } @override diff --git a/example/lib/video_trimmer/video_trim_screen.dart b/example/lib/video_trimmer/video_trim_screen.dart index 7d782ca..c8191cd 100644 --- a/example/lib/video_trimmer/video_trim_screen.dart +++ b/example/lib/video_trimmer/video_trim_screen.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:image_picker/image_picker.dart'; import 'package:path_provider/path_provider.dart'; import 'package:tapioca/tapioca.dart'; @@ -20,6 +21,7 @@ class _VideoTrimScreenState extends State { double endPos = -1; initializeVideo() { + if (_video == null) return; _controller = VideoPlayerController.file(File(_video!.path)) ..initialize().then((_) { // Ensure the first frame is shown after the video is initialized, even before the play button has been pressed. @@ -52,10 +54,13 @@ class _VideoTrimScreenState extends State { print("start time === $startPos === end time === $endPos"); var tempDir = await getTemporaryDirectory(); final path = '${tempDir.path}/result.mp4'; - await VideoEditor.onTrimVideo(_video!.path, path, startPos, endPos); + print("outputpath === $path"); + // await VideoEditor.onTrimVideo(_video!.path, path, startPos, endPos); + print("outputpath after === $path"); + await VideoEditor.speed(_video!.path, path, 3); Navigator.push( context, MaterialPageRoute(builder: (_) => VideoScreen(path))); - } on Exception catch (e) { + } on PlatformException catch (e) { print(e); } } @@ -69,8 +74,7 @@ class _VideoTrimScreenState extends State { onPressed: this._onVideoSelectPressed, icon: Icon(Icons.ondemand_video)), IconButton( - onPressed: this.onTrimVideoPressed, - icon: Icon(Icons.done)) + onPressed: this.onTrimVideoPressed, icon: Icon(Icons.done)) ], ), body: Center( @@ -81,21 +85,25 @@ class _VideoTrimScreenState extends State { aspectRatio: _controller!.value.aspectRatio, child: VideoPlayer(_controller!), ), - TrimEditor( - viewerWidth: MediaQuery.of(context).size.width, - viewerHeight: 50, - videoFile: _video!.path, - videoPlayerController: _controller!, - fit: BoxFit.cover, - onChangeEnd: (position) { - this.endPos = position; - print("onchange end ==== $position"); - }, - onChangeStart: (position) { - this.startPos = position; - print("onchange start ==== $position"); - }, - onChangePlaybackState: (state) {}, + Positioned( + left: 20, + right: 20, + child: TrimEditor( + viewerWidth: MediaQuery.of(context).size.width - 40, + viewerHeight: 50, + videoFile: _video!.path, + videoPlayerController: _controller!, + fit: BoxFit.cover, + onChangeEnd: (position) { + this.endPos = position; + print("onchange end ==== $position"); + }, + onChangeStart: (position) { + this.startPos = position; + print("onchange start ==== $position"); + }, + onChangePlaybackState: (state) {}, + ), ) ], ) diff --git a/ios/Classes/SpeedChanger.swift b/ios/Classes/SpeedChanger.swift new file mode 100644 index 0000000..82c3f22 --- /dev/null +++ b/ios/Classes/SpeedChanger.swift @@ -0,0 +1,167 @@ +// +// VideoTrimmer.swift +// tapioca +// +// Created by Umesh Basnet on 09/06/2021. +// + +import Foundation +import AVFoundation + +import AVFoundation +import Foundation +import Flutter + +import UIKit +import AVFoundation + +enum SpeedoMode { + case Slower + case Faster +} + +class VSVideoSpeeder: NSObject { + + var sourceURL: URL + var outputURL: URL + var result: FlutterResult + + + init(sourceFile: String, outputFile: String, result: @escaping FlutterResult) { + self.sourceURL = URL.init(fileURLWithPath: sourceFile) + self.outputURL = URL.init(fileURLWithPath: outputFile) + self.result = result + } + + func verifyPresetForAsset(preset: String, asset: AVAsset) -> Bool { + let compatiblePresets = AVAssetExportSession.exportPresets(compatibleWith: asset) + let filteredPresets = compatiblePresets.filter { $0 == preset } + return filteredPresets.count > 0 || preset == AVAssetExportPresetPassthrough + } + + func removeFileAtURLIfExists(url: URL) { + + let fileManager = FileManager.default + + guard fileManager.fileExists(atPath: url.path) else { return } + + do { + try fileManager.removeItem(at: url) + } catch let error { + print("TrimVideo - Couldn't remove existing destination file: \(String(describing: error))") + } + } + + + + /// Range is b/w 1x, 2x and 3x. Will not happen anything if scale is out of range. Exporter will be nil in case url is invalid or unable to make asset instance. + func scaleAsset( by scale: Int64, withMode mode: SpeedoMode) { + + + guard sourceURL.isFileURL else { return } + guard outputURL.isFileURL else { return } + + let manager = FileManager.default + + + let asset = AVAsset(url: self.sourceURL) + let length = Float(asset.duration.value) / Float(asset.duration.timescale) + print("video length: \(length) seconds") + + + + do { + try manager.createDirectory(at: outputURL, withIntermediateDirectories: true, attributes: nil) + + }catch let error { + print(error) + } + + //Remove existing file + _ = try? manager.removeItem(at: outputURL) + + + + /// Check the valid scale + if scale < 1 || scale > 20 { + /// Can not proceed, Invalid range + self.result(nil) + return + } + + /// Asset + + /// Video Tracks + let videoTracks = asset.tracks(withMediaType: AVMediaType.video) + if videoTracks.count == 0 { + /// Can not find any video track + self.result(nil) + return + } + + /// Get the scaled video duration + let scaledVideoDuration = (mode == .Faster) ? CMTimeMake(value: asset.duration.value / scale, timescale: asset.duration.timescale) : CMTimeMake(value: asset.duration.value * scale, timescale: asset.duration.timescale) + let timeRange = CMTimeRangeMake(start: CMTime.zero, duration: asset.duration) + + /// Video track + let videoTrack = videoTracks.first! + + let mixComposition = AVMutableComposition() + let compositionVideoTrack = mixComposition.addMutableTrack(withMediaType: AVMediaType.video, preferredTrackID: kCMPersistentTrackID_Invalid) + + /// Audio Tracks + let audioTracks = asset.tracks(withMediaType: AVMediaType.audio) + if audioTracks.count > 0 { + /// Use audio if video contains the audio track + let compositionAudioTrack = mixComposition.addMutableTrack(withMediaType: AVMediaType.audio, preferredTrackID: kCMPersistentTrackID_Invalid) + + /// Audio track + let audioTrack = audioTracks.first! + do { + try compositionAudioTrack?.insertTimeRange(timeRange, of: audioTrack, at: CMTime.zero) + compositionAudioTrack?.scaleTimeRange(timeRange, toDuration: scaledVideoDuration) + } catch _ { + /// Ignore audio error + } + } + + do { + try compositionVideoTrack?.insertTimeRange(timeRange, of: videoTrack, at: CMTime.zero) + compositionVideoTrack?.scaleTimeRange(timeRange, toDuration: scaledVideoDuration) + + /// Keep original transformation + compositionVideoTrack?.preferredTransform = videoTrack.preferredTransform + + + + + + guard let exportSession = AVAssetExportSession(asset: mixComposition, presetName: AVAssetExportPresetHighestQuality) else {return} + exportSession.outputURL = outputURL + exportSession.outputFileType = .mp4 + exportSession.exportAsynchronously{ + switch exportSession.status { + case .completed: + print("exported at \(self.outputURL)") + self.result(nil) + case .failed: + print("failed \(String(describing: exportSession.error))") + self.result(FlutterError(code: "video_trim_failed", message: exportSession.error?.localizedDescription, details: exportSession.error)) + + case .cancelled: + print("cancelled \(String(describing: exportSession.error))") + self.result(FlutterError(code: "video_trim_cancelled", message: exportSession.error?.localizedDescription, details: exportSession.error)) + default: break + } + } + + + } catch let error { + print(error.localizedDescription) + self.result(FlutterError(code: "video_trim_failed", message: "exportSession.error?.localizedDescriptio", details: "exportSession.error")) + + return + } + } + +} diff --git a/ios/Classes/SwiftVideoEditorPlugin.swift b/ios/Classes/SwiftVideoEditorPlugin.swift index ee0457b..7fe8c03 100644 --- a/ios/Classes/SwiftVideoEditorPlugin.swift +++ b/ios/Classes/SwiftVideoEditorPlugin.swift @@ -37,6 +37,53 @@ public class SwiftVideoEditorPlugin: NSObject, FlutterPlugin { return } video.writeVideofile(srcPath: srcName, destPath: destName, processing: processing,result: result) + case "trim_video": + guard let args = call.arguments as? [String: Any] else { + result(FlutterError(code: "arguments_not_found", + message: "the arguments is not found.", + details: nil)) + return + } + guard let srcName = args["srcFilePath"] as? String else { + result(FlutterError(code: "src_file_path_not_found", + message: "the src file path sr is not found.", + details: nil)) + return + } + guard let destName = args["destFilePath"] as? String else { + result(FlutterError(code: "dest_file_path_not_found", + message: "the dest file path is not found.", + details: nil)) + return + } + let startTime = args["startTime"] as? Double + let endTime = args["endTime"] as? Double + + VideoTrimmer(sourceFile: srcName, outputFile: destName, result: result).trimVideo(startTime: startTime ?? 0, endTime: endTime ?? -1 ) + + case "speed_change": + guard let args = call.arguments as? [String: Any] else { + result(FlutterError(code: "arguments_not_found", + message: "the arguments is not found.", + details: nil)) + return + } + guard let srcName = args["srcFilePath"] as? String else { + result(FlutterError(code: "src_file_path_not_found", + message: "the src file path sr is not found.", + details: nil)) + return + } + guard let destName = args["destFilePath"] as? String else { + result(FlutterError(code: "dest_file_path_not_found", + message: "the dest file path is not found.", + details: nil)) + return + } + let speed = args["speed"] as? Double + + + VSVideoSpeeder(sourceFile: srcName, outputFile: destName, result: result).scaleAsset(by: 5, withMode: SpeedoMode.Faster) default: result("iOS d" + UIDevice.current.systemVersion) } diff --git a/ios/Classes/VideoTrimmer.swift b/ios/Classes/VideoTrimmer.swift new file mode 100644 index 0000000..7ef48d9 --- /dev/null +++ b/ios/Classes/VideoTrimmer.swift @@ -0,0 +1,101 @@ +// +// VideoTrimmer.swift +// tapioca +// +// Created by Umesh Basnet on 09/06/2021. +// + +import Foundation +import AVFoundation + +import AVFoundation +import Foundation +import Flutter + +class VideoTrimmer { + + typealias TrimCompletion = (Error?) -> Void + typealias TrimPoints = [(CMTime, CMTime)] + + var sourceURL: URL + var outputURL: URL + var result: FlutterResult + + init(sourceFile: String, outputFile: String, result: @escaping FlutterResult) { + self.sourceURL = URL.init(fileURLWithPath: sourceFile) + self.outputURL = URL.init(fileURLWithPath: outputFile) + self.result = result + } + + func verifyPresetForAsset(preset: String, asset: AVAsset) -> Bool { + let compatiblePresets = AVAssetExportSession.exportPresets(compatibleWith: asset) + let filteredPresets = compatiblePresets.filter { $0 == preset } + return filteredPresets.count > 0 || preset == AVAssetExportPresetPassthrough + } + + func removeFileAtURLIfExists(url: URL) { + + let fileManager = FileManager.default + + guard fileManager.fileExists(atPath: url.path) else { return } + + do { + try fileManager.removeItem(at: url) + } catch let error { + print("TrimVideo - Couldn't remove existing destination file: \(String(describing: error))") + } + } + + func trimVideo(startTime:Double, endTime:Double) + { + + guard sourceURL.isFileURL else { return } + guard outputURL.isFileURL else { return } + + let manager = FileManager.default + + + let asset = AVAsset(url: self.sourceURL) + let length = Float(asset.duration.value) / Float(asset.duration.timescale) + print("video length: \(length) seconds") + + + + do { + try manager.createDirectory(at: outputURL, withIntermediateDirectories: true, attributes: nil) + + }catch let error { + print(error) + } + + //Remove existing file + _ = try? manager.removeItem(at: outputURL) + + + guard let exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) else {return} + exportSession.outputURL = outputURL + exportSession.outputFileType = .mp4 + + let startCMTime = CMTime(seconds: startTime/1000, preferredTimescale: 1000) + let endCMTime = CMTime(seconds:endTime/1000, preferredTimescale: 1000) + let timeRange = CMTimeRange(start: startCMTime, end: endCMTime) + + exportSession.timeRange = timeRange + exportSession.exportAsynchronously{ + switch exportSession.status { + case .completed: + print("exported at \(self.outputURL)") + self.result(nil) + case .failed: + print("failed \(String(describing: exportSession.error))") + self.result(FlutterError(code: "video_trim_failed", message: exportSession.error?.localizedDescription, details: exportSession.error)) + + case .cancelled: + print("cancelled \(String(describing: exportSession.error))") + self.result(FlutterError(code: "video_trim_cancelled", message: exportSession.error?.localizedDescription, details: exportSession.error)) + default: break + } + } + + } +} diff --git a/lib/src/video_editor.dart b/lib/src/video_editor.dart index d14e257..14a3311 100644 --- a/lib/src/video_editor.dart +++ b/lib/src/video_editor.dart @@ -10,9 +10,7 @@ class VideoEditor { return version; } - static Future writeVideofile( - String srcFilePath, - String destFilePath, + static Future writeVideofile(String srcFilePath, String destFilePath, Map> processing, {int? startTime, int? endTime}) async { await _channel.invokeMethod('writeVideofile', { @@ -24,8 +22,8 @@ class VideoEditor { }); } - static Future onTrimVideo(String srcFilePath, - String destFilePath, double startTime, double endTime) async{ + static Future onTrimVideo(String srcFilePath, String destFilePath, + double startTime, double endTime) async { await _channel.invokeMethod('trim_video', { 'srcFilePath': srcFilePath, 'destFilePath': destFilePath, @@ -33,4 +31,16 @@ class VideoEditor { 'endTime': endTime.toInt() }); } + + static Future speed( + String srcFilePath, + String destFilePath, + double speed, + ) async { + await _channel.invokeMethod('speed_change', { + 'srcFilePath': srcFilePath, + 'destFilePath': destFilePath, + 'speed': speed, + }); + } } diff --git a/lib/src/video_trimmer/thumbnail_viewer.dart b/lib/src/video_trimmer/thumbnail_viewer.dart index 3946a60..3978c11 100644 --- a/lib/src/video_trimmer/thumbnail_viewer.dart +++ b/lib/src/video_trimmer/thumbnail_viewer.dart @@ -38,8 +38,7 @@ class ThumbnailViewer extends StatelessWidget { timeMs: (_eachPart * i).toInt(), quality: quality, ); - - _byteList.add(_bytes); + if (_bytes != null) _byteList.add(_bytes); yield _byteList; }