From 7beb46aa36131616e82512257eeb4c04944ba490 Mon Sep 17 00:00:00 2001 From: Pawit Pornkitprasan Date: Mon, 28 Sep 2020 11:15:25 +0700 Subject: [PATCH 01/44] Upgrade to Flutter Android Plugin API V2 Required for this plugin to work in embedded Flutter app See https://flutter.dev/docs/development/packages-and-plugins/plugin-api-migration --- .../video_compress/VideoCompressPlugin.kt | 44 +++++++++++++++---- .../android/app/src/main/AndroidManifest.xml | 6 ++- .../MainActivity.kt | 13 ------ 3 files changed, 41 insertions(+), 22 deletions(-) delete mode 100644 example/android/app/src/main/kotlin/com/example/flutter_video_compress_example/MainActivity.kt 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 b26fd5ce..cb5627f1 100644 --- a/android/src/main/kotlin/com/example/video_compress/VideoCompressPlugin.kt +++ b/android/src/main/kotlin/com/example/video_compress/VideoCompressPlugin.kt @@ -1,15 +1,17 @@ package com.example.video_compress -import android.app.Activity import android.content.Context import android.content.Intent import android.net.Uri +import android.util.Log import com.otaliastudios.transcoder.Transcoder import com.otaliastudios.transcoder.TranscoderListener import com.otaliastudios.transcoder.strategy.DefaultVideoStrategies import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy import com.otaliastudios.transcoder.strategy.TrackStrategy import com.otaliastudios.transcoder.strategy.size.* +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler @@ -22,10 +24,20 @@ import java.util.* /** * VideoCompressPlugin */ -class VideoCompressPlugin private constructor(private val activity: Activity, private val context: Context, private val channel: MethodChannel) : MethodCallHandler { +class VideoCompressPlugin : MethodCallHandler, FlutterPlugin { - var channelName = "video_compress" + private var _context: Context? = null + private var _channel: MethodChannel? = null + + val channelName = "video_compress" override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + val context = _context; + val channel = _channel; + + if (context == null || channel == null) { + Log.w(TAG, "Calling VideoCompress plugin before initialization") + return + } when (call.method) { "getByteThumbnail" -> { @@ -61,7 +73,7 @@ class VideoCompressPlugin private constructor(private val activity: Activity, pr val includeAudio = call.argument("includeAudio") val frameRate = if (call.argument("frameRate")==null) 30 else call.argument("frameRate") - val tempDir: String = this.context.getExternalFilesDir("video_compress")!!.absolutePath + val tempDir: String = context.getExternalFilesDir("video_compress")!!.absolutePath val out = SimpleDateFormat("yyyy-MM-dd hh-mm-ss").format(Date()) val destPath: String = tempDir + File.separator + "VID_" + out + ".mp4" @@ -123,14 +135,30 @@ class VideoCompressPlugin private constructor(private val activity: Activity, pr } } + override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { + init(binding.applicationContext, binding.binaryMessenger) + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + _channel?.setMethodCallHandler(null) + _context = null + _channel = null + } + + private fun init(context: Context, messenger: BinaryMessenger) { + val channel = MethodChannel(messenger, channelName) + channel.setMethodCallHandler(this) + _context = context + _channel = channel + } + companion object { - const val ACTIVITY_2_REQUEST = 999 + private const val TAG = "video_compress" @JvmStatic fun registerWith(registrar: Registrar) { - val channel = MethodChannel(registrar.messenger(), "video_compress") - val instance = VideoCompressPlugin(registrar.activity(), registrar.context(), channel) - channel.setMethodCallHandler(instance) + val instance = VideoCompressPlugin() + instance.init(registrar.context(), registrar.messenger()) } } diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index ecb88cf1..a75b29dd 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -12,7 +12,7 @@ android:label="video_compress_example" android:icon="@mipmap/ic_launcher"> + + diff --git a/example/android/app/src/main/kotlin/com/example/flutter_video_compress_example/MainActivity.kt b/example/android/app/src/main/kotlin/com/example/flutter_video_compress_example/MainActivity.kt deleted file mode 100644 index 5d961b19..00000000 --- a/example/android/app/src/main/kotlin/com/example/flutter_video_compress_example/MainActivity.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.video_compress_example - -import android.os.Bundle - -import io.flutter.app.FlutterActivity -import io.flutter.plugins.GeneratedPluginRegistrant - -class MainActivity: FlutterActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - GeneratedPluginRegistrant.registerWith(this) - } -} From 9dc46654ec62aa5529c0b04fa90fb5cb46c837b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20P=C5=82onka?= Date: Tue, 29 Sep 2020 16:40:20 +0200 Subject: [PATCH 02/44] Handling includeAudio on Android This PR is connected with the issue I have created: https://github.com/jonataslaw/VideoCompress/issues/45 I've noticed that includeAudio was not used at the Android codebase, therefore there was no option to mute the sound of the videos from Flutter side. --- .../video_compress/VideoCompressPlugin.kt | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 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 b26fd5ce..9ae4d4e7 100644 --- a/android/src/main/kotlin/com/example/video_compress/VideoCompressPlugin.kt +++ b/android/src/main/kotlin/com/example/video_compress/VideoCompressPlugin.kt @@ -2,14 +2,13 @@ package com.example.video_compress import android.app.Activity import android.content.Context -import android.content.Intent import android.net.Uri import com.otaliastudios.transcoder.Transcoder import com.otaliastudios.transcoder.TranscoderListener -import com.otaliastudios.transcoder.strategy.DefaultVideoStrategies +import com.otaliastudios.transcoder.strategy.DefaultAudioStrategy import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy +import com.otaliastudios.transcoder.strategy.RemoveTrackStrategy import com.otaliastudios.transcoder.strategy.TrackStrategy -import com.otaliastudios.transcoder.strategy.size.* import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler @@ -58,31 +57,32 @@ class VideoCompressPlugin private constructor(private val activity: Activity, pr val deleteOrigin = call.argument("deleteOrigin")!! val startTime = call.argument("startTime") val duration = call.argument("duration") - val includeAudio = call.argument("includeAudio") + val includeAudio = call.argument("includeAudio")!! val frameRate = if (call.argument("frameRate")==null) 30 else call.argument("frameRate") val tempDir: String = this.context.getExternalFilesDir("video_compress")!!.absolutePath val out = SimpleDateFormat("yyyy-MM-dd hh-mm-ss").format(Date()) val destPath: String = tempDir + File.separator + "VID_" + out + ".mp4" - var strategy: TrackStrategy = DefaultVideoStrategy.atMost(340).build(); + var videoTrackStrategy: TrackStrategy = DefaultVideoStrategy.atMost(340).build(); + val audioTrackStrategy: TrackStrategy when (quality) { 0 -> { - strategy = DefaultVideoStrategy.atMost(720).build() + videoTrackStrategy = DefaultVideoStrategy.atMost(720).build() } 1 -> { - strategy = DefaultVideoStrategy.atMost(360).build() + videoTrackStrategy = DefaultVideoStrategy.atMost(360).build() } 2 -> { - strategy = DefaultVideoStrategy.atMost(640).build() + videoTrackStrategy = DefaultVideoStrategy.atMost(640).build() } 3 -> { assert(value = frameRate != null) - strategy = DefaultVideoStrategy.Builder() + videoTrackStrategy = DefaultVideoStrategy.Builder() .keyFrameInterval(3f) .bitRate(1280 * 720 * 4.toLong()) .frameRate(frameRate!!) // will be capped to the input frameRate @@ -90,10 +90,23 @@ class VideoCompressPlugin private constructor(private val activity: Activity, pr } } + audioTrackStrategy = if (includeAudio) { + val sampleRate = DefaultAudioStrategy.SAMPLE_RATE_AS_INPUT + val channels = DefaultAudioStrategy.CHANNELS_AS_INPUT + + DefaultAudioStrategy.builder() + .channels(channels) + .sampleRate(sampleRate) + .build() + } else { + RemoveTrackStrategy() + } + Transcoder.into(destPath!!) .addDataSource(context, Uri.parse(path)) - .setVideoTrackStrategy(strategy) + .setAudioTrackStrategy(audioTrackStrategy) + .setVideoTrackStrategy(videoTrackStrategy) .setListener(object : TranscoderListener { override fun onTranscodeProgress(progress: Double) { channel.invokeMethod("updateProgress", progress * 100.00) From 6f6864cf844ca13ea2d0c97ed6cbabdc99f03ffa Mon Sep 17 00:00:00 2001 From: Yaniv Shaked Date: Wed, 14 Oct 2020 10:51:10 +0300 Subject: [PATCH 03/44] [Android, LogLevel] Support for control over log level printings in Android platform --- .../com/example/video_compress/VideoCompressPlugin.kt | 8 ++++++++ example/lib/main.dart | 1 + ios/Classes/SwiftVideoCompressPlugin.swift | 2 ++ lib/src/video_compressor.dart | 6 ++++++ 4 files changed, 17 insertions(+) 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 b26fd5ce..ab9e44ba 100644 --- a/android/src/main/kotlin/com/example/video_compress/VideoCompressPlugin.kt +++ b/android/src/main/kotlin/com/example/video_compress/VideoCompressPlugin.kt @@ -10,6 +10,7 @@ import com.otaliastudios.transcoder.strategy.DefaultVideoStrategies import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy import com.otaliastudios.transcoder.strategy.TrackStrategy import com.otaliastudios.transcoder.strategy.size.* +import com.otaliastudios.transcoder.internal.Logger import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler @@ -24,6 +25,8 @@ import java.util.* */ class VideoCompressPlugin private constructor(private val activity: Activity, private val context: Context, private val channel: MethodChannel) : MethodCallHandler { + private val TAG = "VideoCompressPlugin" + private val LOG = Logger(TAG) var channelName = "video_compress" override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { @@ -48,6 +51,11 @@ class VideoCompressPlugin private constructor(private val activity: Activity, pr "deleteAllCache" -> { result.success(Utility(channelName).deleteAllCache(context, result)); } + "setLogLevel" -> { + val logLevel = call.argument("logLevel")!! + Logger.setLogLevel(logLevel) + result.success(true); + } "cancelCompression" -> { result.success(false); //TODO: Made Transcoder.into Global to call Transcoder.cancel(true); here diff --git a/example/lib/main.dart b/example/lib/main.dart index c1d3e2c0..e2e586b8 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -98,6 +98,7 @@ class _MyHomePageState extends State { floatingActionButton: FloatingActionButton( onPressed: () async { File file = await ImagePicker.pickVideo(source: ImageSource.gallery); + await VideoCompress.setLogLevel(3); final info = await VideoCompress.compressVideo( file.path, quality: VideoQuality.MediumQuality, diff --git a/ios/Classes/SwiftVideoCompressPlugin.swift b/ios/Classes/SwiftVideoCompressPlugin.swift index 11f2c28c..ad95ed5c 100644 --- a/ios/Classes/SwiftVideoCompressPlugin.swift +++ b/ios/Classes/SwiftVideoCompressPlugin.swift @@ -49,6 +49,8 @@ public class SwiftVideoCompressPlugin: NSObject, FlutterPlugin { case "deleteAllCache": Utility.deleteFile(Utility.basePath(), clear: true) result(true) + case "setLogLevel": + result(true) default: result(FlutterMethodNotImplemented) } diff --git a/lib/src/video_compressor.dart b/lib/src/video_compressor.dart index c23a40f7..19a2e370 100644 --- a/lib/src/video_compressor.dart +++ b/lib/src/video_compressor.dart @@ -167,6 +167,12 @@ class VideoCompress { static Future deleteAllCache() async { return await _invoke('deleteAllCache'); } + + static Future setLogLevel(int logLevel) async { + return await _invoke('setLogLevel', { + 'logLevel': logLevel, + }); + } } class ObservableBuilder { From f8bb8db3c4195810cc22965a81796babce7f1a29 Mon Sep 17 00:00:00 2001 From: Yaniv Shaked Date: Wed, 14 Oct 2020 13:34:23 +0300 Subject: [PATCH 04/44] [Android, cancelCompression] Support for cancelling compression process on Android --- .../video_compress/VideoCompressPlugin.kt | 7 ++++--- example/lib/main.dart | 16 +++++++++++++--- lib/src/video_compressor.dart | 8 ++++++-- 3 files changed, 23 insertions(+), 8 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 3f0ecffe..17fe5ecd 100644 --- a/android/src/main/kotlin/com/example/video_compress/VideoCompressPlugin.kt +++ b/android/src/main/kotlin/com/example/video_compress/VideoCompressPlugin.kt @@ -17,7 +17,7 @@ import io.flutter.plugin.common.PluginRegistry.Registrar import java.io.File import java.text.SimpleDateFormat import java.util.* - +import java.util.concurrent.Future /** * VideoCompressPlugin @@ -26,6 +26,7 @@ class VideoCompressPlugin private constructor(private val activity: Activity, pr private val TAG = "VideoCompressPlugin" private val LOG = Logger(TAG) + private var transcodeFuture:Future? = null var channelName = "video_compress" override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { @@ -56,8 +57,8 @@ class VideoCompressPlugin private constructor(private val activity: Activity, pr result.success(true); } "cancelCompression" -> { + transcodeFuture?.cancel(true) result.success(false); - //TODO: Made Transcoder.into Global to call Transcoder.cancel(true); here } "compressVideo" -> { val path = call.argument("path")!! @@ -111,7 +112,7 @@ class VideoCompressPlugin private constructor(private val activity: Activity, pr } - Transcoder.into(destPath!!) + transcodeFuture = Transcoder.into(destPath!!) .addDataSource(context, Uri.parse(path)) .setAudioTrackStrategy(audioTrackStrategy) .setVideoTrackStrategy(videoTrackStrategy) diff --git a/example/lib/main.dart b/example/lib/main.dart index e2e586b8..fd87dd7d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -92,20 +92,30 @@ class _MyHomePageState extends State { '$_counter', style: Theme.of(context).textTheme.headline4, ), + InkWell( + child: Icon( + Icons.cancel, + size: 55, + ), + onTap: () { + VideoCompress.cancelCompression(); + }) ], ), ), floatingActionButton: FloatingActionButton( onPressed: () async { File file = await ImagePicker.pickVideo(source: ImageSource.gallery); - await VideoCompress.setLogLevel(3); + await VideoCompress.setLogLevel(0); final info = await VideoCompress.compressVideo( file.path, quality: VideoQuality.MediumQuality, deleteOrigin: false, + includeAudio: true, ); - - _counter = info.path; + if (info != null) { + _counter = info.path; + } setState(() {}); }, tooltip: 'Increment', diff --git a/lib/src/video_compressor.dart b/lib/src/video_compressor.dart index 19a2e370..5b45fc1b 100644 --- a/lib/src/video_compressor.dart +++ b/lib/src/video_compressor.dart @@ -152,8 +152,12 @@ class VideoCompress { 'frameRate': frameRate, }); _isCompressing = false; - final jsonMap = json.decode(jsonStr); - return MediaInfo.fromJson(jsonMap); + if (jsonStr != null) { + final jsonMap = json.decode(jsonStr); + return MediaInfo.fromJson(jsonMap); + } else { + return null; + } } /// stop compressing the file that is currently being compressed. From 54074aa41c66d0dd2b47eb9089b9abcdca9dedc7 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2020 22:35:57 +0000 Subject: [PATCH 05/44] docs: update README.md [skip ci] --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0a178bbb..37410b0d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # video_compress -[![All Contributors](https://img.shields.io/badge/all_contributors-3-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-4-orange.svg?style=flat-square)](#contributors-) Compress videos, remove audio, manipulate thumbnails, and make your video compatible with all platforms through this lightweight and efficient library. @@ -127,6 +127,7 @@ Thanks goes to these wonderful people:
khainhero

🚧
Andres Gutierrez

🚧
Jonny Borges

💻 +
Tibout Shaik

🚧 From d5a1ecf884b7c511f493ee5b8c83ab8999ba9312 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2020 22:35:58 +0000 Subject: [PATCH 06/44] docs: update .all-contributorsrc [skip ci] --- .all-contributorsrc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index ec97979e..eb1be37b 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -31,6 +31,15 @@ "contributions": [ "code" ] + }, + { + "login": "Martibis", + "name": "Tibout Shaik", + "avatar_url": "https://avatars0.githubusercontent.com/u/13279066?v=4", + "profile": "http://www.tiboutshaik.com", + "contributions": [ + "maintenance" + ] } ], "contributorsPerLine": 7, From bb1c0661580ffbf918602c6aa6c5d909db6566ba Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2020 22:36:58 +0000 Subject: [PATCH 07/44] docs: update README.md [skip ci] --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 37410b0d..3bb8bd7c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # video_compress -[![All Contributors](https://img.shields.io/badge/all_contributors-4-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-5-orange.svg?style=flat-square)](#contributors-) Compress videos, remove audio, manipulate thumbnails, and make your video compatible with all platforms through this lightweight and efficient library. @@ -128,6 +128,7 @@ Thanks goes to these wonderful people:
Andres Gutierrez

🚧
Jonny Borges

💻
Tibout Shaik

🚧 +
Marco A. Braghim

🚧 From ed815ff6575b0eadf4d748790474f65cf9e544eb Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2020 22:36:59 +0000 Subject: [PATCH 08/44] docs: update .all-contributorsrc [skip ci] --- .all-contributorsrc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index eb1be37b..6c400764 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -40,6 +40,15 @@ "contributions": [ "maintenance" ] + }, + { + "login": "marcobraghim", + "name": "Marco A. Braghim", + "avatar_url": "https://avatars3.githubusercontent.com/u/2068045?v=4", + "profile": "https://www.thizer.com", + "contributions": [ + "maintenance" + ] } ], "contributorsPerLine": 7, From 1d0e107f533e5ddae53293b48f47148c99cfaa98 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2020 22:37:37 +0000 Subject: [PATCH 09/44] docs: update README.md [skip ci] --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3bb8bd7c..9e48a058 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # video_compress -[![All Contributors](https://img.shields.io/badge/all_contributors-5-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-6-orange.svg?style=flat-square)](#contributors-) Compress videos, remove audio, manipulate thumbnails, and make your video compatible with all platforms through this lightweight and efficient library. @@ -129,6 +129,7 @@ Thanks goes to these wonderful people:
Jonny Borges

💻
Tibout Shaik

🚧
Marco A. Braghim

🚧 +
ppornkitpras-agoda

🚧 From 0c0a04f3b8d0aa55827f4455bc20456c5d70679d Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2020 22:37:38 +0000 Subject: [PATCH 10/44] docs: update .all-contributorsrc [skip ci] --- .all-contributorsrc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 6c400764..b79e491e 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -49,6 +49,15 @@ "contributions": [ "maintenance" ] + }, + { + "login": "ppornkitpras-agoda", + "name": "ppornkitpras-agoda", + "avatar_url": "https://avatars3.githubusercontent.com/u/54128106?v=4", + "profile": "https://github.com/ppornkitpras-agoda", + "contributions": [ + "maintenance" + ] } ], "contributorsPerLine": 7, From 7e5f6bf367f74a62a21985154f3f2379e052b965 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2020 22:38:23 +0000 Subject: [PATCH 11/44] docs: update README.md [skip ci] --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9e48a058..7cd81117 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # video_compress -[![All Contributors](https://img.shields.io/badge/all_contributors-6-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-7-orange.svg?style=flat-square)](#contributors-) Compress videos, remove audio, manipulate thumbnails, and make your video compatible with all platforms through this lightweight and efficient library. @@ -130,6 +130,7 @@ Thanks goes to these wonderful people:
Tibout Shaik

🚧
Marco A. Braghim

🚧
ppornkitpras-agoda

