diff --git a/.all-contributorsrc b/.all-contributorsrc index ec97979e..d47ad86d 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -31,6 +31,51 @@ "contributions": [ "code" ] + }, + { + "login": "Martibis", + "name": "Tibout Shaik", + "avatar_url": "https://avatars0.githubusercontent.com/u/13279066?v=4", + "profile": "http://www.tiboutshaik.com", + "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" + ] + }, + { + "login": "ppornkitpras-agoda", + "name": "ppornkitpras-agoda", + "avatar_url": "https://avatars3.githubusercontent.com/u/54128106?v=4", + "profile": "https://github.com/ppornkitpras-agoda", + "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" + ] + }, + { + "login": "yanivshaked", + "name": "Yaniv Shaked", + "avatar_url": "https://avatars3.githubusercontent.com/u/13107481?v=4", + "profile": "https://github.com/yanivshaked", + "contributions": [ + "maintenance" + ] } ], "contributorsPerLine": 7, diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b921e07..3545936a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,19 +1,42 @@ -## 0.1.0 -- initial release +## 3.1.1 +- Fix issue on iOS with files containing whitespaces +## 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) -## 0.1.1 -- Change default value to HD +## 3.0.0 +- Added MacOS support (thank's @efraespada) +- Null-safety support (thank's @rlazom feat @leynier) -## 0.1.2 -- Removed unecessary intent when process is done +## 2.1.1 +- Fix Subscription import +- Fix Error on android 10 with no includeAudio option -## 0.1.3 -- added progress listen +## 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 -## 2.0.0 -- refactor code -Breaking changes, call VideoCompress.method directly, without having to instantiate it. +## 0.1.3 +- added progress listen + +## 0.1.2 +- Removed unecessary intent when process is done + +## 0.1.1 +- Change default value to HD + +## 0.1.0 +- initial release \ No newline at end of file diff --git a/README.md b/README.md index 0a178bbb..1c4681da 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,63 @@ # video_compress - -[![All Contributors](https://img.shields.io/badge/all_contributors-3-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. 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). -### 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: ^3.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 @@ -91,19 +138,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 @@ -115,23 +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

🚧

Andres Gutierrez

🚧

Jonny Borges

