From d028524a3556926d57cae712231bd6c9ef7e70d0 Mon Sep 17 00:00:00 2001 From: Marvin Willms Date: Wed, 6 Nov 2024 10:54:11 +0100 Subject: [PATCH] feat(ios, android): control quality by `bitrate`/`fileLengthLimit` --- .../video_compress/VideoCompressPlugin.kt | 48 ++++++++++----- ios/Classes/SwiftVideoCompressPlugin.swift | 8 ++- lib/src/video_compress/video_compressor.dart | 58 ++++++++++++++++++- 3 files changed, 96 insertions(+), 18 deletions(-) diff --git a/android/src/main/kotlin/com/example/video_compress/VideoCompressPlugin.kt b/android/src/main/kotlin/com/example/video_compress/VideoCompressPlugin.kt index 032e0bf5..d06aad16 100644 --- a/android/src/main/kotlin/com/example/video_compress/VideoCompressPlugin.kt +++ b/android/src/main/kotlin/com/example/video_compress/VideoCompressPlugin.kt @@ -83,6 +83,7 @@ class VideoCompressPlugin : MethodCallHandler, FlutterPlugin { val duration = call.argument("duration") val includeAudio = call.argument("includeAudio") ?: true val frameRate = if (call.argument("frameRate")==null) 30 else call.argument("frameRate") + val bitrate = if (call.argument("bitrate")==null) DefaultVideoStrategy.BITRATE_UNKNOWN else call.argument("bitrate") val tempDir: String = context.getExternalFilesDir("video_compress")!!.absolutePath val out = SimpleDateFormat("yyyy-MM-dd hh-mm-ss").format(Date()) @@ -94,36 +95,55 @@ class VideoCompressPlugin : MethodCallHandler, FlutterPlugin { when (quality) { 0 -> { - videoTrackStrategy = DefaultVideoStrategy.atMost(720).build() + videoTrackStrategy = DefaultVideoStrategy + .atMost(720) + .bitRate(bitrate!!.toLong()) + .frameRate(frameRate!!).build() } 1 -> { - videoTrackStrategy = DefaultVideoStrategy.atMost(360).build() + videoTrackStrategy = DefaultVideoStrategy + .atMost(360) + .bitRate(bitrate!!.toLong()) + .frameRate(frameRate!!).build() } 2 -> { - videoTrackStrategy = DefaultVideoStrategy.atMost(640).build() + videoTrackStrategy = DefaultVideoStrategy + .atMost(640) + .bitRate(bitrate!!.toLong()) + .frameRate(frameRate!!).build() } 3 -> { - assert(value = frameRate != null) - videoTrackStrategy = DefaultVideoStrategy.Builder() - .keyFrameInterval(3f) - .bitRate(1280 * 720 * 4.toLong()) - .frameRate(frameRate!!) // will be capped to the input frameRate - .build() + videoTrackStrategy = DefaultVideoStrategy + .atMost(720) + .bitRate(bitrate!!.toLong()) + .frameRate(frameRate!!).build() } 4 -> { - videoTrackStrategy = DefaultVideoStrategy.atMost(480, 640).build() + videoTrackStrategy = DefaultVideoStrategy + .atMost(480, 640) + .bitRate(bitrate!!.toLong()) + .frameRate(frameRate!!).build() } 5 -> { - videoTrackStrategy = DefaultVideoStrategy.atMost(540, 960).build() + videoTrackStrategy = DefaultVideoStrategy + .atMost(540, 960) + .bitRate(bitrate!!.toLong()) + .frameRate(frameRate!!).build() } 6 -> { - videoTrackStrategy = DefaultVideoStrategy.atMost(720, 1280).build() + videoTrackStrategy = DefaultVideoStrategy + .atMost(720, 1280) + .bitRate(bitrate!!.toLong()) + .frameRate(frameRate!!).build() } 7 -> { - videoTrackStrategy = DefaultVideoStrategy.atMost(1080, 1920).build() - } + videoTrackStrategy = DefaultVideoStrategy + .atMost(1080, 1920) + .bitRate(bitrate!!.toLong()) + .frameRate(frameRate!!).build() + } } audioTrackStrategy = if (includeAudio) { diff --git a/ios/Classes/SwiftVideoCompressPlugin.swift b/ios/Classes/SwiftVideoCompressPlugin.swift index ef899d6b..df8f566a 100644 --- a/ios/Classes/SwiftVideoCompressPlugin.swift +++ b/ios/Classes/SwiftVideoCompressPlugin.swift @@ -42,8 +42,9 @@ public class SwiftVideoCompressPlugin: NSObject, FlutterPlugin { let duration = args!["duration"] as? Double let includeAudio = args!["includeAudio"] as? Bool let frameRate = args!["frameRate"] as? Int + let fileLengthLimit = args!["fileLengthLimit"] as? Int compressVideo(path, quality, deleteOrigin, startTime, duration, includeAudio, - frameRate, result) + frameRate, fileLengthLimit, result) case "cancelCompression": cancelCompression(result) case "deleteAllCache": @@ -178,7 +179,7 @@ public class SwiftVideoCompressPlugin: NSObject, FlutterPlugin { } private func compressVideo(_ path: String,_ quality: NSNumber,_ deleteOrigin: Bool,_ startTime: Double?, - _ duration: Double?,_ includeAudio: Bool?,_ frameRate: Int?, + _ duration: Double?,_ includeAudio: Bool?,_ frameRate: Int?,_ fileLengthLimit: Int?, _ result: @escaping FlutterResult) { let sourceVideoUrl = Utility.getPathUrl(path) let sourceVideoType = "mp4" @@ -214,6 +215,9 @@ public class SwiftVideoCompressPlugin: NSObject, FlutterPlugin { exporter.outputURL = compressionUrl exporter.outputFileType = AVFileType.mp4 exporter.shouldOptimizeForNetworkUse = true + if let fileLengthLimit = fileLengthLimit { + exporter.fileLengthLimit = Int64(fileLengthLimit) + } if let frameRate = frameRate, Float(frameRate) <= avController.getNominalFrameRate(sourceVideoTrack) { let videoComposition = AVMutableVideoComposition(propertiesOf: sourceVideoAsset) diff --git a/lib/src/video_compress/video_compressor.dart b/lib/src/video_compress/video_compressor.dart index fb794c0c..f1c549ee 100644 --- a/lib/src/video_compress/video_compressor.dart +++ b/lib/src/video_compress/video_compressor.dart @@ -112,6 +112,11 @@ extension Compress on IVideoCompress { /// determine whether to delete his source file by [deleteOrigin] /// optional parameters [startTime] [duration] [includeAudio] [frameRate] /// + /// platform specific options [options] + /// See: + /// [IOSOptions] + /// [AndroidOptions] + /// /// ## example /// ```dart /// final info = await _flutterVideoCompress.compressVideo( @@ -128,6 +133,7 @@ extension Compress on IVideoCompress { int? duration, bool? includeAudio, int? frameRate = 30, + List? options, }) async { if (isCompressing) { throw StateError('''VideoCompress Error: @@ -156,7 +162,8 @@ extension Compress on IVideoCompress { // ignore: invalid_use_of_protected_member setProcessingStatus(true); - final jsonStr = await _invoke('compressVideo', { + + final arguments = { 'path': path, 'quality': quality.index, 'deleteOrigin': deleteOrigin, @@ -164,7 +171,15 @@ extension Compress on IVideoCompress { 'duration': duration, 'includeAudio': includeAudio, 'frameRate': frameRate, - }); + }; + + if (options != null) { + for (final option in options) { + arguments.addAll(option.toMap()); + } + } + + final jsonStr = await _invoke('compressVideo', arguments); // ignore: invalid_use_of_protected_member setProcessingStatus(false); @@ -195,3 +210,42 @@ extension Compress on IVideoCompress { }); } } + +abstract class PlatformOptions { + Map toMap(); +} + +class IOSOptions extends PlatformOptions { + /// The file length that the output of the session must not exceed. + /// https://developer.apple.com/documentation/avfoundation/avassetexportsession/1622333-filelengthlimit + final int? fileLengthLimit; + + IOSOptions({ + this.fileLengthLimit, + }); + + @override + Map toMap() { + return { + 'fileLengthLimit': fileLengthLimit, + }; + } +} + +class AndroidOptions extends PlatformOptions { + /// Desired bit rate (bits per second). Can optionally be + /// null, in which case the strategy will try to estimate the bitrate. + /// https://opensource.deepmedia.io/transcoder/track-strategies#other-options + final int? bitrate; + + AndroidOptions({ + this.bitrate, + }); + + @override + Map toMap() { + return { + 'bitrate': bitrate, + }; + } +}