🚧 +
Rafał Płonka

🚧 From c8eb226477ef9c0b702f10a870dd0e68c19f1c1f Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2020 22:38:24 +0000 Subject: [PATCH 12/44] docs: update .all-contributorsrc [skip ci] --- .all-contributorsrc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index b79e491e..665b887a 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -58,6 +58,15 @@ "contributions": [ "maintenance" ] + }, + { + "login": "snowb1shop", + "name": "Rafał Płonka", + "avatar_url": "https://avatars3.githubusercontent.com/u/18536122?v=4", + "profile": "https://github.com/snowb1shop", + "contributions": [ + "maintenance" + ] } ], "contributorsPerLine": 7, From 13c190bcee7a6e53a6b8d036cab324ea88e2ba23 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2020 22:39:15 +0000 Subject: [PATCH 13/44] docs: update README.md [skip ci] --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7cd81117..ad3f4340 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # video_compress -[![All Contributors](https://img.shields.io/badge/all_contributors-7-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-8-orange.svg?style=flat-square)](#contributors-) Compress videos, remove audio, manipulate thumbnails, and make your video compatible with all platforms through this lightweight and efficient library. @@ -132,6 +132,9 @@ Thanks goes to these wonderful people:
ppornkitpras-agoda

🚧
Rafał Płonka

🚧 + +
Yaniv Shaked