💻
- - - - - -This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 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/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/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 5f647406..1aafcd77 100644 --- a/android/src/main/kotlin/com/example/video_compress/VideoCompressPlugin.kt +++ b/android/src/main/kotlin/com/example/video_compress/VideoCompressPlugin.kt @@ -5,11 +5,16 @@ 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.PassThroughTrackStrategy +import com.otaliastudios.transcoder.strategy.RemoveTrackStrategy 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 com.otaliastudios.transcoder.internal.Logger import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler @@ -17,17 +22,21 @@ import io.flutter.plugin.common.PluginRegistry.Registrar import java.io.File import java.text.SimpleDateFormat import java.util.* - +import java.util.concurrent.Future /** * VideoCompressPlugin */ class VideoCompressPlugin : MethodCallHandler, FlutterPlugin { + private var _context: Context? = null private var _channel: MethodChannel? = null + private val TAG = "VideoCompressPlugin" + private val LOG = Logger(TAG) + private var transcodeFuture:Future? = null + var channelName = "video_compress" - val channelName = "video_compress" override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { val context = _context; val channel = _channel; @@ -56,11 +65,16 @@ 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")!! + Logger.setLogLevel(logLevel) + 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")!! @@ -68,53 +82,75 @@ class VideoCompressPlugin : MethodCallHandler, FlutterPlugin { val deleteOrigin = call.argument("deleteOrigin")!! val startTime = call.argument("startTime") val duration = call.argument("duration") - val includeAudio = call.argument("includeAudio") - - // Transcoder will automatically cap this framerate to input framerate - val frameRate = call.argument("frameRate") ?: 30 + 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 val out = SimpleDateFormat("yyyy-MM-dd hh-mm-ss").format(Date()) val destPath: String = tempDir + File.separator + "VID_" + out + ".mp4" - val strategy: TrackStrategy + var videoTrackStrategy: TrackStrategy = DefaultVideoStrategy.atMost(340).build(); + val audioTrackStrategy: TrackStrategy when (quality) { - // LowQuality + + 0 -> { + videoTrackStrategy = DefaultVideoStrategy.atMost(720).build() + } + 1 -> { - strategy = DefaultVideoStrategy.atMost(240) - .frameRate(frameRate) - .build() + videoTrackStrategy = DefaultVideoStrategy.atMost(360).build() } - // MediumQuality 2 -> { - strategy = DefaultVideoStrategy.atMost(480) - .frameRate(frameRate) - .build() + videoTrackStrategy = DefaultVideoStrategy.atMost(640).build() } - // HighQuality 3 -> { - strategy = DefaultVideoStrategy.Builder() + + assert(value = frameRate != null) + videoTrackStrategy = DefaultVideoStrategy.Builder() .keyFrameInterval(3f) .bitRate(1280 * 720 * 4.toLong()) - .frameRate(frameRate) + .frameRate(frameRate!!) // will be capped to the input frameRate .build() } - // DefaultQuality - else -> { - strategy = DefaultVideoStrategy.atMost(720) - .frameRate(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) { + val sampleRate = DefaultAudioStrategy.SAMPLE_RATE_AS_INPUT + val channels = DefaultAudioStrategy.CHANNELS_AS_INPUT + + DefaultAudioStrategy.builder() + .channels(channels) + .sampleRate(sampleRate) + .build() + } else { + 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)) } - Transcoder.into(destPath!!) - .addDataSource(context, Uri.parse(path)) - .setVideoTrackStrategy(strategy) - // Don't transcode audio to workaround - // https://github.com/natario1/Transcoder/issues/102 - .setAudioTrackStrategy(PassThroughTrackStrategy()) + transcodeFuture = Transcoder.into(destPath) + .addDataSource(dataSource) + .setAudioTrackStrategy(audioTrackStrategy) + .setVideoTrackStrategy(videoTrackStrategy) .setListener(object : TranscoderListener { override fun onTranscodeProgress(progress: Double) { channel.invokeMethod("updateProgress", progress * 100.00) diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies deleted file mode 100644 index 2de573c7..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":"/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 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/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() diff --git a/example/ios/Flutter/flutter_export_environment.sh b/example/ios/Flutter/flutter_export_environment.sh deleted file mode 100755 index 3e9f82f6..00000000 --- a/example/ios/Flutter/flutter_export_environment.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/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_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" diff --git a/example/lib/main.dart b/example/lib/main.dart index 207eb123..cacbf58e 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,31 +1,21 @@ -import 'dart:io'; - import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:image_picker/image_picker.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:video_compress/video_compress.dart'; +import 'package:file_selector/file_selector.dart'; +import 'dart:io'; + +import './video_thumbnail.dart'; void main() { runApp(MyApp()); } 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'), @@ -34,149 +24,86 @@ class MyApp extends StatelessWidget { } class MyHomePage extends StatefulWidget { - MyHomePage({Key key, this.title}) : super(key: key); + 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; + final String? title; @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State { - String _status = "init"; - String _outputPath = ""; - String _outputFileSize = ""; + String _counter = "video"; - Future updateOutputState() async { - final outputFile = File(_outputPath); - if (await outputFile.exists()) { - _outputFileSize = (await outputFile.length()).toString(); + _compressVideo() async { + var file; + if (Platform.isMacOS) { + final typeGroup = XTypeGroup(label: 'videos', extensions: ['mov', 'mp4']); + file = await openFile(acceptedTypeGroups: [typeGroup]); } else { - _outputFileSize = "-1"; + final picker = ImagePicker(); + PickedFile? pickedFile = await picker.getVideo(source: ImageSource.gallery); + file = File(pickedFile!.path); } - } - - Future compressVideo(String path) async { - _status = 'compressing'; - setState(() {}); - - final info = await VideoCompress.compressVideo( - path, + if (file == null) { + return; + } + await VideoCompress.setLogLevel(0); + final MediaInfo? info = await VideoCompress.compressVideo( + file.path, quality: VideoQuality.MediumQuality, deleteOrigin: false, + includeAudio: true, ); - - _status = 'compressed'; - _outputPath = info.path; - await updateOutputState(); - setState(() {}); + if (info != null) { + setState(() { + _counter = info.path!; + }); + } } @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), + 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( - 'Status:', - ), - Text( - '$_status', - style: Theme.of(context).textTheme.bodyText1, - key: Key('status'), + 'You have pushed the button this many times:', ), Text( - 'Output Path:', + '$_counter', + style: Theme.of(context).textTheme.headline4, ), - Text( - '$_outputPath', - style: Theme.of(context).textTheme.bodyText1, - key: Key('output_path'), - ), - Text( - 'Output File Size:', - ), - Text( - '$_outputFileSize', - style: Theme.of(context).textTheme.bodyText1, - key: Key('output_file_size'), - ), - RaisedButton( - key: Key('use_sample_video'), - onPressed: () async { - final data = await rootBundle.load( - 'assets/samples/sample.mp4', + InkWell( + child: Icon( + Icons.cancel, + size: 55, + ), + onTap: () { + VideoCompress.cancelCompression(); + }), + ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => VideoThumbnail()), ); - final bytes = data.buffer.asUint8List(); - final dir = await getTemporaryDirectory(); - final file = - await File('${dir.path}/sample.mp4').writeAsBytes(bytes); - - await this.compressVideo(file.path); }, - child: const Text('Use sample video'), - ), - RaisedButton( - key: Key('select_video'), - onPressed: () async { - File file = - await ImagePicker.pickVideo(source: ImageSource.gallery); - await this.compressVideo(file.path); - }, - child: const Text('Select video'), - ), - RaisedButton( - key: Key('clear_cache'), - onPressed: () async { - await VideoCompress.deleteAllCache(); - - _status = 'cache cleared'; - await this.updateOutputState(); - setState(() {}); - }, - child: const Text('Clear cache'), + child: Text('Test thumbnail'), ), ], ), ), + floatingActionButton: FloatingActionButton( + 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/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 00000000..3c4935a7 Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ 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 00000000..ed4cc164 Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 00000000..483be613 Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ 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 00000000..bcbf36df Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 00000000..9c0a6528 Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ 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 00000000..e71a7261 Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 00000000..8a31fe2d Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/example/macos/Runner/Base.lproj/MainMenu.xib b/example/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 00000000..537341ab --- /dev/null +++ b/example/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,339 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 177f7414..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,10 +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.6.0+10 - video_player: ^0.10.1+3 - path_provider: + 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: @@ -23,11 +24,6 @@ dev_dependencies: video_compress: path: ../ - flutter_driver: - sdk: flutter - - test: any - # For information on the generic Dart part of this file, see the # following page: https://www.dartlang.org/tools/pub/pubspec @@ -40,8 +36,9 @@ flutter: uses-material-design: true # To add assets to your application, add an assets section, like this: - assets: - - assets/samples/ + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware. 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 79% rename from lib/src/media_info.dart rename to lib/src/media/media_info.dart index 3277bd66..0801ac76 100644 --- a/lib/src/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/compress_mixin.dart b/lib/src/progress_callback/compress_mixin.dart new file mode 100644 index 00000000..916a8cb4 --- /dev/null +++ b/lib/src/progress_callback/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(_progressCallback); + } + + MethodChannel get channel => _channel; + + bool _isCompressing = false; + + bool get isCompressing => _isCompressing; + + @protected + void setProcessingStatus(bool status) { + _isCompressing = status; + } + + Future _progressCallback(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/subscription.dart b/lib/src/progress_callback/subscription.dart new file mode 100644 index 00000000..251a63ca --- /dev/null +++ b/lib/src/progress_callback/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 52% rename from lib/src/video_compressor.dart rename to lib/src/video_compress/video_compressor.dart index c23a40f7..01ea3e52 100644 --- a/lib/src/video_compressor.dart +++ b/lib/src/video_compress/video_compressor.dart @@ -2,47 +2,42 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; -import 'dart:ui'; -import 'video_quality.dart'; +import '../progress_callback/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); - } + static _VideoCompressImpl? _instance; - static const _channel = const MethodChannel('video_compress'); + static _VideoCompressImpl get instance { + return _instance ??= _VideoCompressImpl._(); + } - /// 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 { - T result; + 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 @@ -53,13 +48,12 @@ 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( + /// select the position unit in the video by [position] is milliseconds + Future getByteThumbnail( String path, { int quality = 100, int position = -1, }) async { - assert(path != null); assert(quality > 1 || quality < 100); return await _invoke('getByteThumbnail', { @@ -71,22 +65,23 @@ 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( + /// select the position unit in the video by [position] is milliseconds + Future getFileThumbnail( String path, { int quality = 100, int position = -1, }) async { - assert(path != null); assert(quality > 1 || quality < 100); - final filePath = await _invoke('getFileThumbnail', { + // 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, - }); + })); - final file = File(filePath); + final file = File(filePath!); return file; } @@ -100,10 +95,11 @@ class VideoCompress { /// final info = await _flutterVideoCompress.getMediaInfo(file.path); /// debugPrint(info.toJson()); /// ``` - static Future getMediaInfo(String path) async { - assert(path != null); - final jsonStr = await _invoke('getMediaInfo', {'path': path}); - final jsonMap = json.decode(jsonStr); + 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); } @@ -122,26 +118,27 @@ class VideoCompress { /// ); /// debugPrint(info.toJson()); /// ``` - static 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) { + 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.'''); } + // ignore: invalid_use_of_protected_member + setProcessingStatus(true); final jsonStr = await _invoke('compressVideo', { 'path': path, 'quality': quality.index, @@ -151,45 +148,33 @@ class VideoCompress { 'includeAudio': includeAudio, 'frameRate': frameRate, }); - _isCompressing = false; - final jsonMap = json.decode(jsonStr); - return MediaInfo.fromJson(jsonMap); + + // ignore: invalid_use_of_protected_member + setProcessingStatus(false); + + if (jsonStr != null) { + final jsonMap = json.decode(jsonStr); + return MediaInfo.fromJson(jsonMap); + } else { + return null; + } } /// 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'); } -} -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(); + Future setLogLevel(int logLevel) async { + return await _invoke('setLogLevel', { + 'logLevel': logLevel, }); } } - -class Subscription { - final VoidCallback unsubscribe; - const Subscription(this.unsubscribe); -} diff --git a/lib/src/video_compress/video_quality.dart b/lib/src/video_compress/video_quality.dart new file mode 100644 index 00000000..c24b6058 --- /dev/null +++ b/lib/src/video_compress/video_quality.dart @@ -0,0 +1,10 @@ +enum VideoQuality { + DefaultQuality, + LowQuality, + MediumQuality, + HighestQuality, + Res640x480Quality, + Res960x540Quality, + Res1280x720Quality, + Res1920x1080Quality +} diff --git a/lib/src/video_quality.dart b/lib/src/video_quality.dart deleted file mode 100644 index 9dccaf69..00000000 --- a/lib/src/video_quality.dart +++ /dev/null @@ -1,3 +0,0 @@ - - -enum VideoQuality { DefaultQuality, LowQuality, MediumQuality, HighestQuality } diff --git a/lib/video_compress.dart b/lib/video_compress.dart index 4c6203a9..73555043 100644 --- a/lib/video_compress.dart +++ b/lib/video_compress.dart @@ -1,6 +1,7 @@ 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'; +export 'src/progress_callback/subscription.dart'; 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..e0e674df --- /dev/null +++ b/macos/Classes/VideoCompressPlugin.swift @@ -0,0 +1,263 @@ +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 + 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: { + 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 4c4fa43d..3b72041a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,31 +1,33 @@ 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 -homepage: https://github.com/jonataslaw +version: 3.1.1 +homepage: https://github.com/jonataslaw/VideoCompress environment: - sdk: ">=2.0.0 <3.0.0" + sdk: '>=2.12.0 <3.0.0' + flutter: ">=2.0.0" 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 -# 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 + macos: + pluginClass: VideoCompressPlugin # To add assets to your plugin package, add an assets section, like this: # assets: 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); + // }); }