🚧 + From 087deed2af625ada6f1dcb59be1f7ce9210725b6 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2020 22:39:16 +0000 Subject: [PATCH 14/44] docs: update .all-contributorsrc [skip ci] --- .all-contributorsrc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 665b887a..d47ad86d 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -67,6 +67,15 @@ "contributions": [ "maintenance" ] + }, + { + "login": "yanivshaked", + "name": "Yaniv Shaked", + "avatar_url": "https://avatars3.githubusercontent.com/u/13107481?v=4", + "profile": "https://github.com/yanivshaked", + "contributions": [ + "maintenance" + ] } ], "contributorsPerLine": 7, From 64b8d972dd18b108e73fc3f267974ea9e414a08f Mon Sep 17 00:00:00 2001 From: Jonatas Date: Wed, 14 Oct 2020 19:46:30 -0300 Subject: [PATCH 15/44] update to version 2.1.0 --- CHANGELOG.md | 30 +++--- example/.flutter-plugins-dependencies | 2 +- .../ios/Flutter/flutter_export_environment.sh | 6 +- example/lib/main.dart | 9 +- lib/src/{ => media}/media_data.dart | 0 lib/src/{ => media}/media_info.dart | 0 .../compress_mixin.dart | 33 +++++++ .../progress_callback.dart/subscription.dart | 30 ++++++ .../video_compressor.dart | 99 +++++++------------ .../{ => video_compress}/video_quality.dart | 0 lib/video_compress.dart | 8 +- pubspec.yaml | 4 +- 12 files changed, 135 insertions(+), 86 deletions(-) rename lib/src/{ => media}/media_data.dart (100%) rename lib/src/{ => media}/media_info.dart (100%) create mode 100644 lib/src/progress_callback.dart/compress_mixin.dart create mode 100644 lib/src/progress_callback.dart/subscription.dart rename lib/src/{ => video_compress}/video_compressor.dart (64%) rename lib/src/{ => video_compress}/video_quality.dart (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b921e07..437cf543 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,19 +1,25 @@ -## 0.1.0 -- initial release +## 2.1.0 +- Added cancel compression to android +- Fix compress progress +- Added audio remove/include to android +- Upgrade to android v2 +- Fix subscription progress receive same listener + +## 2.0.0 +- refactor code +Breaking changes, call VideoCompress.method directly, without having to instantiate it. +## 1.0.0 +- release new version -## 0.1.1 -- Change default value to HD +## 0.1.3 +- added progress listen ## 0.1.2 - Removed unecessary intent when process is done -## 0.1.3 -- added progress listen - -## 1.0.0 -- release new version +## 0.1.1 +- Change default value to HD -## 2.0.0 -- refactor code -Breaking changes, call VideoCompress.method directly, without having to instantiate it. +## 0.1.0 +- initial release \ No newline at end of file diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies index 2de573c7..7cc6f616 100644 --- a/example/.flutter-plugins-dependencies +++ b/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"image_picker","path":"/opt/flutter/.pub-cache/hosted/pub.dartlang.org/image_picker-0.6.5/","dependencies":[]},{"name":"video_compress","path":"/home/jonny/Downloads/VideoCompress-master/","dependencies":[]},{"name":"video_player","path":"/opt/flutter/.pub-cache/hosted/pub.dartlang.org/video_player-0.10.8+1/","dependencies":[]}],"android":[{"name":"flutter_plugin_android_lifecycle","path":"/opt/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_plugin_android_lifecycle-1.0.6/","dependencies":[]},{"name":"image_picker","path":"/opt/flutter/.pub-cache/hosted/pub.dartlang.org/image_picker-0.6.5/","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"video_compress","path":"/home/jonny/Downloads/VideoCompress-master/","dependencies":[]},{"name":"video_player","path":"/opt/flutter/.pub-cache/hosted/pub.dartlang.org/video_player-0.10.8+1/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[{"name":"video_player_web","path":"/opt/flutter/.pub-cache/hosted/pub.dartlang.org/video_player_web-0.1.2+1/","dependencies":[]}]},"dependencyGraph":[{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"image_picker","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"video_compress","dependencies":[]},{"name":"video_player","dependencies":["video_player_web"]},{"name":"video_player_web","dependencies":[]}],"date_created":"2020-04-20 17:09:59.398701","version":"1.18.0-dev.5.0"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"video_compress","path":"/home/jonny/Downloads/videocompressplig/VideoCompress/","dependencies":[]},{"name":"image_picker","path":"/opt/flutter/.pub-cache/hosted/pub.dartlang.org/image_picker-0.6.7+12/","dependencies":[]},{"name":"video_player","path":"/opt/flutter/.pub-cache/hosted/pub.dartlang.org/video_player-0.10.12+5/","dependencies":[]}],"android":[{"name":"video_compress","path":"/home/jonny/Downloads/videocompressplig/VideoCompress/","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","path":"/opt/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_plugin_android_lifecycle-1.0.11/","dependencies":[]},{"name":"image_picker","path":"/opt/flutter/.pub-cache/hosted/pub.dartlang.org/image_picker-0.6.7+12/","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"video_player","path":"/opt/flutter/.pub-cache/hosted/pub.dartlang.org/video_player-0.10.12+5/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[{"name":"video_player_web","path":"/opt/flutter/.pub-cache/hosted/pub.dartlang.org/video_player_web-0.1.4/","dependencies":[]}]},"dependencyGraph":[{"name":"video_compress","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"image_picker","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"video_player","dependencies":["video_player_web"]},{"name":"video_player_web","dependencies":[]}],"date_created":"2020-10-14 19:45:59.207690","version":"1.23.0-13.0.pre"} \ No newline at end of file diff --git a/example/ios/Flutter/flutter_export_environment.sh b/example/ios/Flutter/flutter_export_environment.sh index 3e9f82f6..357328dd 100755 --- a/example/ios/Flutter/flutter_export_environment.sh +++ b/example/ios/Flutter/flutter_export_environment.sh @@ -1,7 +1,7 @@ #!/bin/sh # This is a generated file; do not edit or check into version control. export "FLUTTER_ROOT=/opt/flutter" -export "FLUTTER_APPLICATION_PATH=/home/jonny/Downloads/VideoCompress-master/example" +export "FLUTTER_APPLICATION_PATH=/home/jonny/Downloads/videocompressplig/VideoCompress/example" export "FLUTTER_TARGET=lib/main.dart" export "FLUTTER_BUILD_DIR=build" export "SYMROOT=${SOURCE_ROOT}/../build/ios" @@ -9,3 +9,7 @@ export "OTHER_LDFLAGS=$(inherited) -framework Flutter" export "FLUTTER_FRAMEWORK_DIR=/opt/flutter/bin/cache/artifacts/engine/ios" export "FLUTTER_BUILD_NAME=1.0.0" export "FLUTTER_BUILD_NUMBER=1" +export "DART_OBFUSCATION=false" +export "TRACK_WIDGET_CREATION=false" +export "TREE_SHAKE_ICONS=false" +export "PACKAGE_CONFIG=.packages" diff --git a/example/lib/main.dart b/example/lib/main.dart index fd87dd7d..e4d31926 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,4 +1,3 @@ -import 'dart:io'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:video_compress/video_compress.dart'; @@ -105,7 +104,8 @@ class _MyHomePageState extends State { ), floatingActionButton: FloatingActionButton( onPressed: () async { - File file = await ImagePicker.pickVideo(source: ImageSource.gallery); + final file = + await ImagePicker().getVideo(source: ImageSource.gallery); await VideoCompress.setLogLevel(0); final info = await VideoCompress.compressVideo( file.path, @@ -114,9 +114,10 @@ class _MyHomePageState extends State { includeAudio: true, ); if (info != null) { - _counter = info.path; + setState(() { + _counter = info.path; + }); } - setState(() {}); }, tooltip: 'Increment', child: Icon(Icons.add), diff --git a/lib/src/media_data.dart b/lib/src/media/media_data.dart similarity index 100% rename from lib/src/media_data.dart rename to lib/src/media/media_data.dart diff --git a/lib/src/media_info.dart b/lib/src/media/media_info.dart similarity index 100% rename from lib/src/media_info.dart rename to lib/src/media/media_info.dart diff --git a/lib/src/progress_callback.dart/compress_mixin.dart b/lib/src/progress_callback.dart/compress_mixin.dart new file mode 100644 index 00000000..c9733022 --- /dev/null +++ b/lib/src/progress_callback.dart/compress_mixin.dart @@ -0,0 +1,33 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'subscription.dart'; + +class CompressMixin { + final compressProgress$ = ObservableBuilder(); + final _channel = const MethodChannel('video_compress'); + + @protected + void initProcessCallback() { + _channel.setMethodCallHandler(_progresCallback); + } + + MethodChannel get channel => _channel; + + bool _isCompressing = false; + + bool get isCompressing => _isCompressing; + + @protected + void setProcessingStatus(bool status) { + _isCompressing = status; + } + + Future _progresCallback(MethodCall call) async { + switch (call.method) { + case 'updateProgress': + final progress = double.tryParse(call.arguments.toString()); + if (progress != null) compressProgress$.next(progress); + break; + } + } +} diff --git a/lib/src/progress_callback.dart/subscription.dart b/lib/src/progress_callback.dart/subscription.dart new file mode 100644 index 00000000..3e28c694 --- /dev/null +++ b/lib/src/progress_callback.dart/subscription.dart @@ -0,0 +1,30 @@ +import 'dart:async'; + +import 'dart:ui'; + +class ObservableBuilder { + StreamController _observable = StreamController(); + bool notSubscribed = true; + + void next(T value) { + _observable.add(value); + } + + Subscription subscribe(void onData(T event), + {Function onError, void onDone(), bool cancelOnError}) { + notSubscribed = false; + _observable.stream.listen(onData, + onError: onError, onDone: onDone, cancelOnError: cancelOnError); + return Subscription(() { + _observable.close(); + + // Create a new instance to avoid errors + _observable = StreamController(); + }); + } +} + +class Subscription { + final VoidCallback unsubscribe; + const Subscription(this.unsubscribe); +} diff --git a/lib/src/video_compressor.dart b/lib/src/video_compress/video_compressor.dart similarity index 64% rename from lib/src/video_compressor.dart rename to lib/src/video_compress/video_compressor.dart index 5b45fc1b..87d28a47 100644 --- a/lib/src/video_compressor.dart +++ b/lib/src/video_compress/video_compressor.dart @@ -2,47 +2,44 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; -import 'dart:ui'; -import 'video_quality.dart'; +import '../progress_callback.dart/compress_mixin.dart'; +import '../video_compress/video_quality.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'media_info.dart'; +import '../media/media_info.dart'; -class VideoCompress { - static VideoCompress _instance; +abstract class IVideoCompress extends CompressMixin {} - factory VideoCompress() { - if (_instance == null) _instance = VideoCompress._(); - return _instance; +class _VideoCompressImpl extends IVideoCompress { + _VideoCompressImpl._() { + initProcessCallback(); } - VideoCompress._() { - _channel.setMethodCallHandler(_progresCallback); + factory _VideoCompressImpl() => instance; + static _VideoCompressImpl _instance; + static _VideoCompressImpl get instance { + return _instance ??= _VideoCompressImpl._(); } - static const _channel = const MethodChannel('video_compress'); - - /// Check compress state - static bool get isCompressing => _isCompressing; + static void _dispose() { + _instance = null; + } +} - static bool _isCompressing = false; +// ignore: non_constant_identifier_names +IVideoCompress get VideoCompress => _VideoCompressImpl.instance; - Future _progresCallback(MethodCall call) async { - switch (call.method) { - case 'updateProgress': - final progress = double.tryParse(call.arguments.toString()); - if (progress != null) compressProgress$.next(progress); - break; - } +extension Compress on IVideoCompress { + void dispose() { + _VideoCompressImpl._dispose(); } - /// Subscribe the compress progress - static ObservableBuilder compressProgress$ = ObservableBuilder(); - - static Future _invoke(String name, [Map params]) async { + Future _invoke(String name, [Map params]) async { T result; try { - result = params != null ? await _channel.invokeMethod(name, params) : await _channel.invokeMethod(name); + result = params != null + ? await channel.invokeMethod(name, params) + : await channel.invokeMethod(name); } on PlatformException catch (e) { debugPrint('''Error from VideoCompress: Method: $name @@ -54,7 +51,7 @@ class VideoCompress { /// getByteThumbnail return [Future], /// quality can be controlled by [quality] from 1 to 100, /// select the position unit in the video by [position] is seconds - static Future getByteThumbnail( + Future getByteThumbnail( String path, { int quality = 100, int position = -1, @@ -72,7 +69,7 @@ class VideoCompress { /// getFileThumbnail return [Future] /// quality can be controlled by [quality] from 1 to 100, /// select the position unit in the video by [position] is seconds - static Future getFileThumbnail( + Future getFileThumbnail( String path, { int quality = 100, int position = -1, @@ -100,7 +97,7 @@ class VideoCompress { /// final info = await _flutterVideoCompress.getMediaInfo(file.path); /// debugPrint(info.toJson()); /// ``` - static Future getMediaInfo(String path) async { + Future getMediaInfo(String path) async { assert(path != null); final jsonStr = await _invoke('getMediaInfo', {'path': path}); final jsonMap = json.decode(jsonStr); @@ -122,7 +119,7 @@ class VideoCompress { /// ); /// debugPrint(info.toJson()); /// ``` - static Future compressVideo( + Future compressVideo( String path, { VideoQuality quality = VideoQuality.DefaultQuality, bool deleteOrigin = false, @@ -132,12 +129,12 @@ class VideoCompress { int frameRate = 30, }) async { assert(path != null); - if (_isCompressing) { + if (isCompressing) { throw StateError('''VideoCompress Error: Method: compressVideo Already have a compression process, you need to wait for the process to finish or stop it'''); } - _isCompressing = true; + if (compressProgress$.notSubscribed) { debugPrint('''VideoCompress: You can try to subscribe to the compressProgress\$ stream to know the compressing state.'''); @@ -151,7 +148,10 @@ class VideoCompress { 'includeAudio': includeAudio, 'frameRate': frameRate, }); - _isCompressing = false; + + // ignore: invalid_use_of_protected_member + setProcessingStatus(false); + if (jsonStr != null) { final jsonMap = json.decode(jsonStr); return MediaInfo.fromJson(jsonMap); @@ -162,44 +162,19 @@ class VideoCompress { /// stop compressing the file that is currently being compressed. /// If there is no compression process, nothing will happen. - static Future cancelCompression() async { + Future cancelCompression() async { await _invoke('cancelCompression'); } /// delete the cache folder, please do not put other things /// in the folder of this plugin, it will be cleared - static Future deleteAllCache() async { + Future deleteAllCache() async { return await _invoke('deleteAllCache'); } - static Future setLogLevel(int logLevel) async { + Future setLogLevel(int logLevel) async { return await _invoke('setLogLevel', { 'logLevel': logLevel, }); } } - -class ObservableBuilder { - StreamController _observable = StreamController(); - bool notSubscribed = true; - - void next(T value) { - _observable.add(value); - } - - Subscription subscribe(void onData(T event), {Function onError, void onDone(), bool cancelOnError}) { - notSubscribed = false; - _observable.stream.listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError); - return Subscription(() { - _observable.close(); - - // Create a new instance to avoid errors - _observable = StreamController(); - }); - } -} - -class Subscription { - final VoidCallback unsubscribe; - const Subscription(this.unsubscribe); -} diff --git a/lib/src/video_quality.dart b/lib/src/video_compress/video_quality.dart similarity index 100% rename from lib/src/video_quality.dart rename to lib/src/video_compress/video_quality.dart diff --git a/lib/video_compress.dart b/lib/video_compress.dart index 4c6203a9..954c90ac 100644 --- a/lib/video_compress.dart +++ b/lib/video_compress.dart @@ -1,6 +1,6 @@ library video_compress; -export 'src/video_compressor.dart'; -export 'src/video_quality.dart'; -export 'src/media_info.dart'; -export 'src/media_data.dart'; +export 'src/video_compress/video_compressor.dart'; +export 'src/video_compress/video_quality.dart'; +export 'src/media/media_info.dart'; +export 'src/media/media_data.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 4c4fa43d..dc268e53 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,10 +1,10 @@ name: video_compress description: Light library of video manipulation of Flutter. Compress video, remove audio, get video thumbnail from dart code. -version: 2.0.0 +version: 2.1.0 homepage: https://github.com/jonataslaw environment: - sdk: ">=2.0.0 <3.0.0" + sdk: ">=2.6.0 <3.0.0" dependencies: flutter: From 74ff42c744551bd6213c087a89e5ec07e4521120 Mon Sep 17 00:00:00 2001 From: Jonatas Date: Wed, 14 Oct 2020 19:51:36 -0300 Subject: [PATCH 16/44] fix isCompressing getter --- lib/src/video_compress/video_compressor.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/video_compress/video_compressor.dart b/lib/src/video_compress/video_compressor.dart index 87d28a47..04b0eaec 100644 --- a/lib/src/video_compress/video_compressor.dart +++ b/lib/src/video_compress/video_compressor.dart @@ -15,7 +15,6 @@ class _VideoCompressImpl extends IVideoCompress { initProcessCallback(); } - factory _VideoCompressImpl() => instance; static _VideoCompressImpl _instance; static _VideoCompressImpl get instance { return _instance ??= _VideoCompressImpl._(); @@ -139,6 +138,8 @@ extension Compress on IVideoCompress { debugPrint('''VideoCompress: You can try to subscribe to the compressProgress\$ stream to know the compressing state.'''); } + // ignore: invalid_use_of_protected_member + setProcessingStatus(true); final jsonStr = await _invoke('compressVideo', { 'path': path, 'quality': quality.index, From d8861a42b4c7a4f811f7040ce171e8307a4a9d62 Mon Sep 17 00:00:00 2001 From: Jonatas Date: Wed, 14 Oct 2020 20:07:45 -0300 Subject: [PATCH 17/44] update to 2.1.0 --- README.md | 28 +++++++++++------------ example/.flutter-plugins-dependencies | 2 +- lib/src/video_compress/video_quality.dart | 2 -- pubspec.yaml | 16 ++++++------- 4 files changed, 23 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index ad3f4340..c8f0c97f 100644 --- a/README.md +++ b/README.md @@ -91,19 +91,19 @@ class _Compress extends State { - Add cancel function to Android ## Methods -|Functions|Parameters|Description|Returns| -|--|--|--|--| -|getByteThumbnail|String `path`[video path], int `quality`(1-100)[thumbnail quality], int `position`[Get a thumbnail from video position]|get thumbnail from video `path`|`Future`| -|getFileThumbnail|String `path`[video path], int `quality`(1-100)[thumbnail quality], int `position`[Get a thumbnail from video position]|get thumbnail file from video `path`|`Future`| -|getMediaInfo|String `path`[video path]|get media information from video `path`|`Future`| -|compressVideo|String `path`[video path], VideoQuality `quality`[compressed video quality], bool `deleteOrigin`[delete the origin video], int `startTime`[compression video start time], int `duration`[compression video duration from start time], bool `includeAudio`[is include audio in compressed video], int `frameRate`[compressed video frame rate]|compression video at origin video `path`|`Future`| -|cancelCompression|`none`|cancel compressing|`Future`| -|deleteAllCache|`none`|Delete all files generated by 'video_compress' will delete all files located at 'video_compress'|`Future`| +| Functions | Parameters | Description | Returns | +| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ------------------- | +| getByteThumbnail | String `path`[video path], int `quality`(1-100)[thumbnail quality], int `position`[Get a thumbnail from video position] | get thumbnail from video `path` | `Future` | +| getFileThumbnail | String `path`[video path], int `quality`(1-100)[thumbnail quality], int `position`[Get a thumbnail from video position] | get thumbnail file from video `path` | `Future` | +| getMediaInfo | String `path`[video path] | get media information from video `path` | `Future` | +| compressVideo | String `path`[video path], VideoQuality `quality`[compressed video quality], bool `deleteOrigin`[delete the origin video], int `startTime`[compression video start time], int `duration`[compression video duration from start time], bool `includeAudio`[is include audio in compressed video], int `frameRate`[compressed video frame rate] | compression video at origin video `path` | `Future` | +| cancelCompression | `none` | cancel compressing | `Future` | +| deleteAllCache | `none` | Delete all files generated by 'video_compress' will delete all files located at 'video_compress' | `Future` | ## Subscriptions -|Subscriptions|Description|Stream| -|--|--|--| -|compressProgress$|Subscribe the compression progress steam|double `progress`| +| Subscriptions | Description | Stream | +| ----------------- | ---------------------------------------- | ----------------- | +| compressProgress$ | Subscribe the compression progress steam | double `progress` | ## Contribute @@ -124,11 +124,11 @@ Thanks goes to these wonderful people: - + - - + + diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies index 7cc6f616..232acd7f 100644 --- a/example/.flutter-plugins-dependencies +++ b/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"video_compress","path":"/home/jonny/Downloads/videocompressplig/VideoCompress/","dependencies":[]},{"name":"image_picker","path":"/opt/flutter/.pub-cache/hosted/pub.dartlang.org/image_picker-0.6.7+12/","dependencies":[]},{"name":"video_player","path":"/opt/flutter/.pub-cache/hosted/pub.dartlang.org/video_player-0.10.12+5/","dependencies":[]}],"android":[{"name":"video_compress","path":"/home/jonny/Downloads/videocompressplig/VideoCompress/","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","path":"/opt/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_plugin_android_lifecycle-1.0.11/","dependencies":[]},{"name":"image_picker","path":"/opt/flutter/.pub-cache/hosted/pub.dartlang.org/image_picker-0.6.7+12/","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"video_player","path":"/opt/flutter/.pub-cache/hosted/pub.dartlang.org/video_player-0.10.12+5/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[{"name":"video_player_web","path":"/opt/flutter/.pub-cache/hosted/pub.dartlang.org/video_player_web-0.1.4/","dependencies":[]}]},"dependencyGraph":[{"name":"video_compress","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"image_picker","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"video_player","dependencies":["video_player_web"]},{"name":"video_player_web","dependencies":[]}],"date_created":"2020-10-14 19:45:59.207690","version":"1.23.0-13.0.pre"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"video_compress","path":"/home/jonny/Downloads/videocompressplig/VideoCompress/","dependencies":[]},{"name":"image_picker","path":"/opt/flutter/.pub-cache/hosted/pub.dartlang.org/image_picker-0.6.7+12/","dependencies":[]},{"name":"video_player","path":"/opt/flutter/.pub-cache/hosted/pub.dartlang.org/video_player-0.10.12+5/","dependencies":[]}],"android":[{"name":"video_compress","path":"/home/jonny/Downloads/videocompressplig/VideoCompress/","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","path":"/opt/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_plugin_android_lifecycle-1.0.11/","dependencies":[]},{"name":"image_picker","path":"/opt/flutter/.pub-cache/hosted/pub.dartlang.org/image_picker-0.6.7+12/","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"video_player","path":"/opt/flutter/.pub-cache/hosted/pub.dartlang.org/video_player-0.10.12+5/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[{"name":"video_player_web","path":"/opt/flutter/.pub-cache/hosted/pub.dartlang.org/video_player_web-0.1.4/","dependencies":[]}]},"dependencyGraph":[{"name":"video_compress","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"image_picker","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"video_player","dependencies":["video_player_web"]},{"name":"video_player_web","dependencies":[]}],"date_created":"2020-10-14 20:02:24.932234","version":"1.23.0-13.0.pre"} \ No newline at end of file diff --git a/lib/src/video_compress/video_quality.dart b/lib/src/video_compress/video_quality.dart index 9dccaf69..2b89f418 100644 --- a/lib/src/video_compress/video_quality.dart +++ b/lib/src/video_compress/video_quality.dart @@ -1,3 +1 @@ - - enum VideoQuality { DefaultQuality, LowQuality, MediumQuality, HighestQuality } diff --git a/pubspec.yaml b/pubspec.yaml index dc268e53..53256d85 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,10 +1,11 @@ name: video_compress description: Light library of video manipulation of Flutter. Compress video, remove audio, get video thumbnail from dart code. version: 2.1.0 -homepage: https://github.com/jonataslaw +homepage: https://github.com/jonataslaw/VideoCompress environment: sdk: ">=2.6.0 <3.0.0" + flutter: ">=1.12.0 <2.0.0" dependencies: flutter: @@ -17,15 +18,14 @@ dev_dependencies: # For information on the generic Dart part of this file, see the # following page: https://www.dartlang.org/tools/pub/pubspec -# The following section is specific to Flutter. flutter: - # This section identifies this Flutter project as a plugin project. - # The androidPackage and pluginClass identifiers should not ordinarily - # be modified. They are used by the tooling to maintain consistency when - # adding or updating assets for this project. plugin: - androidPackage: com.example.video_compress - pluginClass: VideoCompressPlugin + platforms: + android: + package: com.example.video_compress + pluginClass: VideoCompressPlugin + ios: + pluginClass: VideoCompressPlugin # To add assets to your plugin package, add an assets section, like this: # assets: From 1d594ec73bf8a8aebb15eb441303eacf47d354b8 Mon Sep 17 00:00:00 2001 From: Arjan Aswal Date: Wed, 21 Oct 2020 14:16:07 +0530 Subject: [PATCH 18/44] Fix Typo - '_progresCallback' to '_progressCallback' --- lib/src/progress_callback.dart/compress_mixin.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/progress_callback.dart/compress_mixin.dart b/lib/src/progress_callback.dart/compress_mixin.dart index c9733022..916a8cb4 100644 --- a/lib/src/progress_callback.dart/compress_mixin.dart +++ b/lib/src/progress_callback.dart/compress_mixin.dart @@ -8,7 +8,7 @@ class CompressMixin { @protected void initProcessCallback() { - _channel.setMethodCallHandler(_progresCallback); + _channel.setMethodCallHandler(_progressCallback); } MethodChannel get channel => _channel; @@ -22,7 +22,7 @@ class CompressMixin { _isCompressing = status; } - Future _progresCallback(MethodCall call) async { + Future _progressCallback(MethodCall call) async { switch (call.method) { case 'updateProgress': final progress = double.tryParse(call.arguments.toString()); From 758b594bcf58341c1ea12db5aec6156da03a0a85 Mon Sep 17 00:00:00 2001 From: Arjan Aswal Date: Wed, 21 Oct 2020 17:27:32 +0530 Subject: [PATCH 19/44] Give 'includeAudio' parameter the default value of true to make it null safe --- .../kotlin/com/example/video_compress/VideoCompressPlugin.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1ec5afaa..f11c86c2 100644 --- a/android/src/main/kotlin/com/example/video_compress/VideoCompressPlugin.kt +++ b/android/src/main/kotlin/com/example/video_compress/VideoCompressPlugin.kt @@ -80,7 +80,7 @@ class VideoCompressPlugin : MethodCallHandler, FlutterPlugin { val deleteOrigin = call.argument("deleteOrigin")!! val startTime = call.argument("startTime") val duration = call.argument("duration") - val includeAudio = call.argument("includeAudio")!! + val includeAudio = call.argument("includeAudio") ?: true val frameRate = if (call.argument("frameRate")==null) 30 else call.argument("frameRate") val tempDir: String = context.getExternalFilesDir("video_compress")!!.absolutePath From c6dac13245d3f0cf534f38ff7a441b67f869c909 Mon Sep 17 00:00:00 2001 From: Kawaljeet Singh Date: Fri, 23 Oct 2020 15:33:50 +0530 Subject: [PATCH 20/44] Update README.md --- README.md | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 77 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c8f0c97f..ef4e8da9 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,32 @@ # video_compress -[![All Contributors](https://img.shields.io/badge/all_contributors-8-orange.svg?style=flat-square)](#contributors-) + + All Contributors + + + Platform + + + Pub Package + + + License: MIT + + + Forks + + + Stars + +
+
Compress videos, remove audio, manipulate thumbnails, and make your video compatible with all platforms through this lightweight and efficient library. 100% native code was used, we do not use FFMPEG as it is very slow, bloated and the GNU license is an obstacle for commercial applications. @@ -10,7 +35,57 @@ All video files are encoded in an MP4 container with AAC audio that allows 100% Works on ANDROID and IOS. -### How to use + + +# Table of Contents + - [Installing](#lets-get-started) + - [How to use](#how-to-use) + * [Imports](#imports) + * [Video compression](#video-compression) + * [Check compress state](#check-compress-state) + * [Get memory thumbnail from VideoPath](#get-memory-thumbnail-from-videopath) + * [Get File thumbnail from VideoPath](#get-file-thumbnail-from-videopath) + * [Get media information](#get-media-information) + * [delete all cache files](#delete-all-cache-files) + * [Listen the compression progress](#listen-the-compression-progress) + - [TODO](#todo) + +# Lets Get Started + +### 1. Depend on it +Add this to your package's `pubspec.yaml` file: + +```yaml +dependencies: + video_compress: ^2.1.0 +``` + +### 2. Install it + +You can install packages from the command line: + +with `pub`: + +```css +$ pub get +``` + +### 3. Import it + +Now in your `Dart` code, you can use: + +````dart +import 'package:video_compress/video_compress.dart'; +```` + +# How to use + +### Imports + +````dart +import 'package:video_compress/video_compress.dart'; + +```` ## Video compression From 6b888fce3991dcfaa3882c2fe8546bff5516f8cc Mon Sep 17 00:00:00 2001 From: MrJ Date: Wed, 4 Nov 2020 16:34:52 +0100 Subject: [PATCH 21/44] Added video trimming on Android --- .../com/example/video_compress/VideoCompressPlugin.kt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) 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 f11c86c2..c072b4b3 100644 --- a/android/src/main/kotlin/com/example/video_compress/VideoCompressPlugin.kt +++ b/android/src/main/kotlin/com/example/video_compress/VideoCompressPlugin.kt @@ -5,6 +5,8 @@ import android.net.Uri import android.util.Log import com.otaliastudios.transcoder.Transcoder import com.otaliastudios.transcoder.TranscoderListener +import com.otaliastudios.transcoder.source.TrimDataSource +import com.otaliastudios.transcoder.source.UriDataSource import com.otaliastudios.transcoder.strategy.DefaultAudioStrategy import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy import com.otaliastudios.transcoder.strategy.RemoveTrackStrategy @@ -125,9 +127,16 @@ class VideoCompressPlugin : MethodCallHandler, FlutterPlugin { RemoveTrackStrategy() } + val dataSource = if (startTime != null || duration != null){ + val source = UriDataSource(context, Uri.parse(path)) + TrimDataSource(source, (1000 * 1000 * (startTime ?: 0)).toLong(), (1000 * 1000 * (duration ?: 0)).toLong()) + }else{ + UriDataSource(context, Uri.parse(path)) + } + transcodeFuture = Transcoder.into(destPath!!) - .addDataSource(context, Uri.parse(path)) + .addDataSource(dataSource) .setAudioTrackStrategy(audioTrackStrategy) .setVideoTrackStrategy(videoTrackStrategy) .setListener(object : TranscoderListener { From 4e8dd1d9c040d6f20a6149dd0e85da48c945d6a3 Mon Sep 17 00:00:00 2001 From: Jonatas Date: Wed, 18 Nov 2020 11:34:38 -0300 Subject: [PATCH 22/44] update to 2.1.1 --- CHANGELOG.md | 4 ++ example/.flutter-plugins-dependencies | 2 +- .../ios/Flutter/flutter_export_environment.sh | 2 +- example/lib/main.dart | 43 ------------------- .../compress_mixin.dart | 0 .../subscription.dart | 0 lib/src/video_compress/video_compressor.dart | 2 +- lib/video_compress.dart | 1 + pubspec.yaml | 2 +- 9 files changed, 9 insertions(+), 47 deletions(-) rename lib/src/{progress_callback.dart => progress_callback}/compress_mixin.dart (100%) rename lib/src/{progress_callback.dart => progress_callback}/subscription.dart (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 437cf543..5fb154a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.1.1 +- Fix Subscription import +- Fix Error on android 10 with no includeAudio option + ## 2.1.0 - Added cancel compression to android - Fix compress progress diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies index 232acd7f..2cd18cfd 100644 --- a/example/.flutter-plugins-dependencies +++ b/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"video_compress","path":"/home/jonny/Downloads/videocompressplig/VideoCompress/","dependencies":[]},{"name":"image_picker","path":"/opt/flutter/.pub-cache/hosted/pub.dartlang.org/image_picker-0.6.7+12/","dependencies":[]},{"name":"video_player","path":"/opt/flutter/.pub-cache/hosted/pub.dartlang.org/video_player-0.10.12+5/","dependencies":[]}],"android":[{"name":"video_compress","path":"/home/jonny/Downloads/videocompressplig/VideoCompress/","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","path":"/opt/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_plugin_android_lifecycle-1.0.11/","dependencies":[]},{"name":"image_picker","path":"/opt/flutter/.pub-cache/hosted/pub.dartlang.org/image_picker-0.6.7+12/","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"video_player","path":"/opt/flutter/.pub-cache/hosted/pub.dartlang.org/video_player-0.10.12+5/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[{"name":"video_player_web","path":"/opt/flutter/.pub-cache/hosted/pub.dartlang.org/video_player_web-0.1.4/","dependencies":[]}]},"dependencyGraph":[{"name":"video_compress","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"image_picker","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"video_player","dependencies":["video_player_web"]},{"name":"video_player_web","dependencies":[]}],"date_created":"2020-10-14 20:02:24.932234","version":"1.23.0-13.0.pre"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"video_compress","path":"/home/jonny/Downloads/VideoCompress/","dependencies":[]},{"name":"image_picker","path":"/opt/flutter/.pub-cache/hosted/pub.dartlang.org/image_picker-0.6.7+14/","dependencies":[]},{"name":"video_player","path":"/opt/flutter/.pub-cache/hosted/pub.dartlang.org/video_player-0.10.12+5/","dependencies":[]}],"android":[{"name":"video_compress","path":"/home/jonny/Downloads/VideoCompress/","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","path":"/opt/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_plugin_android_lifecycle-1.0.11/","dependencies":[]},{"name":"image_picker","path":"/opt/flutter/.pub-cache/hosted/pub.dartlang.org/image_picker-0.6.7+14/","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"video_player","path":"/opt/flutter/.pub-cache/hosted/pub.dartlang.org/video_player-0.10.12+5/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[{"name":"video_player_web","path":"/opt/flutter/.pub-cache/hosted/pub.dartlang.org/video_player_web-0.1.4+1/","dependencies":[]}]},"dependencyGraph":[{"name":"video_compress","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"image_picker","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"video_player","dependencies":["video_player_web"]},{"name":"video_player_web","dependencies":[]}],"date_created":"2020-11-18 11:32:14.876474","version":"1.22.3"} \ No newline at end of file diff --git a/example/ios/Flutter/flutter_export_environment.sh b/example/ios/Flutter/flutter_export_environment.sh index 357328dd..b07897d5 100755 --- a/example/ios/Flutter/flutter_export_environment.sh +++ b/example/ios/Flutter/flutter_export_environment.sh @@ -1,7 +1,7 @@ #!/bin/sh # This is a generated file; do not edit or check into version control. export "FLUTTER_ROOT=/opt/flutter" -export "FLUTTER_APPLICATION_PATH=/home/jonny/Downloads/videocompressplig/VideoCompress/example" +export "FLUTTER_APPLICATION_PATH=/home/jonny/Downloads/VideoCompress/example" export "FLUTTER_TARGET=lib/main.dart" export "FLUTTER_BUILD_DIR=build" export "SYMROOT=${SOURCE_ROOT}/../build/ios" diff --git a/example/lib/main.dart b/example/lib/main.dart index e4d31926..415cc70a 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -7,21 +7,11 @@ void main() { } class MyApp extends StatelessWidget { - // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( - // This is the theme of your application. - // - // Try running your application with "flutter run". You'll see the - // application has a blue toolbar. Then, without quitting the app, try - // changing the primarySwatch below to Colors.green and then invoke - // "hot reload" (press "r" in the console where you ran "flutter run", - // or simply save your changes to "hot reload" in a Flutter IDE). - // Notice that the counter didn't reset back to zero; the application - // is not restarted. primarySwatch: Colors.blue, ), home: MyHomePage(title: 'Flutter Demo Home Page'), @@ -32,15 +22,6 @@ class MyApp extends StatelessWidget { class MyHomePage extends StatefulWidget { MyHomePage({Key key, this.title}) : super(key: key); - // This widget is the home page of your application. It is stateful, meaning - // that it has a State object (defined below) that contains fields that affect - // how it looks. - - // This class is the configuration for the state. It holds the values (in this - // case the title) provided by the parent (in this case the App widget) and - // used by the build method of the State. Fields in a Widget subclass are - // always marked "final". - final String title; @override @@ -52,36 +33,12 @@ class _MyHomePageState extends State { @override Widget build(BuildContext context) { - // This method is rerun every time setState is called, for instance as done - // by the _incrementCounter method above. - // - // The Flutter framework has been optimized to make rerunning build methods - // fast, so that you can just rebuild anything that needs updating rather - // than having to individually change instances of widgets. return Scaffold( appBar: AppBar( - // Here we take the value from the MyHomePage object that was created by - // the App.build method, and use it to set our appbar title. title: Text(widget.title), ), body: Center( - // Center is a layout widget. It takes a single child and positions it - // in the middle of the parent. child: Column( - // Column is also a layout widget. It takes a list of children and - // arranges them vertically. By default, it sizes itself to fit its - // children horizontally, and tries to be as tall as its parent. - // - // Invoke "debug painting" (press "p" in the console, choose the - // "Toggle Debug Paint" action from the Flutter Inspector in Android - // Studio, or the "Toggle Debug Paint" command in Visual Studio Code) - // to see the wireframe for each widget. - // - // Column has various properties to control how it sizes itself and - // how it positions its children. Here we use mainAxisAlignment to - // center the children vertically; the main axis here is the vertical - // axis because Columns are vertical (the cross axis would be - // horizontal). mainAxisAlignment: MainAxisAlignment.center, children: [ Text( diff --git a/lib/src/progress_callback.dart/compress_mixin.dart b/lib/src/progress_callback/compress_mixin.dart similarity index 100% rename from lib/src/progress_callback.dart/compress_mixin.dart rename to lib/src/progress_callback/compress_mixin.dart diff --git a/lib/src/progress_callback.dart/subscription.dart b/lib/src/progress_callback/subscription.dart similarity index 100% rename from lib/src/progress_callback.dart/subscription.dart rename to lib/src/progress_callback/subscription.dart diff --git a/lib/src/video_compress/video_compressor.dart b/lib/src/video_compress/video_compressor.dart index 04b0eaec..aed55399 100644 --- a/lib/src/video_compress/video_compressor.dart +++ b/lib/src/video_compress/video_compressor.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; -import '../progress_callback.dart/compress_mixin.dart'; +import '../progress_callback/compress_mixin.dart'; import '../video_compress/video_quality.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; diff --git a/lib/video_compress.dart b/lib/video_compress.dart index 954c90ac..73555043 100644 --- a/lib/video_compress.dart +++ b/lib/video_compress.dart @@ -4,3 +4,4 @@ export 'src/video_compress/video_compressor.dart'; export 'src/video_compress/video_quality.dart'; export 'src/media/media_info.dart'; export 'src/media/media_data.dart'; +export 'src/progress_callback/subscription.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 53256d85..88b0fb9b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: video_compress description: Light library of video manipulation of Flutter. Compress video, remove audio, get video thumbnail from dart code. -version: 2.1.0 +version: 2.1.1 homepage: https://github.com/jonataslaw/VideoCompress environment: From 82df0274e10f9f79da0d2bcba3fc1cb800bb020a Mon Sep 17 00:00:00 2001 From: Jonatas Date: Wed, 18 Nov 2020 11:43:41 -0300 Subject: [PATCH 23/44] upgrade android dependencies --- android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/build.gradle b/android/build.gradle index 9fc57383..c7301cea 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -41,5 +41,5 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'com.otaliastudios:transcoder:0.9.0' + implementation 'com.otaliastudios:transcoder:0.9.1' } From 9bd328d3a04c041c8c3636bf4cac65a302321629 Mon Sep 17 00:00:00 2001 From: Rene Lazo Date: Wed, 10 Mar 2021 11:30:06 -0500 Subject: [PATCH 24/44] Support null safety --- lib/src/media/media_info.dart | 27 +++++++++---------- lib/src/progress_callback/subscription.dart | 2 +- lib/src/video_compress/video_compressor.dart | 28 +++++++++----------- pubspec.yaml | 2 +- 4 files changed, 27 insertions(+), 32 deletions(-) diff --git a/lib/src/media/media_info.dart b/lib/src/media/media_info.dart index 3277bd66..0801ac76 100644 --- a/lib/src/media/media_info.dart +++ b/lib/src/media/media_info.dart @@ -1,25 +1,24 @@ import 'dart:io'; -import 'package:flutter/material.dart'; class MediaInfo { - String path; - String title; - String author; - int width; - int height; + String? path; + String? title; + String? author; + int? width; + int? height; /// [Android] API level 17 - int orientation; + int? orientation; /// bytes - int filesize; // filesize + int? filesize; // filesize /// microsecond - double duration; - bool isCancel; - File file; + double? duration; + bool? isCancel; + File? file; MediaInfo({ - @required this.path, + required this.path, this.title, this.author, this.width, @@ -41,7 +40,7 @@ class MediaInfo { filesize = json['filesize']; duration = double.tryParse('${json['duration']}'); isCancel = json['isCancel']; - file = File(path); + file = File(path!); } Map toJson() { @@ -59,7 +58,7 @@ class MediaInfo { if (this.isCancel != null) { data['isCancel'] = this.isCancel; } - data['file'] = File(path).toString(); + data['file'] = File(path!).toString(); return data; } } diff --git a/lib/src/progress_callback/subscription.dart b/lib/src/progress_callback/subscription.dart index 3e28c694..251a63ca 100644 --- a/lib/src/progress_callback/subscription.dart +++ b/lib/src/progress_callback/subscription.dart @@ -11,7 +11,7 @@ class ObservableBuilder { } Subscription subscribe(void onData(T event), - {Function onError, void onDone(), bool cancelOnError}) { + {Function? onError, void onDone()?, bool? cancelOnError}) { notSubscribed = false; _observable.stream.listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError); diff --git a/lib/src/video_compress/video_compressor.dart b/lib/src/video_compress/video_compressor.dart index aed55399..d248021a 100644 --- a/lib/src/video_compress/video_compressor.dart +++ b/lib/src/video_compress/video_compressor.dart @@ -15,7 +15,7 @@ class _VideoCompressImpl extends IVideoCompress { initProcessCallback(); } - static _VideoCompressImpl _instance; + static _VideoCompressImpl? _instance; static _VideoCompressImpl get instance { return _instance ??= _VideoCompressImpl._(); } @@ -33,8 +33,8 @@ extension Compress on IVideoCompress { _VideoCompressImpl._dispose(); } - Future _invoke(String name, [Map params]) async { - T result; + Future _invoke(String name, [Map? params]) async { + T? result; try { result = params != null ? await channel.invokeMethod(name, params) @@ -50,12 +50,11 @@ extension Compress on IVideoCompress { /// getByteThumbnail return [Future], /// quality can be controlled by [quality] from 1 to 100, /// select the position unit in the video by [position] is seconds - Future getByteThumbnail( + Future getByteThumbnail( String path, { int quality = 100, int position = -1, }) async { - assert(path != null); assert(quality > 1 || quality < 100); return await _invoke('getByteThumbnail', { @@ -73,14 +72,13 @@ extension Compress on IVideoCompress { int quality = 100, int position = -1, }) async { - assert(path != null); assert(quality > 1 || quality < 100); - final filePath = await _invoke('getFileThumbnail', { + final filePath = await (_invoke('getFileThumbnail', { 'path': path, 'quality': quality, 'position': position, - }); + }) as FutureOr); final file = File(filePath); @@ -97,8 +95,7 @@ extension Compress on IVideoCompress { /// debugPrint(info.toJson()); /// ``` Future getMediaInfo(String path) async { - assert(path != null); - final jsonStr = await _invoke('getMediaInfo', {'path': path}); + final jsonStr = await (_invoke('getMediaInfo', {'path': path}) as FutureOr); final jsonMap = json.decode(jsonStr); return MediaInfo.fromJson(jsonMap); } @@ -118,16 +115,15 @@ extension Compress on IVideoCompress { /// ); /// debugPrint(info.toJson()); /// ``` - Future compressVideo( + Future compressVideo( String path, { VideoQuality quality = VideoQuality.DefaultQuality, bool deleteOrigin = false, - int startTime, - int duration, - bool includeAudio, + int? startTime, + int? duration, + bool? includeAudio, int frameRate = 30, }) async { - assert(path != null); if (isCompressing) { throw StateError('''VideoCompress Error: Method: compressVideo @@ -169,7 +165,7 @@ extension Compress on IVideoCompress { /// delete the cache folder, please do not put other things /// in the folder of this plugin, it will be cleared - Future deleteAllCache() async { + Future deleteAllCache() async { return await _invoke('deleteAllCache'); } diff --git a/pubspec.yaml b/pubspec.yaml index 88b0fb9b..a649b65c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ version: 2.1.1 homepage: https://github.com/jonataslaw/VideoCompress environment: - sdk: ">=2.6.0 <3.0.0" + sdk: '>=2.12.0 <3.0.0' flutter: ">=1.12.0 <2.0.0" dependencies: From 250ffd1204a8d465cc6b9f0b3b7f2c066878506d Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Mon, 22 Mar 2021 15:43:11 +0100 Subject: [PATCH 25/44] feature: added macOS support --- example/lib/main.dart | 17 +- example/macos/.gitignore | 6 + example/macos/Flutter/Flutter-Debug.xcconfig | 2 + .../macos/Flutter/Flutter-Release.xcconfig | 2 + .../Flutter/GeneratedPluginRegistrant.swift | 14 + .../macos/Runner.xcodeproj/project.pbxproj | 632 ++++++++++++++++++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 89 +++ .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + example/macos/Runner/AppDelegate.swift | 9 + .../AppIcon.appiconset/Contents.json | 68 ++ .../AppIcon.appiconset/app_icon_1024.png | Bin 0 -> 46993 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 0 -> 3276 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 0 -> 1429 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 0 -> 5933 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 0 -> 1243 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 0 -> 14800 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 0 -> 1874 bytes example/macos/Runner/Base.lproj/MainMenu.xib | 339 ++++++++++ example/macos/Runner/Configs/AppInfo.xcconfig | 14 + example/macos/Runner/Configs/Debug.xcconfig | 2 + example/macos/Runner/Configs/Release.xcconfig | 2 + .../macos/Runner/Configs/Warnings.xcconfig | 13 + .../macos/Runner/DebugProfile.entitlements | 14 + example/macos/Runner/Info.plist | 32 + example/macos/Runner/MainFlutterWindow.swift | 15 + example/macos/Runner/Release.entitlements | 10 + example/pubspec.yaml | 4 +- macos/.gitignore | 36 + macos/Assets/.gitkeep | 0 macos/Classes/AvController.swift | 53 ++ macos/Classes/Utility.swift | 52 ++ macos/Classes/VideoCompressPlugin.swift | 255 +++++++ macos/video_compress.podspec | 21 + pubspec.yaml | 6 +- 36 files changed, 1728 insertions(+), 5 deletions(-) create mode 100644 example/macos/.gitignore create mode 100644 example/macos/Flutter/Flutter-Debug.xcconfig create mode 100644 example/macos/Flutter/Flutter-Release.xcconfig create mode 100644 example/macos/Flutter/GeneratedPluginRegistrant.swift create mode 100644 example/macos/Runner.xcodeproj/project.pbxproj create mode 100644 example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 example/macos/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 example/macos/Runner/AppDelegate.swift create mode 100644 example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png create mode 100644 example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png create mode 100644 example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png create mode 100644 example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png create mode 100644 example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png create mode 100644 example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png create mode 100644 example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png create mode 100644 example/macos/Runner/Base.lproj/MainMenu.xib create mode 100644 example/macos/Runner/Configs/AppInfo.xcconfig create mode 100644 example/macos/Runner/Configs/Debug.xcconfig create mode 100644 example/macos/Runner/Configs/Release.xcconfig create mode 100644 example/macos/Runner/Configs/Warnings.xcconfig create mode 100644 example/macos/Runner/DebugProfile.entitlements create mode 100644 example/macos/Runner/Info.plist create mode 100644 example/macos/Runner/MainFlutterWindow.swift create mode 100644 example/macos/Runner/Release.entitlements create mode 100644 macos/.gitignore create mode 100644 macos/Assets/.gitkeep create mode 100644 macos/Classes/AvController.swift create mode 100644 macos/Classes/Utility.swift create mode 100644 macos/Classes/VideoCompressPlugin.swift create mode 100644 macos/video_compress.podspec diff --git a/example/lib/main.dart b/example/lib/main.dart index 415cc70a..4db2adc5 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:video_compress/video_compress.dart'; +import 'package:file_selector/file_selector.dart'; +import 'dart:io'; void main() { runApp(MyApp()); @@ -61,8 +63,18 @@ class _MyHomePageState extends State { ), floatingActionButton: FloatingActionButton( onPressed: () async { - final file = - await ImagePicker().getVideo(source: ImageSource.gallery); + var file; + if (Platform.isMacOS) { + final typeGroup = XTypeGroup(label: 'videos', extensions: ['mov', 'mp4']); + file = await openFile(acceptedTypeGroups: [typeGroup]); + } else { + final picker = ImagePicker(); + PickedFile pickedFile = await picker.getVideo(source: ImageSource.gallery); + file = File(pickedFile.path); + } + if (file == null) { + return; + } await VideoCompress.setLogLevel(0); final info = await VideoCompress.compressVideo( file.path, @@ -70,6 +82,7 @@ class _MyHomePageState extends State { deleteOrigin: false, includeAudio: true, ); + print(info.path); if (info != null) { setState(() { _counter = info.path; diff --git a/example/macos/.gitignore b/example/macos/.gitignore new file mode 100644 index 00000000..d2fd3772 --- /dev/null +++ b/example/macos/.gitignore @@ -0,0 +1,6 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/xcuserdata/ diff --git a/example/macos/Flutter/Flutter-Debug.xcconfig b/example/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 00000000..4b81f9b2 --- /dev/null +++ b/example/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/example/macos/Flutter/Flutter-Release.xcconfig b/example/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 00000000..5caa9d15 --- /dev/null +++ b/example/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 00000000..875bdd33 --- /dev/null +++ b/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,14 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import file_selector_macos +import video_compress + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + VideoCompressPlugin.register(with: registry.registrar(forPlugin: "VideoCompressPlugin")) +} diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..060cec7b --- /dev/null +++ b/example/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,632 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 465493BC5441E0D091BD5D94 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 55500D71601D644F7BEBE216 /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0331188151B693CDB3BBB87F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 55500D71601D644F7BEBE216 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + B783FECDF0149937F22DD33F /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + F34699D16471E8AC5B03DD00 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 465493BC5441E0D091BD5D94 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 08915241FA5D7DCB340077C3 /* Pods */ = { + isa = PBXGroup; + children = ( + 0331188151B693CDB3BBB87F /* Pods-Runner.debug.xcconfig */, + F34699D16471E8AC5B03DD00 /* Pods-Runner.release.xcconfig */, + B783FECDF0149937F22DD33F /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + 08915241FA5D7DCB340077C3 /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* example.app */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 55500D71601D644F7BEBE216 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 16DFBFF786E36F3FD74DDC20 /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + 8BAF1C4D01BC029A0D835E11 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 0930; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 16DFBFF786E36F3FD74DDC20 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 8BAF1C4D01BC029A0D835E11 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..ae8ff59d --- /dev/null +++ b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/example/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..21a3cc14 --- /dev/null +++ b/example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/macos/Runner/AppDelegate.swift b/example/macos/Runner/AppDelegate.swift new file mode 100644 index 00000000..d53ef643 --- /dev/null +++ b/example/macos/Runner/AppDelegate.swift @@ -0,0 +1,9 @@ +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..a2ec33f1 --- /dev/null +++ b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000000000000000000000000000000000000..3c4935a7ca84f0976aca34b7f2895d65fb94d1ea GIT binary patch literal 46993 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 literal 0 HcmV?d00001 diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000000000000000000000000000000000000..ed4cc16421680a50164ba74381b4b35ceaa0ccfc GIT binary patch literal 3276 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 N;OXk;vd$@?2>?>Ex^Vyi literal 0 HcmV?d00001 diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000000000000000000000000000000000000..bcbf36df2f2aaaa0a63c7dabc94e600184229d0d GIT binary patch literal 5933 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 f$KZHO*s&vt|9^w-6P?|#0pRK8NSwWJ?9znhg z3{`3j3=J&|48MRv4KElNN(~qoUL`OvSj}Ky5HFasE6@fg!ItFh?!xdN1Q+aGJ{c&& zS>O>_%)r1c48n{Iv*t(u1=&kHeO=ifbFy+6aSK)V_AxLppYn8Z42d|rc6w}vOsL55 z`t&mC&y2@JTEyg!eDiFX^k#CC!jq%>erB=yHqUP0XcDOTw6ko}L zX;EmMrq(fKk*eygEuA616;0)>@A{TK|55PV@70 z$OfzS*(VJxQev3J?yY?O=ul(v`fp}?u9z`JK3ugibK>)DyCwImZOF4d{xK%%Ks1*} zv$oa)9anR%lXIBUqYnhLmT>VOzHfNP?ZwJNZ!5$s9M08RynIvaXw>@G^T9@r9^KH1 zVy??F&uuk)bH9Y4pQY!hP58i_H6 znl-NcuCpLV6ZWU;4C zu@9exF&OZi`Bovq_m%T+WhU2kvkz@^_LpycBvqm3bMpLw8X-Or5sL>0AKE1$(k_L=_Zc=CUq#=x1-QZf)G7nHu@fmsQ1eN_N3+nTEz`4HI4Z6uVlE zJH+X&det8JU?tO?upcM4Z=cV!JV;yF>FfL5Q$M|W_2Z!P`S=}Wzp|_1^#d%e?_H`> zV@%vA$+bFVqhw9`U;TfP|5|PD{||OiYdor8P*i??|NJcb%kzT_73*7WE?Ua5hAnR2 z=7WE=PhTlJ#ZeRznjTUb;`E(wkMZrj4e|Hilz-mK>9cZHQY**5TUPw~u}k;u73KI}xAx!0m-)GVia|x^d3p~s_9gh83jA&Ra<8rM%`>U3x69t&NzbwWY}7Ar?)FK#IZ0z|d0H0EkRO w3{9;}4Xg|ebq&m|3=9_N6z8I7$jwj5OsmAL;bP(Gi$Dzwp00i_>zopr02+f8CIA2c literal 0 HcmV?d00001 diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000000000000000000000000000000000000..e71a726136a47ed24125c7efc79d68a4a01961b4 GIT binary patch literal 14800 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/macos/Runner/Configs/AppInfo.xcconfig b/example/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 00000000..cf9be60c --- /dev/null +++ b/example/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = example + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.example + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2021 com.example. All rights reserved. diff --git a/example/macos/Runner/Configs/Debug.xcconfig b/example/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 00000000..36b0fd94 --- /dev/null +++ b/example/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/example/macos/Runner/Configs/Release.xcconfig b/example/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 00000000..dff4f495 --- /dev/null +++ b/example/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/example/macos/Runner/Configs/Warnings.xcconfig b/example/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 00000000..42bcbf47 --- /dev/null +++ b/example/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/example/macos/Runner/DebugProfile.entitlements b/example/macos/Runner/DebugProfile.entitlements new file mode 100644 index 00000000..68acb49b --- /dev/null +++ b/example/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + com.apple.security.files.user-selected.read-write + + + diff --git a/example/macos/Runner/Info.plist b/example/macos/Runner/Info.plist new file mode 100644 index 00000000..4789daa6 --- /dev/null +++ b/example/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/example/macos/Runner/MainFlutterWindow.swift b/example/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 00000000..2722837e --- /dev/null +++ b/example/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController.init() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/example/macos/Runner/Release.entitlements b/example/macos/Runner/Release.entitlements new file mode 100644 index 00000000..012d091f --- /dev/null +++ b/example/macos/Runner/Release.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-write + + + diff --git a/example/pubspec.yaml b/example/pubspec.yaml index ad4cd9a9..1f93bb97 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -12,8 +12,10 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^0.1.2 - image_picker: ^0.6.0+10 + image_picker: ^0.7.3 video_player: ^0.10.1+3 + file_selector: ^0.8.2 # web + file_selector_macos: ^0.0.4 # macos dev_dependencies: flutter_test: diff --git a/macos/.gitignore b/macos/.gitignore new file mode 100644 index 00000000..710ec6cf --- /dev/null +++ b/macos/.gitignore @@ -0,0 +1,36 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig diff --git a/macos/Assets/.gitkeep b/macos/Assets/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/macos/Classes/AvController.swift b/macos/Classes/AvController.swift new file mode 100644 index 00000000..0473a814 --- /dev/null +++ b/macos/Classes/AvController.swift @@ -0,0 +1,53 @@ + +import AVFoundation +// import MobileCoreServices + +class AvController: NSObject { + public func getVideoAsset(_ url:URL)->AVURLAsset { + return AVURLAsset(url: url) + } + + public func getTrack(_ asset: AVURLAsset)->AVAssetTrack? { + var track : AVAssetTrack? = nil + let group = DispatchGroup() + group.enter() + asset.loadValuesAsynchronously(forKeys: ["tracks"], completionHandler: { + var error: NSError? = nil; + let status = asset.statusOfValue(forKey: "tracks", error: &error) + if (status == .loaded) { + track = asset.tracks(withMediaType: AVMediaType.video).first + } + group.leave() + }) + group.wait() + return track + } + + public func getVideoOrientation(_ path:String)-> Int? { + let url = Utility.getPathUrl(path) + let asset = getVideoAsset(url) + guard let track = getTrack(asset) else { + return nil + } + let size = track.naturalSize + let txf = track.preferredTransform + if size.width == txf.tx && size.height == txf.ty { + return 0 + } else if txf.tx == 0 && txf.ty == 0 { + return 90 + } else if txf.tx == 0 && txf.ty == size.width { + return 180 + } else { + return 270 + } + } + + public func getMetaDataByTag(_ asset:AVAsset,key:String)->String { + for item in asset.commonMetadata { + if item.commonKey?.rawValue == key { + return item.stringValue ?? ""; + } + } + return "" + } +} diff --git a/macos/Classes/Utility.swift b/macos/Classes/Utility.swift new file mode 100644 index 00000000..05ee119c --- /dev/null +++ b/macos/Classes/Utility.swift @@ -0,0 +1,52 @@ + +class Utility: NSObject { + static let fileManager = FileManager.default + + static func basePath()->String { + let path = "\(NSTemporaryDirectory())video_compress" + do { + if !fileManager.fileExists(atPath: path) { + try! fileManager.createDirectory(atPath: path, + withIntermediateDirectories: true, attributes: nil) + } + } + return path + } + + static func stripFileExtension(_ fileName:String)->String { + var components = fileName.components(separatedBy: ".") + if components.count > 1 { + components.removeLast() + return components.joined(separator: ".") + } else { + return fileName + } + } + static func getFileName(_ path: String)->String { + return stripFileExtension((path as NSString).lastPathComponent) + } + + static func getPathUrl(_ path: String)->URL { + return URL(fileURLWithPath: excludeFileProtocol(path)) + } + + static func excludeFileProtocol(_ path: String)->String { + return path.replacingOccurrences(of: "file://", with: "") + } + + static func keyValueToJson(_ keyAndValue: [String : Any?])->String { + let data = try! JSONSerialization.data(withJSONObject: keyAndValue as NSDictionary, options: []) + let jsonString = NSString(data:data as Data,encoding: String.Encoding.utf8.rawValue) + return jsonString! as String + } + + static func deleteFile(_ path: String, clear: Bool = false) { + let url = getPathUrl(path) + if fileManager.fileExists(atPath: url.absoluteString) { + try? fileManager.removeItem(at: url) + } + if clear { + try? fileManager.removeItem(at: url) + } + } +} diff --git a/macos/Classes/VideoCompressPlugin.swift b/macos/Classes/VideoCompressPlugin.swift new file mode 100644 index 00000000..2e2c016b --- /dev/null +++ b/macos/Classes/VideoCompressPlugin.swift @@ -0,0 +1,255 @@ +import FlutterMacOS +import AVFoundation +import Cocoa + +public class VideoCompressPlugin: NSObject, FlutterPlugin { + private let channelName = "video_compress" + private var exporter: AVAssetExportSession? = nil + private var stopCommand = false + private let channel: FlutterMethodChannel + private let avController = AvController() + + init(channel: FlutterMethodChannel) { + self.channel = channel + } + + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "video_compress", binaryMessenger: registrar.messenger) + let instance = VideoCompressPlugin(channel: channel) + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let args = call.arguments as? Dictionary + switch call.method { + case "getByteThumbnail": + let path = args!["path"] as! String + let quality = args!["quality"] as! NSNumber + let position = args!["position"] as! NSNumber + getByteThumbnail(path, quality, position, result) + case "getFileThumbnail": + let path = args!["path"] as! String + let quality = args!["quality"] as! NSNumber + let position = args!["position"] as! NSNumber + getFileThumbnail(path, quality, position, result) + case "getMediaInfo": + let path = args!["path"] as! String + getMediaInfo(path, result) + case "compressVideo": + let path = args!["path"] as! String + let quality = args!["quality"] as! NSNumber + let deleteOrigin = args!["deleteOrigin"] as! Bool + let startTime = args!["startTime"] as? Double + let duration = args!["duration"] as? Double + let includeAudio = args!["includeAudio"] as? Bool + let frameRate = args!["frameRate"] as? Int + compressVideo(path, quality, deleteOrigin, startTime, duration, includeAudio, + frameRate, result) + case "cancelCompression": + cancelCompression(result) + case "deleteAllCache": + Utility.deleteFile(Utility.basePath(), clear: true) + result(true) + case "setLogLevel": + result(true) + default: + result(FlutterMethodNotImplemented) + } + } + + private func getBitMap(_ path: String,_ quality: NSNumber,_ position: NSNumber,_ result: FlutterResult)-> Data? { + let url = Utility.getPathUrl(path) + let asset = avController.getVideoAsset(url) + guard let track = avController.getTrack(asset) else { return nil } + + let assetImgGenerate = AVAssetImageGenerator(asset: asset) + assetImgGenerate.appliesPreferredTrackTransform = true + + let timeScale = CMTimeScale(track.nominalFrameRate) + let time = CMTimeMakeWithSeconds(Float64(truncating: position),preferredTimescale: timeScale) + guard let img = try? assetImgGenerate.copyCGImage(at:time, actualTime: nil) else { + return nil + } + + let bitmapRep = NSBitmapImageRep(cgImage: img) + let jpegData = bitmapRep.representation(using: NSBitmapImageRep.FileType.jpeg, properties: [:])! + return jpegData + } + + private func getByteThumbnail(_ path: String,_ quality: NSNumber,_ position: NSNumber,_ result: FlutterResult) { + if let bitmap = getBitMap(path,quality,position,result) { + result(bitmap) + } + } + + private func getFileThumbnail(_ path: String,_ quality: NSNumber,_ position: NSNumber,_ result: FlutterResult) { + let fileName = Utility.getFileName(path) + let url = Utility.getPathUrl("\(Utility.basePath())/\(fileName).jpg") + Utility.deleteFile(path) + if let bitmap = getBitMap(path,quality,position,result) { + guard (try? bitmap.write(to: url)) != nil else { + return result(FlutterError(code: channelName,message: "getFileThumbnail error",details: "getFileThumbnail error")) + } + result(Utility.excludeFileProtocol(url.absoluteString)) + } + } + + public func getMediaInfoJson(_ path: String)->[String : Any?] { + let url = Utility.getPathUrl(path) + let asset = avController.getVideoAsset(url) + guard let track = avController.getTrack(asset) else { return [:] } + + let playerItem = AVPlayerItem(url: url) + let metadataAsset = playerItem.asset + + let orientation = avController.getVideoOrientation(path) + + let title = avController.getMetaDataByTag(metadataAsset,key: "title") + let author = avController.getMetaDataByTag(metadataAsset,key: "author") + + let duration = asset.duration.seconds * 1000 + let filesize = track.totalSampleDataLength + + let size = track.naturalSize.applying(track.preferredTransform) + + let width = abs(size.width) + let height = abs(size.height) + + let dictionary = [ + "path":Utility.excludeFileProtocol(path), + "title":title, + "author":author, + "width":width, + "height":height, + "duration":duration, + "filesize":filesize, + "orientation":orientation + ] as [String : Any?] + return dictionary + } + + private func getMediaInfo(_ path: String,_ result: FlutterResult) { + let json = getMediaInfoJson(path) + let string = Utility.keyValueToJson(json) + result(string) + } + + + @objc private func updateProgress(timer:Timer) { + let asset = timer.userInfo as! AVAssetExportSession + if(!stopCommand) { + channel.invokeMethod("updateProgress", arguments: "\(String(describing: asset.progress * 100))") + } + } + + private func getExportPreset(_ quality: NSNumber)->String { + switch(quality) { + case 1: + return AVAssetExportPresetLowQuality + case 2: + return AVAssetExportPresetMediumQuality + case 3: + return AVAssetExportPresetHighestQuality + default: + return AVAssetExportPresetMediumQuality + } + } + + private func getComposition(_ isIncludeAudio: Bool,_ timeRange: CMTimeRange, _ sourceVideoTrack: AVAssetTrack)->AVAsset { + let composition = AVMutableComposition() + if !isIncludeAudio { + let compressionVideoTrack = composition.addMutableTrack(withMediaType: AVMediaType.video, preferredTrackID: kCMPersistentTrackID_Invalid) + compressionVideoTrack!.preferredTransform = sourceVideoTrack.preferredTransform + try? compressionVideoTrack!.insertTimeRange(timeRange, of: sourceVideoTrack, at: CMTime.zero) + } else { + return sourceVideoTrack.asset! + } + + return composition + } + + private func compressVideo(_ path: String,_ quality: NSNumber,_ deleteOrigin: Bool,_ startTime: Double?, + _ duration: Double?,_ includeAudio: Bool?,_ frameRate: Int?, + _ result: @escaping FlutterResult) { + let sourceVideoUrl = Utility.getPathUrl(path) + let sourceVideoType = "mp4" + + let sourceVideoAsset = avController.getVideoAsset(sourceVideoUrl) + let sourceVideoTrack = avController.getTrack(sourceVideoAsset) + + let compressionUrl = + Utility.getPathUrl("\(Utility.basePath())/\(Utility.getFileName(path)).\(sourceVideoType)") + + let timescale = sourceVideoAsset.duration.timescale + let minStartTime = Double(startTime ?? 0) + + let videoDuration = sourceVideoAsset.duration.seconds + let minDuration = Double(duration ?? videoDuration) + let maxDurationTime = minStartTime + minDuration < videoDuration ? minDuration : videoDuration + + let cmStartTime = CMTimeMakeWithSeconds(minStartTime, preferredTimescale: timescale) + let cmDurationTime = CMTimeMakeWithSeconds(maxDurationTime, preferredTimescale: timescale) + let timeRange: CMTimeRange = CMTimeRangeMake(start: cmStartTime, duration: cmDurationTime) + + let isIncludeAudio = includeAudio != nil ? includeAudio! : true + + let session = getComposition(isIncludeAudio, timeRange, sourceVideoTrack!) + + let exporter = AVAssetExportSession(asset: session, presetName: getExportPreset(quality))! + + exporter.outputURL = compressionUrl + exporter.outputFileType = AVFileType.mp4 + exporter.shouldOptimizeForNetworkUse = true + + if frameRate != nil { + let videoComposition = AVMutableVideoComposition(propertiesOf: sourceVideoAsset) + videoComposition.frameDuration = CMTimeMake(value: 1, timescale: Int32(frameRate!)) + exporter.videoComposition = videoComposition + } + + if !isIncludeAudio { + exporter.timeRange = timeRange + } + + Utility.deleteFile(compressionUrl.absoluteString) + + let timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(self.updateProgress), + userInfo: exporter, repeats: true) + + exporter.exportAsynchronously(completionHandler: { + if(self.stopCommand) { + timer.invalidate() + self.stopCommand = false + var json = self.getMediaInfoJson(path) + json["isCancel"] = true + let jsonString = Utility.keyValueToJson(json) + return result(jsonString) + } + if deleteOrigin { + timer.invalidate() + let fileManager = FileManager.default + do { + if fileManager.fileExists(atPath: path) { + try fileManager.removeItem(atPath: path) + } + self.exporter = nil + self.stopCommand = false + } + catch let error as NSError { + print(error) + } + } + var json = self.getMediaInfoJson(compressionUrl.absoluteString) + json["isCancel"] = false + let jsonString = Utility.keyValueToJson(json) + result(jsonString) + }) + } + + private func cancelCompression(_ result: FlutterResult) { + exporter?.cancelExport() + stopCommand = true + result("") + } + +} diff --git a/macos/video_compress.podspec b/macos/video_compress.podspec new file mode 100644 index 00000000..788046c2 --- /dev/null +++ b/macos/video_compress.podspec @@ -0,0 +1,21 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# +Pod::Spec.new do |s| + s.name = 'video_compress' + s.version = '0.3.0' + s.swift_version = '5.0' + s.summary = 'A new flutter plugin project.' + s.description = <<-DESC +A new flutter plugin project. + DESC + s.homepage = 'https://github.com/jonataslaw/video_compress' + s.license = { :file => '../LICENSE' } + s.author = { 'Jonny Borges' => 'jonataborges01@gmail.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'FlutterMacOS' + s.platform = :osx, '10.11' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } +end + diff --git a/pubspec.yaml b/pubspec.yaml index 88b0fb9b..6df316f0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,8 +4,8 @@ version: 2.1.1 homepage: https://github.com/jonataslaw/VideoCompress environment: - sdk: ">=2.6.0 <3.0.0" - flutter: ">=1.12.0 <2.0.0" + sdk: ">=2.7.0 <3.0.0" + flutter: ">=1.20.0" dependencies: flutter: @@ -26,6 +26,8 @@ flutter: pluginClass: VideoCompressPlugin ios: pluginClass: VideoCompressPlugin + macos: + pluginClass: VideoCompressPlugin # To add assets to your plugin package, add an assets section, like this: # assets: From e394457a63336af7f673c15f5a9a14966b2e8842 Mon Sep 17 00:00:00 2001 From: Jonatas Borges Date: Wed, 31 Mar 2021 10:23:48 -0300 Subject: [PATCH 26/44] update to 3.0.0 --- CHANGELOG.md | 4 ++++ example/.flutter-plugins-dependencies | 2 +- example/ios/Flutter/flutter_export_environment.sh | 6 ++---- pubspec.yaml | 8 ++++---- test/video_compress_test.dart | 11 +++++------ 5 files changed, 16 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fb154a3..56c14b35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.0.0 +- Added MacOS support (thank's @efraespada) +- Null-safety support (thank's @rlazom feat @leynier) + ## 2.1.1 - Fix Subscription import - Fix Error on android 10 with no includeAudio option diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies index 2cd18cfd..afab8a4e 100644 --- a/example/.flutter-plugins-dependencies +++ b/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"video_compress","path":"/home/jonny/Downloads/VideoCompress/","dependencies":[]},{"name":"image_picker","path":"/opt/flutter/.pub-cache/hosted/pub.dartlang.org/image_picker-0.6.7+14/","dependencies":[]},{"name":"video_player","path":"/opt/flutter/.pub-cache/hosted/pub.dartlang.org/video_player-0.10.12+5/","dependencies":[]}],"android":[{"name":"video_compress","path":"/home/jonny/Downloads/VideoCompress/","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","path":"/opt/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_plugin_android_lifecycle-1.0.11/","dependencies":[]},{"name":"image_picker","path":"/opt/flutter/.pub-cache/hosted/pub.dartlang.org/image_picker-0.6.7+14/","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"video_player","path":"/opt/flutter/.pub-cache/hosted/pub.dartlang.org/video_player-0.10.12+5/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[{"name":"video_player_web","path":"/opt/flutter/.pub-cache/hosted/pub.dartlang.org/video_player_web-0.1.4+1/","dependencies":[]}]},"dependencyGraph":[{"name":"video_compress","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"image_picker","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"video_player","dependencies":["video_player_web"]},{"name":"video_player_web","dependencies":[]}],"date_created":"2020-11-18 11:32:14.876474","version":"1.22.3"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"image_picker","path":"/Users/jonatasborges/flutter/.pub-cache/hosted/pub.dartlang.org/image_picker-0.7.4/","dependencies":[]},{"name":"video_compress","path":"/Users/jonatasborges/VideoCompress/","dependencies":[]},{"name":"video_player","path":"/Users/jonatasborges/flutter/.pub-cache/hosted/pub.dartlang.org/video_player-0.10.12+5/","dependencies":[]}],"android":[{"name":"flutter_plugin_android_lifecycle","path":"/Users/jonatasborges/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_plugin_android_lifecycle-2.0.1/","dependencies":[]},{"name":"image_picker","path":"/Users/jonatasborges/flutter/.pub-cache/hosted/pub.dartlang.org/image_picker-0.7.4/","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"video_compress","path":"/Users/jonatasborges/VideoCompress/","dependencies":[]},{"name":"video_player","path":"/Users/jonatasborges/flutter/.pub-cache/hosted/pub.dartlang.org/video_player-0.10.12+5/","dependencies":[]}],"macos":[{"name":"file_selector_macos","path":"/Users/jonatasborges/flutter/.pub-cache/hosted/pub.dartlang.org/file_selector_macos-0.0.4/","dependencies":[]},{"name":"video_compress","path":"/Users/jonatasborges/VideoCompress/","dependencies":[]}],"linux":[],"windows":[],"web":[{"name":"file_selector_web","path":"/Users/jonatasborges/flutter/.pub-cache/hosted/pub.dartlang.org/file_selector_web-0.8.1/","dependencies":[]},{"name":"image_picker_for_web","path":"/Users/jonatasborges/flutter/.pub-cache/hosted/pub.dartlang.org/image_picker_for_web-2.0.0/","dependencies":[]},{"name":"video_player_web","path":"/Users/jonatasborges/flutter/.pub-cache/hosted/pub.dartlang.org/video_player_web-0.1.4+1/","dependencies":[]}]},"dependencyGraph":[{"name":"file_selector","dependencies":["file_selector_web"]},{"name":"file_selector_macos","dependencies":[]},{"name":"file_selector_web","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"image_picker","dependencies":["flutter_plugin_android_lifecycle","image_picker_for_web"]},{"name":"image_picker_for_web","dependencies":[]},{"name":"video_compress","dependencies":[]},{"name":"video_player","dependencies":["video_player_web"]},{"name":"video_player_web","dependencies":[]}],"date_created":"2021-03-31 10:22:40.807552","version":"2.0.3"} \ No newline at end of file diff --git a/example/ios/Flutter/flutter_export_environment.sh b/example/ios/Flutter/flutter_export_environment.sh index b07897d5..6a1c4dce 100755 --- a/example/ios/Flutter/flutter_export_environment.sh +++ b/example/ios/Flutter/flutter_export_environment.sh @@ -1,12 +1,10 @@ #!/bin/sh # This is a generated file; do not edit or check into version control. -export "FLUTTER_ROOT=/opt/flutter" -export "FLUTTER_APPLICATION_PATH=/home/jonny/Downloads/VideoCompress/example" +export "FLUTTER_ROOT=/Users/jonatasborges/flutter" +export "FLUTTER_APPLICATION_PATH=/Users/jonatasborges/VideoCompress/example" export "FLUTTER_TARGET=lib/main.dart" export "FLUTTER_BUILD_DIR=build" export "SYMROOT=${SOURCE_ROOT}/../build/ios" -export "OTHER_LDFLAGS=$(inherited) -framework Flutter" -export "FLUTTER_FRAMEWORK_DIR=/opt/flutter/bin/cache/artifacts/engine/ios" export "FLUTTER_BUILD_NAME=1.0.0" export "FLUTTER_BUILD_NUMBER=1" export "DART_OBFUSCATION=false" diff --git a/pubspec.yaml b/pubspec.yaml index 4ac08fda..5319cec8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: video_compress description: Light library of video manipulation of Flutter. Compress video, remove audio, get video thumbnail from dart code. -version: 2.1.1 +version: 3.0.0 homepage: https://github.com/jonataslaw/VideoCompress environment: @@ -11,9 +11,9 @@ dependencies: flutter: sdk: flutter -dev_dependencies: - flutter_test: - sdk: flutter +# dev_dependencies: +# flutter_test: +# sdk: flutter # For information on the generic Dart part of this file, see the # following page: https://www.dartlang.org/tools/pub/pubspec diff --git a/test/video_compress_test.dart b/test/video_compress_test.dart index 1902b03f..47fc6ea3 100644 --- a/test/video_compress_test.dart +++ b/test/video_compress_test.dart @@ -1,10 +1,9 @@ -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; +// import 'package:flutter/services.dart'; void main() { - const MethodChannel channel = MethodChannel('video_compress'); + // const MethodChannel channel = MethodChannel('video_compress'); - tearDown(() { - channel.setMockMethodCallHandler(null); - }); + // tearDown(() { + // channel.setMockMethodCallHandler(null); + // }); } From 8b9b7f1bee4e19157634950ad6209f5027f524a8 Mon Sep 17 00:00:00 2001 From: felixmo_trustMe Date: Wed, 21 Apr 2021 11:48:58 +0800 Subject: [PATCH 27/44] Bug fix on getMediaInfo --- lib/src/video_compress/video_compressor.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/video_compress/video_compressor.dart b/lib/src/video_compress/video_compressor.dart index d248021a..edef6659 100644 --- a/lib/src/video_compress/video_compressor.dart +++ b/lib/src/video_compress/video_compressor.dart @@ -95,8 +95,8 @@ extension Compress on IVideoCompress { /// debugPrint(info.toJson()); /// ``` Future getMediaInfo(String path) async { - final jsonStr = await (_invoke('getMediaInfo', {'path': path}) as FutureOr); - final jsonMap = json.decode(jsonStr); + final jsonStr = await (_invoke('getMediaInfo', {'path': path})); + final jsonMap = json.decode(jsonStr!); return MediaInfo.fromJson(jsonMap); } From f6f1f76c1be3f70bcabebab9eb2d4f98e19ca61f Mon Sep 17 00:00:00 2001 From: felixmo Date: Mon, 26 Apr 2021 10:00:46 +0800 Subject: [PATCH 28/44] Added explanation of bug fix on getMediaInfo() --- lib/src/video_compress/video_compressor.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/src/video_compress/video_compressor.dart b/lib/src/video_compress/video_compressor.dart index edef6659..f0433eb6 100644 --- a/lib/src/video_compress/video_compressor.dart +++ b/lib/src/video_compress/video_compressor.dart @@ -95,6 +95,8 @@ extension Compress on IVideoCompress { /// debugPrint(info.toJson()); /// ``` Future getMediaInfo(String path) async { + // Not to set the result as strong-mode so that it would have exception to + // lead to the failure of compression final jsonStr = await (_invoke('getMediaInfo', {'path': path})); final jsonMap = json.decode(jsonStr!); return MediaInfo.fromJson(jsonMap); From 51cb8dbc6c8ca2a15b88726aab92d0beddb168c7 Mon Sep 17 00:00:00 2001 From: felixmo Date: Mon, 26 Apr 2021 10:05:18 +0800 Subject: [PATCH 29/44] Removed strong-mode on variable in getFileThumbnail() --- lib/src/video_compress/video_compressor.dart | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/src/video_compress/video_compressor.dart b/lib/src/video_compress/video_compressor.dart index f0433eb6..c9b62b90 100644 --- a/lib/src/video_compress/video_compressor.dart +++ b/lib/src/video_compress/video_compressor.dart @@ -16,6 +16,7 @@ class _VideoCompressImpl extends IVideoCompress { } static _VideoCompressImpl? _instance; + static _VideoCompressImpl get instance { return _instance ??= _VideoCompressImpl._(); } @@ -36,9 +37,7 @@ extension Compress on IVideoCompress { Future _invoke(String name, [Map? params]) async { T? result; try { - result = params != null - ? await channel.invokeMethod(name, params) - : await channel.invokeMethod(name); + result = params != null ? await channel.invokeMethod(name, params) : await channel.invokeMethod(name); } on PlatformException catch (e) { debugPrint('''Error from VideoCompress: Method: $name @@ -74,13 +73,15 @@ extension Compress on IVideoCompress { }) async { assert(quality > 1 || quality < 100); + // Not to set the result as strong-mode so that it would have exception to + // lead to the failure of compression final filePath = await (_invoke('getFileThumbnail', { 'path': path, 'quality': quality, 'position': position, - }) as FutureOr); + })); - final file = File(filePath); + final file = File(filePath!); return file; } From 762d2ce46b9b36802b259e9348b699ed84288b8e Mon Sep 17 00:00:00 2001 From: felixmo_trustMe Date: Tue, 27 Apr 2021 10:44:52 +0800 Subject: [PATCH 30/44] Upgrade packages in example and add example for getFileThumbnail --- example/.flutter-plugins-dependencies | 2 +- .../ios/Flutter/flutter_export_environment.sh | 4 +- example/lib/main.dart | 75 +++++++++++-------- example/lib/video_thumbnail.dart | 62 +++++++++++++++ example/pubspec.yaml | 12 +-- 5 files changed, 115 insertions(+), 40 deletions(-) create mode 100644 example/lib/video_thumbnail.dart diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies index afab8a4e..64c0825e 100644 --- a/example/.flutter-plugins-dependencies +++ b/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"image_picker","path":"/Users/jonatasborges/flutter/.pub-cache/hosted/pub.dartlang.org/image_picker-0.7.4/","dependencies":[]},{"name":"video_compress","path":"/Users/jonatasborges/VideoCompress/","dependencies":[]},{"name":"video_player","path":"/Users/jonatasborges/flutter/.pub-cache/hosted/pub.dartlang.org/video_player-0.10.12+5/","dependencies":[]}],"android":[{"name":"flutter_plugin_android_lifecycle","path":"/Users/jonatasborges/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_plugin_android_lifecycle-2.0.1/","dependencies":[]},{"name":"image_picker","path":"/Users/jonatasborges/flutter/.pub-cache/hosted/pub.dartlang.org/image_picker-0.7.4/","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"video_compress","path":"/Users/jonatasborges/VideoCompress/","dependencies":[]},{"name":"video_player","path":"/Users/jonatasborges/flutter/.pub-cache/hosted/pub.dartlang.org/video_player-0.10.12+5/","dependencies":[]}],"macos":[{"name":"file_selector_macos","path":"/Users/jonatasborges/flutter/.pub-cache/hosted/pub.dartlang.org/file_selector_macos-0.0.4/","dependencies":[]},{"name":"video_compress","path":"/Users/jonatasborges/VideoCompress/","dependencies":[]}],"linux":[],"windows":[],"web":[{"name":"file_selector_web","path":"/Users/jonatasborges/flutter/.pub-cache/hosted/pub.dartlang.org/file_selector_web-0.8.1/","dependencies":[]},{"name":"image_picker_for_web","path":"/Users/jonatasborges/flutter/.pub-cache/hosted/pub.dartlang.org/image_picker_for_web-2.0.0/","dependencies":[]},{"name":"video_player_web","path":"/Users/jonatasborges/flutter/.pub-cache/hosted/pub.dartlang.org/video_player_web-0.1.4+1/","dependencies":[]}]},"dependencyGraph":[{"name":"file_selector","dependencies":["file_selector_web"]},{"name":"file_selector_macos","dependencies":[]},{"name":"file_selector_web","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"image_picker","dependencies":["flutter_plugin_android_lifecycle","image_picker_for_web"]},{"name":"image_picker_for_web","dependencies":[]},{"name":"video_compress","dependencies":[]},{"name":"video_player","dependencies":["video_player_web"]},{"name":"video_player_web","dependencies":[]}],"date_created":"2021-03-31 10:22:40.807552","version":"2.0.3"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"image_picker","path":"/Users/felixmo/.pub-cache/hosted/pub.dartlang.org/image_picker-0.7.4/","dependencies":[]},{"name":"video_compress","path":"/Users/felixmo/Desktop/trustME/sample/VideoCompress/","dependencies":[]},{"name":"video_player","path":"/Users/felixmo/.pub-cache/hosted/pub.dartlang.org/video_player-2.1.1/","dependencies":[]}],"android":[{"name":"flutter_plugin_android_lifecycle","path":"/Users/felixmo/.pub-cache/hosted/pub.dartlang.org/flutter_plugin_android_lifecycle-2.0.1/","dependencies":[]},{"name":"image_picker","path":"/Users/felixmo/.pub-cache/hosted/pub.dartlang.org/image_picker-0.7.4/","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"video_compress","path":"/Users/felixmo/Desktop/trustME/sample/VideoCompress/","dependencies":[]},{"name":"video_player","path":"/Users/felixmo/.pub-cache/hosted/pub.dartlang.org/video_player-2.1.1/","dependencies":[]}],"macos":[{"name":"file_selector_macos","path":"/Users/felixmo/.pub-cache/hosted/pub.dartlang.org/file_selector_macos-0.0.4/","dependencies":[]},{"name":"video_compress","path":"/Users/felixmo/Desktop/trustME/sample/VideoCompress/","dependencies":[]}],"linux":[],"windows":[],"web":[{"name":"file_selector_web","path":"/Users/felixmo/.pub-cache/hosted/pub.dartlang.org/file_selector_web-0.8.1/","dependencies":[]},{"name":"image_picker_for_web","path":"/Users/felixmo/.pub-cache/hosted/pub.dartlang.org/image_picker_for_web-2.0.0/","dependencies":[]},{"name":"video_player_web","path":"/Users/felixmo/.pub-cache/hosted/pub.dartlang.org/video_player_web-2.0.0/","dependencies":[]}]},"dependencyGraph":[{"name":"file_selector","dependencies":["file_selector_web"]},{"name":"file_selector_macos","dependencies":[]},{"name":"file_selector_web","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"image_picker","dependencies":["flutter_plugin_android_lifecycle","image_picker_for_web"]},{"name":"image_picker_for_web","dependencies":[]},{"name":"video_compress","dependencies":[]},{"name":"video_player","dependencies":["video_player_web"]},{"name":"video_player_web","dependencies":[]}],"date_created":"2021-04-27 10:34:10.395399","version":"2.0.5"} \ No newline at end of file diff --git a/example/ios/Flutter/flutter_export_environment.sh b/example/ios/Flutter/flutter_export_environment.sh index 6a1c4dce..a7e321b3 100755 --- a/example/ios/Flutter/flutter_export_environment.sh +++ b/example/ios/Flutter/flutter_export_environment.sh @@ -1,7 +1,7 @@ #!/bin/sh # This is a generated file; do not edit or check into version control. -export "FLUTTER_ROOT=/Users/jonatasborges/flutter" -export "FLUTTER_APPLICATION_PATH=/Users/jonatasborges/VideoCompress/example" +export "FLUTTER_ROOT=/Users/felixmo/flutter" +export "FLUTTER_APPLICATION_PATH=/Users/felixmo/Desktop/trustME/sample/VideoCompress/example" export "FLUTTER_TARGET=lib/main.dart" export "FLUTTER_BUILD_DIR=build" export "SYMROOT=${SOURCE_ROOT}/../build/ios" diff --git a/example/lib/main.dart b/example/lib/main.dart index 4db2adc5..16c03b21 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -4,6 +4,8 @@ import 'package:video_compress/video_compress.dart'; import 'package:file_selector/file_selector.dart'; import 'dart:io'; +import './video_thumbnail.dart'; + void main() { runApp(MyApp()); } @@ -22,9 +24,9 @@ class MyApp extends StatelessWidget { } class MyHomePage extends StatefulWidget { - MyHomePage({Key key, this.title}) : super(key: key); + MyHomePage({Key? key, this.title}) : super(key: key); - final String title; + final String? title; @override _MyHomePageState createState() => _MyHomePageState(); @@ -33,11 +35,39 @@ class MyHomePage extends StatefulWidget { class _MyHomePageState extends State { String _counter = "video"; + _compressVideo() async { + var file; + if (Platform.isMacOS) { + final typeGroup = XTypeGroup(label: 'videos', extensions: ['mov', 'mp4']); + file = await openFile(acceptedTypeGroups: [typeGroup]); + } else { + final picker = ImagePicker(); + PickedFile? pickedFile = await picker.getVideo(source: ImageSource.gallery); + file = File(pickedFile!.path); + } + if (file == null) { + return; + } + await VideoCompress.setLogLevel(0); + final MediaInfo? info = await VideoCompress.compressVideo( + file.path, + quality: VideoQuality.MediumQuality, + deleteOrigin: false, + includeAudio: true, + ); + print(info!.path); + if (info != null) { + setState(() { + _counter = info.path!; + }); + } + } + @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(widget.title), + title: Text(widget.title!), ), body: Center( child: Column( @@ -57,38 +87,21 @@ class _MyHomePageState extends State { ), onTap: () { VideoCompress.cancelCompression(); - }) + }), + ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => VideoThumbnail()), + ); + }, + child: Text('Test thumbnail'), + ), ], ), ), floatingActionButton: FloatingActionButton( - onPressed: () async { - var file; - if (Platform.isMacOS) { - final typeGroup = XTypeGroup(label: 'videos', extensions: ['mov', 'mp4']); - file = await openFile(acceptedTypeGroups: [typeGroup]); - } else { - final picker = ImagePicker(); - PickedFile pickedFile = await picker.getVideo(source: ImageSource.gallery); - file = File(pickedFile.path); - } - if (file == null) { - return; - } - await VideoCompress.setLogLevel(0); - final info = await VideoCompress.compressVideo( - file.path, - quality: VideoQuality.MediumQuality, - deleteOrigin: false, - includeAudio: true, - ); - print(info.path); - if (info != null) { - setState(() { - _counter = info.path; - }); - } - }, + onPressed: () async => _compressVideo(), tooltip: 'Increment', child: Icon(Icons.add), ), diff --git a/example/lib/video_thumbnail.dart b/example/lib/video_thumbnail.dart new file mode 100644 index 00000000..eacfefe0 --- /dev/null +++ b/example/lib/video_thumbnail.dart @@ -0,0 +1,62 @@ +import 'dart:io'; + +import 'package:file_selector/file_selector.dart'; +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:video_compress/video_compress.dart'; + +class VideoThumbnail extends StatefulWidget { + @override + _VideoThumbnailState createState() => _VideoThumbnailState(); +} + +class _VideoThumbnailState extends State { + File? _thumbnailFile; + + @override + Widget build(BuildContext context) { + _getVideoThumbnail() async { + var file; + + if (Platform.isMacOS) { + final typeGroup = XTypeGroup(label: 'videos', extensions: ['mov', 'mp4']); + file = await openFile(acceptedTypeGroups: [typeGroup]); + } else { + final picker = ImagePicker(); + PickedFile? pickedFile = await picker.getVideo(source: ImageSource.gallery); + file = File(pickedFile!.path); + } + + if (file != null) { + _thumbnailFile = await VideoCompress.getFileThumbnail(file.path); + setState(() { + print(_thumbnailFile); + }); + } + } + + return Scaffold( + appBar: AppBar(title: Text('File Thumbnail')), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container(child: ElevatedButton(onPressed: _getVideoThumbnail, child: Text('Get File Thumbnail'))), + _buildThumbnail(), + ], + ), + ), + ); + } + + Widget _buildThumbnail() { + if (_thumbnailFile != null) { + return Container( + padding: EdgeInsets.all(20.0), + child: Image(image: FileImage(_thumbnailFile!)), + ); + } + return Container(); + } +} diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 1f93bb97..969e3873 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -3,7 +3,7 @@ description: Demonstrates how to use the video_compress plugin. publish_to: 'none' environment: - sdk: ">=2.2.2 <3.0.0" + sdk: ">=2.12.0 <3.0.0" dependencies: flutter: @@ -11,11 +11,11 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^0.1.2 - image_picker: ^0.7.3 - video_player: ^0.10.1+3 - file_selector: ^0.8.2 # web - file_selector_macos: ^0.0.4 # macos + cupertino_icons: ^1.0.2 + image_picker: ^0.7.4 + video_player: ^2.1.1 + file_selector: ^0.8.2 + file_selector_macos: ^0.0.4 dev_dependencies: flutter_test: From 68757461ca41b7cb3af5d1b3750dce41e24221e8 Mon Sep 17 00:00:00 2001 From: Jin Lee Date: Fri, 2 Jul 2021 21:55:40 +1000 Subject: [PATCH 31/44] Invalidate updateProgress timer after the completion of video export --- ios/Classes/SwiftVideoCompressPlugin.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ios/Classes/SwiftVideoCompressPlugin.swift b/ios/Classes/SwiftVideoCompressPlugin.swift index ad95ed5c..6e120aba 100644 --- a/ios/Classes/SwiftVideoCompressPlugin.swift +++ b/ios/Classes/SwiftVideoCompressPlugin.swift @@ -215,8 +215,8 @@ public class SwiftVideoCompressPlugin: NSObject, FlutterPlugin { userInfo: exporter, repeats: true) exporter.exportAsynchronously(completionHandler: { + timer.invalidate() if(self.stopCommand) { - timer.invalidate() self.stopCommand = false var json = self.getMediaInfoJson(path) json["isCancel"] = true @@ -224,7 +224,6 @@ public class SwiftVideoCompressPlugin: NSObject, FlutterPlugin { return result(jsonString) } if deleteOrigin { - timer.invalidate() let fileManager = FileManager.default do { if fileManager.fileExists(atPath: path) { From ef123afb97023e984b1d4d6601de3accaed3b34a Mon Sep 17 00:00:00 2001 From: Yaniv Shaked Date: Tue, 13 Jul 2021 15:44:19 +0300 Subject: [PATCH 32/44] [Resolution Presets] Support for resolution presets --- .../example/video_compress/VideoCompressPlugin.kt | 12 ++++++++++++ ios/Classes/SwiftVideoCompressPlugin.swift | 8 ++++++++ lib/src/video_compress/video_quality.dart | 11 ++++++++++- macos/Classes/VideoCompressPlugin.swift | 8 ++++++++ 4 files changed, 38 insertions(+), 1 deletion(-) 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 c072b4b3..d8f43de1 100644 --- a/android/src/main/kotlin/com/example/video_compress/VideoCompressPlugin.kt +++ b/android/src/main/kotlin/com/example/video_compress/VideoCompressPlugin.kt @@ -113,6 +113,18 @@ class VideoCompressPlugin : MethodCallHandler, FlutterPlugin { .frameRate(frameRate!!) // will be capped to the input frameRate .build() } + 4 -> { + videoTrackStrategy = DefaultVideoStrategy.atMost(480, 640).build() + } + 5 -> { + videoTrackStrategy = DefaultVideoStrategy.atMost(540, 960).build() + } + 6 -> { + videoTrackStrategy = DefaultVideoStrategy.atMost(720, 1280).build() + } + 7 -> { + videoTrackStrategy = DefaultVideoStrategy.atMost(1080, 1920).build() + } } audioTrackStrategy = if (includeAudio) { diff --git a/ios/Classes/SwiftVideoCompressPlugin.swift b/ios/Classes/SwiftVideoCompressPlugin.swift index ad95ed5c..ba8539ff 100644 --- a/ios/Classes/SwiftVideoCompressPlugin.swift +++ b/ios/Classes/SwiftVideoCompressPlugin.swift @@ -148,6 +148,14 @@ public class SwiftVideoCompressPlugin: NSObject, FlutterPlugin { return AVAssetExportPresetMediumQuality case 3: return AVAssetExportPresetHighestQuality + case 4: + return AVAssetExportPreset640x480 + case 5: + return AVAssetExportPreset960x540 + case 6: + return AVAssetExportPreset1280x720 + case 7: + return AVAssetExportPreset1920x1080 default: return AVAssetExportPresetMediumQuality } diff --git a/lib/src/video_compress/video_quality.dart b/lib/src/video_compress/video_quality.dart index 2b89f418..c24b6058 100644 --- a/lib/src/video_compress/video_quality.dart +++ b/lib/src/video_compress/video_quality.dart @@ -1 +1,10 @@ -enum VideoQuality { DefaultQuality, LowQuality, MediumQuality, HighestQuality } +enum VideoQuality { + DefaultQuality, + LowQuality, + MediumQuality, + HighestQuality, + Res640x480Quality, + Res960x540Quality, + Res1280x720Quality, + Res1920x1080Quality +} diff --git a/macos/Classes/VideoCompressPlugin.swift b/macos/Classes/VideoCompressPlugin.swift index 2e2c016b..e0e674df 100644 --- a/macos/Classes/VideoCompressPlugin.swift +++ b/macos/Classes/VideoCompressPlugin.swift @@ -150,6 +150,14 @@ public class VideoCompressPlugin: NSObject, FlutterPlugin { return AVAssetExportPresetMediumQuality case 3: return AVAssetExportPresetHighestQuality + case 4: + return AVAssetExportPreset640x480 + case 5: + return AVAssetExportPreset960x540 + case 6: + return AVAssetExportPreset1280x720 + case 7: + return AVAssetExportPreset1920x1080 default: return AVAssetExportPresetMediumQuality } From ec8a2df61286b809382b678d98293d74270dab6b Mon Sep 17 00:00:00 2001 From: Yaniv Shaked Date: Fri, 16 Jul 2021 14:35:45 +0300 Subject: [PATCH 33/44] Documentation Fix documentation of `getByteThumbnail` and `getFileThumbnail`: `position` is in milliseconds --- lib/src/video_compress/video_compressor.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/video_compress/video_compressor.dart b/lib/src/video_compress/video_compressor.dart index c9b62b90..01ea3e52 100644 --- a/lib/src/video_compress/video_compressor.dart +++ b/lib/src/video_compress/video_compressor.dart @@ -48,7 +48,7 @@ extension Compress on IVideoCompress { /// getByteThumbnail return [Future], /// quality can be controlled by [quality] from 1 to 100, - /// select the position unit in the video by [position] is seconds + /// select the position unit in the video by [position] is milliseconds Future getByteThumbnail( String path, { int quality = 100, @@ -65,7 +65,7 @@ extension Compress on IVideoCompress { /// getFileThumbnail return [Future] /// quality can be controlled by [quality] from 1 to 100, - /// select the position unit in the video by [position] is seconds + /// select the position unit in the video by [position] is milliseconds Future getFileThumbnail( String path, { int quality = 100, From 81676dc5fe3e86fd84d636f2b9cda92463955fa2 Mon Sep 17 00:00:00 2001 From: Jonny Borges <35742643+jonataslaw@users.noreply.github.com> Date: Fri, 16 Jul 2021 17:16:31 -0300 Subject: [PATCH 34/44] update to 3.1.0 --- CHANGELOG.md | 6 ++ README.md | 59 +------------------ example/.flutter-plugins-dependencies | 2 +- .../ios/Flutter/flutter_export_environment.sh | 5 +- pubspec.yaml | 4 +- 5 files changed, 14 insertions(+), 62 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56c14b35..c4a72aac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 3.1.0 +- Bug fix on getMediaInfo (@trustmefelix) +- Improve getFileThumbnail() (@FelixMoMo/@trustmefelix) +- invalidate updateProgress timer after the completion of video export (@jinthislife) +- Added Support for resolution presets (@yanivshaked) + ## 3.0.0 - Added MacOS support (thank's @efraespada) - Null-safety support (thank's @rlazom feat @leynier) diff --git a/README.md b/README.md index ef4e8da9..1c4681da 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,11 @@ # video_compress - - - All Contributors - - - - Platform - - - Pub Package - - - License: MIT - - - Forks - - - Stars - -
-
Compress videos, remove audio, manipulate thumbnails, and make your video compatible with all platforms through this lightweight and efficient library. 100% native code was used, we do not use FFMPEG as it is very slow, bloated and the GNU license is an obstacle for commercial applications. In addition, google chrome uses VP8/VP9, safari uses h264, and most of the time, it is necessary to encode the video in two formats, but not with this library. All video files are encoded in an MP4 container with AAC audio that allows 100% compatibility with safari, mozila, chrome, android and iOS. -Works on ANDROID and IOS. +Works on ANDROID, IOS and desktop (just MacOS for now). @@ -57,7 +29,7 @@ Add this to your package's `pubspec.yaml` file: ```yaml dependencies: - video_compress: ^2.1.0 + video_compress: ^3.1.0 ``` ### 2. Install it @@ -190,30 +162,3 @@ Contributions are always welcome! Inspired by the flutter_ffmpeg library. https://github.com/rurico/flutter_video_compress -## Contributors ✨ - -Thanks goes to these wonderful people: - - - - -

khainhero

🚧

khainhero

🚧

Andres Gutierrez

🚧

Jonny Borges

💻

Tibout Shaik

🚧

Marco A. Braghim

🚧

Tibout Shaik

🚧

Marco A. Braghim

🚧

ppornkitpras-agoda

🚧

Rafał Płonka

🚧
- - - - - - - - - - - - -

khainhero

🚧

Andres Gutierrez

🚧

Jonny Borges

💻

Tibout Shaik

🚧

Marco A. Braghim

🚧

ppornkitpras-agoda

🚧

Rafał Płonka

🚧

Yaniv Shaked

🚧
- - - - - -This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies index 64c0825e..cec1e232 100644 --- a/example/.flutter-plugins-dependencies +++ b/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"image_picker","path":"/Users/felixmo/.pub-cache/hosted/pub.dartlang.org/image_picker-0.7.4/","dependencies":[]},{"name":"video_compress","path":"/Users/felixmo/Desktop/trustME/sample/VideoCompress/","dependencies":[]},{"name":"video_player","path":"/Users/felixmo/.pub-cache/hosted/pub.dartlang.org/video_player-2.1.1/","dependencies":[]}],"android":[{"name":"flutter_plugin_android_lifecycle","path":"/Users/felixmo/.pub-cache/hosted/pub.dartlang.org/flutter_plugin_android_lifecycle-2.0.1/","dependencies":[]},{"name":"image_picker","path":"/Users/felixmo/.pub-cache/hosted/pub.dartlang.org/image_picker-0.7.4/","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"video_compress","path":"/Users/felixmo/Desktop/trustME/sample/VideoCompress/","dependencies":[]},{"name":"video_player","path":"/Users/felixmo/.pub-cache/hosted/pub.dartlang.org/video_player-2.1.1/","dependencies":[]}],"macos":[{"name":"file_selector_macos","path":"/Users/felixmo/.pub-cache/hosted/pub.dartlang.org/file_selector_macos-0.0.4/","dependencies":[]},{"name":"video_compress","path":"/Users/felixmo/Desktop/trustME/sample/VideoCompress/","dependencies":[]}],"linux":[],"windows":[],"web":[{"name":"file_selector_web","path":"/Users/felixmo/.pub-cache/hosted/pub.dartlang.org/file_selector_web-0.8.1/","dependencies":[]},{"name":"image_picker_for_web","path":"/Users/felixmo/.pub-cache/hosted/pub.dartlang.org/image_picker_for_web-2.0.0/","dependencies":[]},{"name":"video_player_web","path":"/Users/felixmo/.pub-cache/hosted/pub.dartlang.org/video_player_web-2.0.0/","dependencies":[]}]},"dependencyGraph":[{"name":"file_selector","dependencies":["file_selector_web"]},{"name":"file_selector_macos","dependencies":[]},{"name":"file_selector_web","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"image_picker","dependencies":["flutter_plugin_android_lifecycle","image_picker_for_web"]},{"name":"image_picker_for_web","dependencies":[]},{"name":"video_compress","dependencies":[]},{"name":"video_player","dependencies":["video_player_web"]},{"name":"video_player_web","dependencies":[]}],"date_created":"2021-04-27 10:34:10.395399","version":"2.0.5"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"image_picker","path":"/Users/jonatasborges/flutter/.pub-cache/hosted/pub.dartlang.org/image_picker-0.7.4/","dependencies":[]},{"name":"video_compress","path":"/Users/jonatasborges/VideoCompress/","dependencies":[]},{"name":"video_player","path":"/Users/jonatasborges/flutter/.pub-cache/hosted/pub.dartlang.org/video_player-2.1.10/","dependencies":[]}],"android":[{"name":"flutter_plugin_android_lifecycle","path":"/Users/jonatasborges/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_plugin_android_lifecycle-2.0.1/","dependencies":[]},{"name":"image_picker","path":"/Users/jonatasborges/flutter/.pub-cache/hosted/pub.dartlang.org/image_picker-0.7.4/","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"video_compress","path":"/Users/jonatasborges/VideoCompress/","dependencies":[]},{"name":"video_player","path":"/Users/jonatasborges/flutter/.pub-cache/hosted/pub.dartlang.org/video_player-2.1.10/","dependencies":[]}],"macos":[{"name":"file_selector_macos","path":"/Users/jonatasborges/flutter/.pub-cache/hosted/pub.dartlang.org/file_selector_macos-0.0.4/","dependencies":[]},{"name":"video_compress","path":"/Users/jonatasborges/VideoCompress/","dependencies":[]}],"linux":[],"windows":[],"web":[{"name":"file_selector_web","path":"/Users/jonatasborges/flutter/.pub-cache/hosted/pub.dartlang.org/file_selector_web-0.8.1/","dependencies":[]},{"name":"image_picker_for_web","path":"/Users/jonatasborges/flutter/.pub-cache/hosted/pub.dartlang.org/image_picker_for_web-2.0.0/","dependencies":[]},{"name":"video_player_web","path":"/Users/jonatasborges/flutter/.pub-cache/hosted/pub.dartlang.org/video_player_web-2.0.1/","dependencies":[]}]},"dependencyGraph":[{"name":"file_selector","dependencies":["file_selector_web"]},{"name":"file_selector_macos","dependencies":[]},{"name":"file_selector_web","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"image_picker","dependencies":["flutter_plugin_android_lifecycle","image_picker_for_web"]},{"name":"image_picker_for_web","dependencies":[]},{"name":"video_compress","dependencies":[]},{"name":"video_player","dependencies":["video_player_web"]},{"name":"video_player_web","dependencies":[]}],"date_created":"2021-07-16 17:12:24.627421","version":"2.2.3"} \ No newline at end of file diff --git a/example/ios/Flutter/flutter_export_environment.sh b/example/ios/Flutter/flutter_export_environment.sh index a7e321b3..8aa134ee 100755 --- a/example/ios/Flutter/flutter_export_environment.sh +++ b/example/ios/Flutter/flutter_export_environment.sh @@ -1,7 +1,8 @@ #!/bin/sh # This is a generated file; do not edit or check into version control. -export "FLUTTER_ROOT=/Users/felixmo/flutter" -export "FLUTTER_APPLICATION_PATH=/Users/felixmo/Desktop/trustME/sample/VideoCompress/example" +export "FLUTTER_ROOT=/Users/jonatasborges/flutter" +export "FLUTTER_APPLICATION_PATH=/Users/jonatasborges/VideoCompress/example" +export "COCOAPODS_PARALLEL_CODE_SIGN=true" export "FLUTTER_TARGET=lib/main.dart" export "FLUTTER_BUILD_DIR=build" export "SYMROOT=${SOURCE_ROOT}/../build/ios" diff --git a/pubspec.yaml b/pubspec.yaml index 5319cec8..cd27f314 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,11 +1,11 @@ name: video_compress description: Light library of video manipulation of Flutter. Compress video, remove audio, get video thumbnail from dart code. -version: 3.0.0 +version: 3.1.0 homepage: https://github.com/jonataslaw/VideoCompress environment: sdk: '>=2.12.0 <3.0.0' - flutter: ">=1.20.0" + flutter: ">=2.0.0" dependencies: flutter: From 4e1fbe7386d2d05051907cfccf7b179fbc788b53 Mon Sep 17 00:00:00 2001 From: mostafaabdelazim Date: Sun, 1 Aug 2021 13:27:33 +0200 Subject: [PATCH 35/44] upgrade kotlin and gradle plugin versions --- android/build.gradle | 4 ++-- android/gradle/wrapper/gradle-wrapper.properties | 2 +- example/.flutter-plugins-dependencies | 1 - example/.gitignore | 2 ++ example/ios/Flutter/flutter_export_environment.sh | 14 -------------- 5 files changed, 5 insertions(+), 18 deletions(-) delete mode 100644 example/.flutter-plugins-dependencies delete mode 100755 example/ios/Flutter/flutter_export_environment.sh diff --git a/android/build.gradle b/android/build.gradle index c7301cea..b344a43d 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,14 +2,14 @@ group 'com.example.video_compress' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.3.31' + ext.kotlin_version = '1.5.21' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.4.1' + classpath 'com.android.tools.build:gradle:7.0.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 9313387e..aed1030c 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies deleted file mode 100644 index cec1e232..00000000 --- a/example/.flutter-plugins-dependencies +++ /dev/null @@ -1 +0,0 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"image_picker","path":"/Users/jonatasborges/flutter/.pub-cache/hosted/pub.dartlang.org/image_picker-0.7.4/","dependencies":[]},{"name":"video_compress","path":"/Users/jonatasborges/VideoCompress/","dependencies":[]},{"name":"video_player","path":"/Users/jonatasborges/flutter/.pub-cache/hosted/pub.dartlang.org/video_player-2.1.10/","dependencies":[]}],"android":[{"name":"flutter_plugin_android_lifecycle","path":"/Users/jonatasborges/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_plugin_android_lifecycle-2.0.1/","dependencies":[]},{"name":"image_picker","path":"/Users/jonatasborges/flutter/.pub-cache/hosted/pub.dartlang.org/image_picker-0.7.4/","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"video_compress","path":"/Users/jonatasborges/VideoCompress/","dependencies":[]},{"name":"video_player","path":"/Users/jonatasborges/flutter/.pub-cache/hosted/pub.dartlang.org/video_player-2.1.10/","dependencies":[]}],"macos":[{"name":"file_selector_macos","path":"/Users/jonatasborges/flutter/.pub-cache/hosted/pub.dartlang.org/file_selector_macos-0.0.4/","dependencies":[]},{"name":"video_compress","path":"/Users/jonatasborges/VideoCompress/","dependencies":[]}],"linux":[],"windows":[],"web":[{"name":"file_selector_web","path":"/Users/jonatasborges/flutter/.pub-cache/hosted/pub.dartlang.org/file_selector_web-0.8.1/","dependencies":[]},{"name":"image_picker_for_web","path":"/Users/jonatasborges/flutter/.pub-cache/hosted/pub.dartlang.org/image_picker_for_web-2.0.0/","dependencies":[]},{"name":"video_player_web","path":"/Users/jonatasborges/flutter/.pub-cache/hosted/pub.dartlang.org/video_player_web-2.0.1/","dependencies":[]}]},"dependencyGraph":[{"name":"file_selector","dependencies":["file_selector_web"]},{"name":"file_selector_macos","dependencies":[]},{"name":"file_selector_web","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"image_picker","dependencies":["flutter_plugin_android_lifecycle","image_picker_for_web"]},{"name":"image_picker_for_web","dependencies":[]},{"name":"video_compress","dependencies":[]},{"name":"video_player","dependencies":["video_player_web"]},{"name":"video_player_web","dependencies":[]}],"date_created":"2021-07-16 17:12:24.627421","version":"2.2.3"} \ No newline at end of file diff --git a/example/.gitignore b/example/.gitignore index 07488ba6..946c127f 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -8,6 +8,7 @@ .buildlog/ .history .svn/ +.flutter-plugins-dependencies # IntelliJ related *.iml @@ -61,6 +62,7 @@ **/ios/Flutter/flutter_assets/ **/ios/ServiceDefinitions.json **/ios/Runner/GeneratedPluginRegistrant.* +ios/Flutter/flutter_export_environment.sh # Exceptions to above rules. !**/ios/**/default.mode1v3 diff --git a/example/ios/Flutter/flutter_export_environment.sh b/example/ios/Flutter/flutter_export_environment.sh deleted file mode 100755 index 8aa134ee..00000000 --- a/example/ios/Flutter/flutter_export_environment.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/sh -# This is a generated file; do not edit or check into version control. -export "FLUTTER_ROOT=/Users/jonatasborges/flutter" -export "FLUTTER_APPLICATION_PATH=/Users/jonatasborges/VideoCompress/example" -export "COCOAPODS_PARALLEL_CODE_SIGN=true" -export "FLUTTER_TARGET=lib/main.dart" -export "FLUTTER_BUILD_DIR=build" -export "SYMROOT=${SOURCE_ROOT}/../build/ios" -export "FLUTTER_BUILD_NAME=1.0.0" -export "FLUTTER_BUILD_NUMBER=1" -export "DART_OBFUSCATION=false" -export "TRACK_WIDGET_CREATION=false" -export "TREE_SHAKE_ICONS=false" -export "PACKAGE_CONFIG=.packages" From 2d1a3c67593846478c4daa3bace0155f599b28b6 Mon Sep 17 00:00:00 2001 From: Kaique Gazola Date: Wed, 13 Oct 2021 17:30:49 -0300 Subject: [PATCH 36/44] fix issue with whitespaces in iOS --- CHANGELOG.md | 3 +++ ios/Classes/SwiftVideoCompressPlugin.swift | 2 +- ios/Classes/Utility.swift | 4 ++++ pubspec.yaml | 2 +- 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4a72aac..3545936a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 3.1.1 +- Fix issue on iOS with files containing whitespaces + ## 3.1.0 - Bug fix on getMediaInfo (@trustmefelix) - Improve getFileThumbnail() (@FelixMoMo/@trustmefelix) diff --git a/ios/Classes/SwiftVideoCompressPlugin.swift b/ios/Classes/SwiftVideoCompressPlugin.swift index 2880124d..cf70d1bc 100644 --- a/ios/Classes/SwiftVideoCompressPlugin.swift +++ b/ios/Classes/SwiftVideoCompressPlugin.swift @@ -244,7 +244,7 @@ public class SwiftVideoCompressPlugin: NSObject, FlutterPlugin { print(error) } } - var json = self.getMediaInfoJson(compressionUrl.absoluteString) + var json = self.getMediaInfoJson(Utility.excludeEncoding(compressionUrl.path)) json["isCancel"] = false let jsonString = Utility.keyValueToJson(json) result(jsonString) diff --git a/ios/Classes/Utility.swift b/ios/Classes/Utility.swift index 05ee119c..b0c26825 100644 --- a/ios/Classes/Utility.swift +++ b/ios/Classes/Utility.swift @@ -34,6 +34,10 @@ class Utility: NSObject { return path.replacingOccurrences(of: "file://", with: "") } + static func excludeEncoding(_ path: String) -> String{ + return path.removingPercentEncoding! + } + static func keyValueToJson(_ keyAndValue: [String : Any?])->String { let data = try! JSONSerialization.data(withJSONObject: keyAndValue as NSDictionary, options: []) let jsonString = NSString(data:data as Data,encoding: String.Encoding.utf8.rawValue) diff --git a/pubspec.yaml b/pubspec.yaml index cd27f314..3b72041a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: video_compress description: Light library of video manipulation of Flutter. Compress video, remove audio, get video thumbnail from dart code. -version: 3.1.0 +version: 3.1.1 homepage: https://github.com/jonataslaw/VideoCompress environment: From 9de6471c3bb11ca8eda970b3d132ac2ff8bff1c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20Karaka=C5=A1?= <34490121+MilosKarakas@users.noreply.github.com> Date: Tue, 9 Nov 2021 15:03:09 +0100 Subject: [PATCH 37/44] Move to mavenCentral() Move to mavenCentral() and update transcoder. --- android/build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index b344a43d..16283b95 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -5,7 +5,7 @@ buildscript { ext.kotlin_version = '1.5.21' repositories { google() - jcenter() + mavenCentral() } dependencies { @@ -17,7 +17,7 @@ buildscript { rootProject.allprojects { repositories { google() - jcenter() + mavenCentral() } } @@ -41,5 +41,5 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'com.otaliastudios:transcoder:0.9.1' + implementation 'com.otaliastudios:transcoder:0.10.0' } From 537f9f9011678c5e5479092523e286477e81accb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20Karaka=C5=A1?= <34490121+MilosKarakas@users.noreply.github.com> Date: Tue, 9 Nov 2021 15:39:40 +0100 Subject: [PATCH 38/44] Update VideoCompressPlugin.kt Fix --- .../kotlin/com/example/video_compress/VideoCompressPlugin.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 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 d8f43de1..1ad76f6d 100644 --- a/android/src/main/kotlin/com/example/video_compress/VideoCompressPlugin.kt +++ b/android/src/main/kotlin/com/example/video_compress/VideoCompressPlugin.kt @@ -11,10 +11,10 @@ import com.otaliastudios.transcoder.strategy.DefaultAudioStrategy import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy import com.otaliastudios.transcoder.strategy.RemoveTrackStrategy import com.otaliastudios.transcoder.strategy.TrackStrategy -import com.otaliastudios.transcoder.strategy.size.* +import com.otaliastudios.transcoder.common.* import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.BinaryMessenger -import com.otaliastudios.transcoder.internal.Logger +import com.otaliastudios.transcoder.internal.utils.Logger import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler From 8ef39c05342179d028f697092b638d52e85cc252 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20Karaka=C5=A1?= <34490121+MilosKarakas@users.noreply.github.com> Date: Sat, 11 Dec 2021 14:16:59 +0100 Subject: [PATCH 39/44] Update build.gradle --- android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/build.gradle b/android/build.gradle index 16283b95..e82e74ba 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -25,7 +25,7 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdkVersion 29 + compileSdkVersion 31 sourceSets { main.java.srcDirs += 'src/main/kotlin' From bf0e7eeef6c249cdb528f45adbd43e79a855cd1a Mon Sep 17 00:00:00 2001 From: "Siraparapu, Yamuna (Agoda)" Date: Mon, 31 Jan 2022 20:49:00 +0800 Subject: [PATCH 40/44] Update build.gradle --- android/build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 279f61b2..41e12eb7 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,14 +2,14 @@ group 'com.example.video_compress' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.5.21' + ext.kotlin_version = '1.3.31' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.0.0' + classpath 'com.android.tools.build:gradle:3.4.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -25,7 +25,7 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdkVersion 31 + compileSdkVersion 29 sourceSets { main.java.srcDirs += 'src/main/kotlin' From ae2460423a1664dc836feda0c0d7dd1f54bede58 Mon Sep 17 00:00:00 2001 From: "Siraparapu, Yamuna (Agoda)" Date: Mon, 31 Jan 2022 22:40:35 +0800 Subject: [PATCH 41/44] Fix transcoder version --- android/build.gradle | 6 +++--- .../com/example/video_compress/VideoCompressPlugin.kt | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 41e12eb7..f1bd544a 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -5,7 +5,7 @@ buildscript { ext.kotlin_version = '1.3.31' repositories { google() - mavenCentral() + jcenter() } dependencies { @@ -17,7 +17,7 @@ buildscript { rootProject.allprojects { repositories { google() - mavenCentral() + jcenter() } } @@ -44,5 +44,5 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'com.otaliastudios:transcoder:0.10.0' + implementation 'com.otaliastudios:transcoder:0.9.0' } 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 1ad76f6d..d8f43de1 100644 --- a/android/src/main/kotlin/com/example/video_compress/VideoCompressPlugin.kt +++ b/android/src/main/kotlin/com/example/video_compress/VideoCompressPlugin.kt @@ -11,10 +11,10 @@ import com.otaliastudios.transcoder.strategy.DefaultAudioStrategy import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy import com.otaliastudios.transcoder.strategy.RemoveTrackStrategy import com.otaliastudios.transcoder.strategy.TrackStrategy -import com.otaliastudios.transcoder.common.* +import com.otaliastudios.transcoder.strategy.size.* import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.BinaryMessenger -import com.otaliastudios.transcoder.internal.utils.Logger +import com.otaliastudios.transcoder.internal.Logger import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler From 576385a14b617f26d117e3a4aea644142888964a Mon Sep 17 00:00:00 2001 From: "Siraparapu, Yamuna (Agoda)" Date: Tue, 1 Feb 2022 11:20:28 +0800 Subject: [PATCH 42/44] Fix the Kotlin.Unit unsupported value error caused in deleteAllCache() --- .../src/main/kotlin/com/example/video_compress/Utility.kt | 6 +++--- .../com/example/video_compress/VideoCompressPlugin.kt | 4 ++-- example/lib/main.dart | 1 - 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/android/src/main/kotlin/com/example/video_compress/Utility.kt b/android/src/main/kotlin/com/example/video_compress/Utility.kt index f5647097..ff46a1ae 100644 --- a/android/src/main/kotlin/com/example/video_compress/Utility.kt +++ b/android/src/main/kotlin/com/example/video_compress/Utility.kt @@ -127,8 +127,8 @@ class Utility(private val channelName: String) { return fileName } - fun deleteAllCache(context: Context, result: MethodChannel.Result) { + fun deleteAllCache(context: Context): Boolean { val dir = context.getExternalFilesDir("video_compress") - result.success(dir?.deleteRecursively()) + return dir?.deleteRecursively() ?: false } -} \ No newline at end of file +} 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 d8f43de1..1aafcd77 100644 --- a/android/src/main/kotlin/com/example/video_compress/VideoCompressPlugin.kt +++ b/android/src/main/kotlin/com/example/video_compress/VideoCompressPlugin.kt @@ -65,7 +65,7 @@ class VideoCompressPlugin : MethodCallHandler, FlutterPlugin { result.success(Utility(channelName).getMediaInfoJson(context, path!!).toString()) } "deleteAllCache" -> { - result.success(Utility(channelName).deleteAllCache(context, result)); + result.success(Utility(channelName).deleteAllCache(context)); } "setLogLevel" -> { val logLevel = call.argument("logLevel")!! @@ -147,7 +147,7 @@ class VideoCompressPlugin : MethodCallHandler, FlutterPlugin { } - transcodeFuture = Transcoder.into(destPath!!) + transcodeFuture = Transcoder.into(destPath) .addDataSource(dataSource) .setAudioTrackStrategy(audioTrackStrategy) .setVideoTrackStrategy(videoTrackStrategy) diff --git a/example/lib/main.dart b/example/lib/main.dart index 16c03b21..cacbf58e 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -55,7 +55,6 @@ class _MyHomePageState extends State { deleteOrigin: false, includeAudio: true, ); - print(info!.path); if (info != null) { setState(() { _counter = info.path!; From 8f4a1fef328ebea97b9fbb5edecdb3bf483cf517 Mon Sep 17 00:00:00 2001 From: "Ajmera, Udit (Agoda)" Date: Tue, 1 Feb 2022 15:44:10 +0530 Subject: [PATCH 43/44] Rebase to original code --- ios/Classes/SwiftVideoCompressPlugin.swift | 260 --------------------- ios/Classes/Utility.swift | 56 ----- 2 files changed, 316 deletions(-) delete mode 100644 ios/Classes/SwiftVideoCompressPlugin.swift delete mode 100644 ios/Classes/Utility.swift diff --git a/ios/Classes/SwiftVideoCompressPlugin.swift b/ios/Classes/SwiftVideoCompressPlugin.swift deleted file mode 100644 index cf70d1bc..00000000 --- a/ios/Classes/SwiftVideoCompressPlugin.swift +++ /dev/null @@ -1,260 +0,0 @@ -import Flutter -import AVFoundation - -public class SwiftVideoCompressPlugin: NSObject, FlutterPlugin { - private let channelName = "video_compress" - private var exporter: AVAssetExportSession? = nil - private var stopCommand = false - private let channel: FlutterMethodChannel - private let avController = AvController() - - init(channel: FlutterMethodChannel) { - self.channel = channel - } - - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel(name: "video_compress", binaryMessenger: registrar.messenger()) - let instance = SwiftVideoCompressPlugin(channel: channel) - registrar.addMethodCallDelegate(instance, channel: channel) - } - - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - let args = call.arguments as? Dictionary - switch call.method { - case "getByteThumbnail": - let path = args!["path"] as! String - let quality = args!["quality"] as! NSNumber - let position = args!["position"] as! NSNumber - getByteThumbnail(path, quality, position, result) - case "getFileThumbnail": - let path = args!["path"] as! String - let quality = args!["quality"] as! NSNumber - let position = args!["position"] as! NSNumber - getFileThumbnail(path, quality, position, result) - case "getMediaInfo": - let path = args!["path"] as! String - getMediaInfo(path, result) - case "compressVideo": - let path = args!["path"] as! String - let quality = args!["quality"] as! NSNumber - let deleteOrigin = args!["deleteOrigin"] as! Bool - let startTime = args!["startTime"] as? Double - let duration = args!["duration"] as? Double - let includeAudio = args!["includeAudio"] as? Bool - let frameRate = args!["frameRate"] as? Int - compressVideo(path, quality, deleteOrigin, startTime, duration, includeAudio, - frameRate, result) - case "cancelCompression": - cancelCompression(result) - case "deleteAllCache": - Utility.deleteFile(Utility.basePath(), clear: true) - result(true) - case "setLogLevel": - result(true) - default: - result(FlutterMethodNotImplemented) - } - } - - private func getBitMap(_ path: String,_ quality: NSNumber,_ position: NSNumber,_ result: FlutterResult)-> Data? { - let url = Utility.getPathUrl(path) - let asset = avController.getVideoAsset(url) - guard let track = avController.getTrack(asset) else { return nil } - - let assetImgGenerate = AVAssetImageGenerator(asset: asset) - assetImgGenerate.appliesPreferredTrackTransform = true - - let timeScale = CMTimeScale(track.nominalFrameRate) - let time = CMTimeMakeWithSeconds(Float64(truncating: position),preferredTimescale: timeScale) - guard let img = try? assetImgGenerate.copyCGImage(at:time, actualTime: nil) else { - return nil - } - let thumbnail = UIImage(cgImage: img) - let compressionQuality = CGFloat(0.01 * Double(truncating: quality)) - return thumbnail.jpegData(compressionQuality: compressionQuality) - } - - private func getByteThumbnail(_ path: String,_ quality: NSNumber,_ position: NSNumber,_ result: FlutterResult) { - if let bitmap = getBitMap(path,quality,position,result) { - result(bitmap) - } - } - - private func getFileThumbnail(_ path: String,_ quality: NSNumber,_ position: NSNumber,_ result: FlutterResult) { - let fileName = Utility.getFileName(path) - let url = Utility.getPathUrl("\(Utility.basePath())/\(fileName).jpg") - Utility.deleteFile(path) - if let bitmap = getBitMap(path,quality,position,result) { - guard (try? bitmap.write(to: url)) != nil else { - return result(FlutterError(code: channelName,message: "getFileThumbnail error",details: "getFileThumbnail error")) - } - result(Utility.excludeFileProtocol(url.absoluteString)) - } - } - - public func getMediaInfoJson(_ path: String)->[String : Any?] { - let url = Utility.getPathUrl(path) - let asset = avController.getVideoAsset(url) - guard let track = avController.getTrack(asset) else { return [:] } - - let playerItem = AVPlayerItem(url: url) - let metadataAsset = playerItem.asset - - let orientation = avController.getVideoOrientation(path) - - let title = avController.getMetaDataByTag(metadataAsset,key: "title") - let author = avController.getMetaDataByTag(metadataAsset,key: "author") - - let duration = asset.duration.seconds * 1000 - let filesize = track.totalSampleDataLength - - let size = track.naturalSize.applying(track.preferredTransform) - - let width = abs(size.width) - let height = abs(size.height) - - let dictionary = [ - "path":Utility.excludeFileProtocol(path), - "title":title, - "author":author, - "width":width, - "height":height, - "duration":duration, - "filesize":filesize, - "orientation":orientation - ] as [String : Any?] - return dictionary - } - - private func getMediaInfo(_ path: String,_ result: FlutterResult) { - let json = getMediaInfoJson(path) - let string = Utility.keyValueToJson(json) - result(string) - } - - - @objc private func updateProgress(timer:Timer) { - let asset = timer.userInfo as! AVAssetExportSession - if(!stopCommand) { - channel.invokeMethod("updateProgress", arguments: "\(String(describing: asset.progress * 100))") - } - } - - private func getExportPreset(_ quality: NSNumber)->String { - switch(quality) { - case 1: - return AVAssetExportPresetLowQuality - case 2: - return AVAssetExportPresetMediumQuality - case 3: - return AVAssetExportPresetHighestQuality - case 4: - return AVAssetExportPreset640x480 - case 5: - return AVAssetExportPreset960x540 - case 6: - return AVAssetExportPreset1280x720 - case 7: - return AVAssetExportPreset1920x1080 - default: - return AVAssetExportPresetMediumQuality - } - } - - private func getComposition(_ isIncludeAudio: Bool,_ timeRange: CMTimeRange, _ sourceVideoTrack: AVAssetTrack)->AVAsset { - let composition = AVMutableComposition() - if !isIncludeAudio { - let compressionVideoTrack = composition.addMutableTrack(withMediaType: AVMediaType.video, preferredTrackID: kCMPersistentTrackID_Invalid) - compressionVideoTrack!.preferredTransform = sourceVideoTrack.preferredTransform - try? compressionVideoTrack!.insertTimeRange(timeRange, of: sourceVideoTrack, at: CMTime.zero) - } else { - return sourceVideoTrack.asset! - } - - return composition - } - - private func compressVideo(_ path: String,_ quality: NSNumber,_ deleteOrigin: Bool,_ startTime: Double?, - _ duration: Double?,_ includeAudio: Bool?,_ frameRate: Int?, - _ result: @escaping FlutterResult) { - let sourceVideoUrl = Utility.getPathUrl(path) - let sourceVideoType = "mp4" - - let sourceVideoAsset = avController.getVideoAsset(sourceVideoUrl) - let sourceVideoTrack = avController.getTrack(sourceVideoAsset) - - let compressionUrl = - Utility.getPathUrl("\(Utility.basePath())/\(Utility.getFileName(path)).\(sourceVideoType)") - - let timescale = sourceVideoAsset.duration.timescale - let minStartTime = Double(startTime ?? 0) - - let videoDuration = sourceVideoAsset.duration.seconds - let minDuration = Double(duration ?? videoDuration) - let maxDurationTime = minStartTime + minDuration < videoDuration ? minDuration : videoDuration - - let cmStartTime = CMTimeMakeWithSeconds(minStartTime, preferredTimescale: timescale) - let cmDurationTime = CMTimeMakeWithSeconds(maxDurationTime, preferredTimescale: timescale) - let timeRange: CMTimeRange = CMTimeRangeMake(start: cmStartTime, duration: cmDurationTime) - - let isIncludeAudio = includeAudio != nil ? includeAudio! : true - - let session = getComposition(isIncludeAudio, timeRange, sourceVideoTrack!) - - let exporter = AVAssetExportSession(asset: session, presetName: getExportPreset(quality))! - - exporter.outputURL = compressionUrl - exporter.outputFileType = AVFileType.mp4 - exporter.shouldOptimizeForNetworkUse = true - - if frameRate != nil { - let videoComposition = AVMutableVideoComposition(propertiesOf: sourceVideoAsset) - videoComposition.frameDuration = CMTimeMake(value: 1, timescale: Int32(frameRate!)) - exporter.videoComposition = videoComposition - } - - if !isIncludeAudio { - exporter.timeRange = timeRange - } - - Utility.deleteFile(compressionUrl.absoluteString) - - let timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(self.updateProgress), - userInfo: exporter, repeats: true) - - exporter.exportAsynchronously(completionHandler: { - timer.invalidate() - if(self.stopCommand) { - self.stopCommand = false - var json = self.getMediaInfoJson(path) - json["isCancel"] = true - let jsonString = Utility.keyValueToJson(json) - return result(jsonString) - } - if deleteOrigin { - let fileManager = FileManager.default - do { - if fileManager.fileExists(atPath: path) { - try fileManager.removeItem(atPath: path) - } - self.exporter = nil - self.stopCommand = false - } - catch let error as NSError { - print(error) - } - } - var json = self.getMediaInfoJson(Utility.excludeEncoding(compressionUrl.path)) - json["isCancel"] = false - let jsonString = Utility.keyValueToJson(json) - result(jsonString) - }) - } - - private func cancelCompression(_ result: FlutterResult) { - exporter?.cancelExport() - stopCommand = true - result("") - } - -} diff --git a/ios/Classes/Utility.swift b/ios/Classes/Utility.swift deleted file mode 100644 index b0c26825..00000000 --- a/ios/Classes/Utility.swift +++ /dev/null @@ -1,56 +0,0 @@ - -class Utility: NSObject { - static let fileManager = FileManager.default - - static func basePath()->String { - let path = "\(NSTemporaryDirectory())video_compress" - do { - if !fileManager.fileExists(atPath: path) { - try! fileManager.createDirectory(atPath: path, - withIntermediateDirectories: true, attributes: nil) - } - } - return path - } - - static func stripFileExtension(_ fileName:String)->String { - var components = fileName.components(separatedBy: ".") - if components.count > 1 { - components.removeLast() - return components.joined(separator: ".") - } else { - return fileName - } - } - static func getFileName(_ path: String)->String { - return stripFileExtension((path as NSString).lastPathComponent) - } - - static func getPathUrl(_ path: String)->URL { - return URL(fileURLWithPath: excludeFileProtocol(path)) - } - - static func excludeFileProtocol(_ path: String)->String { - return path.replacingOccurrences(of: "file://", with: "") - } - - static func excludeEncoding(_ path: String) -> String{ - return path.removingPercentEncoding! - } - - static func keyValueToJson(_ keyAndValue: [String : Any?])->String { - let data = try! JSONSerialization.data(withJSONObject: keyAndValue as NSDictionary, options: []) - let jsonString = NSString(data:data as Data,encoding: String.Encoding.utf8.rawValue) - return jsonString! as String - } - - static func deleteFile(_ path: String, clear: Bool = false) { - let url = getPathUrl(path) - if fileManager.fileExists(atPath: url.absoluteString) { - try? fileManager.removeItem(at: url) - } - if clear { - try? fileManager.removeItem(at: url) - } - } -} From b8d4a3c77770ab208fdd4346ae8f1726b75e78c9 Mon Sep 17 00:00:00 2001 From: udit Date: Thu, 1 Sep 2022 20:35:52 +0700 Subject: [PATCH 44/44] Updated kotlin version --- android/build.gradle | 2 +- example/android/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index f1bd544a..04ffbe52 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,7 +2,7 @@ group 'com.example.video_compress' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.3.31' + ext.kotlin_version = '1.3.40' repositories { google() jcenter() diff --git a/example/android/build.gradle b/example/android/build.gradle index b811a314..b49af513 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.3.31' + ext.kotlin_version = '1.3.40' repositories { google() jcenter()