├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── package.json ├── plugin.xml ├── src ├── android │ ├── CustomAndroidFormatStrategy.java │ ├── FileUtils.java │ ├── VideoEditor.java │ └── build.gradle ├── ios │ ├── SDAVAssetExportSession.h │ ├── SDAVAssetExportSession.m │ ├── VideoEditor.h │ └── VideoEditor.m ├── windows │ └── VideoEditorProxy.js └── windows8 │ └── VideoEditorProxy.js ├── typings └── VideoEditor.d.ts └── www ├── VideoEditor.js └── VideoEditorOptions.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | test/www/*.js 3 | *.pbxuser 4 | !default.pbxuser 5 | *.mode1v3 6 | !default.mode1v3 7 | *.mode2v3 8 | !default.mode2v3 9 | *.perspectivev3 10 | !default.perspectivev3 11 | *.xcworkspace 12 | !default.xcworkspace 13 | xcuserdata 14 | .idea -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [1.0.0](https://github.com/jbavari/cordova-plugin-video-editor/tree/1.0.0) (2015-11-11) 4 | [Full Changelog](https://github.com/jbavari/cordova-plugin-video-editor/compare/0.0.6...1.0.0) 5 | 6 | **Implemented enhancements:** 7 | 8 | - trim start point [\#16](https://github.com/jbavari/cordova-plugin-video-editor/issues/16) 9 | 10 | **Closed issues:** 11 | 12 | - VideoEditor reference error [\#31](https://github.com/jbavari/cordova-plugin-video-editor/issues/31) 13 | - Setting trim start [\#28](https://github.com/jbavari/cordova-plugin-video-editor/issues/28) 14 | - Stretched and rotated video when in portrait mode [\#9](https://github.com/jbavari/cordova-plugin-video-editor/issues/9) 15 | 16 | ## [0.0.6](https://github.com/jbavari/cordova-plugin-video-editor/tree/0.0.6) (2015-11-11) 17 | [Full Changelog](https://github.com/jbavari/cordova-plugin-video-editor/compare/0.0.5...0.0.6) 18 | 19 | ## [0.0.5](https://github.com/jbavari/cordova-plugin-video-editor/tree/0.0.5) (2015-11-10) 20 | [Full Changelog](https://github.com/jbavari/cordova-plugin-video-editor/compare/0.0.4...0.0.5) 21 | 22 | **Closed issues:** 23 | 24 | - Selecting Videos from Gallery for transcoding [\#33](https://github.com/jbavari/cordova-plugin-video-editor/issues/33) 25 | - is trimming working any android version? [\#27](https://github.com/jbavari/cordova-plugin-video-editor/issues/27) 26 | - Input files are remove by default after encoding [\#23](https://github.com/jbavari/cordova-plugin-video-editor/issues/23) 27 | - Example project installation or installation instructions? [\#22](https://github.com/jbavari/cordova-plugin-video-editor/issues/22) 28 | - Transcoding not working in Jelly Bean [\#15](https://github.com/jbavari/cordova-plugin-video-editor/issues/15) 29 | - Not valid import com.netcompss.loader.LoadJNI; [\#8](https://github.com/jbavari/cordova-plugin-video-editor/issues/8) 30 | 31 | ## [0.0.4](https://github.com/jbavari/cordova-plugin-video-editor/tree/0.0.4) (2015-11-09) 32 | **Closed issues:** 33 | 34 | - Plugin won't compile with Cordova 5 [\#26](https://github.com/jbavari/cordova-plugin-video-editor/issues/26) 35 | - Error generating manifest, icon value also present in... [\#24](https://github.com/jbavari/cordova-plugin-video-editor/issues/24) 36 | - Consider switching from ffmpeg4android library as it costs $400 [\#20](https://github.com/jbavari/cordova-plugin-video-editor/issues/20) 37 | - File not Found but ... file exists [\#11](https://github.com/jbavari/cordova-plugin-video-editor/issues/11) 38 | - Any Documentation? [\#1](https://github.com/jbavari/cordova-plugin-video-editor/issues/1) 39 | 40 | **Merged pull requests:** 41 | 42 | - Abandon ffmpeg4android\_lib in favor of android-ffmpeg-java [\#13](https://github.com/jbavari/cordova-plugin-video-editor/pull/13) ([rossmartin](https://github.com/rossmartin)) 43 | - Add option to save to gallery \(android\). [\#12](https://github.com/jbavari/cordova-plugin-video-editor/pull/12) ([rossmartin](https://github.com/rossmartin)) 44 | - Add createThumbnail support [\#5](https://github.com/jbavari/cordova-plugin-video-editor/pull/5) ([rossmartin](https://github.com/rossmartin)) 45 | - Updated readme \(VideoEditorPlugin --\> VideoEditor\) [\#4](https://github.com/jbavari/cordova-plugin-video-editor/pull/4) ([rossmartin](https://github.com/rossmartin)) 46 | - Added transcoding support for android and video trimming. [\#3](https://github.com/jbavari/cordova-plugin-video-editor/pull/3) ([rossmartin](https://github.com/rossmartin)) 47 | - Fixed JS errors in plugin files and updated transcoding [\#2](https://github.com/jbavari/cordova-plugin-video-editor/pull/2) ([rossmartin](https://github.com/rossmartin)) 48 | 49 | 50 | 51 | \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Licenses 2 | 3 | ## Android version 4 | 5 | Apache 2.0 6 | 7 | ## iOS version 8 | 9 | MIT 10 | 11 | ## Windows version 12 | 13 | Apache 2.0 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm version](https://badge.fury.io/js/cordova-plugin-video-editor.svg)](https://badge.fury.io/js/cordova-plugin-video-editor) 2 | 3 | This is a cordova plugin to assist in several video editing tasks such as: 4 | 5 | * Transcoding 6 | * Trimming 7 | * Creating thumbnails from a video file (now at a specific time in the video) 8 | * Getting info on a video - width, height, orientation, duration, size, & bitrate. 9 | 10 | After looking at an article on [How Vine Satisfied Its Need for Speed](http://www.technologyreview.com/view/510511/how-vine-satisfies-its-need-for-speed/), it was clear Cordova/Phonegap needed a way to modify videos to be faster for app's that need that speed. 11 | 12 | This plugin will address those concerns, hopefully. 13 | 14 | ## Installation 15 | ``` 16 | cordova plugin add cordova-plugin-video-editor 17 | ``` 18 | `VideoEditor` and `VideoEditorOptions` will be available in the window after deviceready. 19 | 20 | ## Usage 21 | 22 | ### Transcode a video 23 | 24 | ```javascript 25 | // parameters passed to transcodeVideo 26 | VideoEditor.transcodeVideo( 27 | success, // success cb 28 | error, // error cb 29 | { 30 | fileUri: 'file-uri-here', // the path to the video on the device 31 | outputFileName: 'output-name', // the file name for the transcoded video 32 | outputFileType: VideoEditorOptions.OutputFileType.MPEG4, // android is always mp4 33 | optimizeForNetworkUse: VideoEditorOptions.OptimizeForNetworkUse.YES, // ios only 34 | saveToLibrary: true, // optional, defaults to true 35 | maintainAspectRatio: true, // optional (ios only), defaults to true 36 | width: 640, // optional, see note below on width and height 37 | height: 640, // optional, see notes below on width and height 38 | videoBitrate: 1000000, // optional, bitrate in bits, defaults to 9 megabit (9000000) 39 | fps: 30, // optional (android only), defaults to 30 40 | audioChannels: 2, // optional, number of audio channels, defaults: iOS - 2, Android - as is 41 | audioSampleRate: 44100, // optional (ios only), sample rate for the audio, defaults to 44100 42 | audioBitrate: 128000, // optional, audio bitrate for the video in bits, defaults: iOS - 128000 (128 kilobits), Android - as is or 128000 43 | skipVideoTranscodingIfAVC: true, // optional (android only), skip any transcoding actions (conversion/resizing/etc..) if the input video is avc video, defaults to false 44 | progress: function(info) {} // info will be a number from 0 to 100 45 | } 46 | ); 47 | ``` 48 | #### A note on width and height used by transcodeVideo 49 | I recommend setting `maintainAspectRatio` to true. When this option is true you can provide any width/height and the height provided will be used to calculate the new width for the output video. If you set `maintainAspectRatio` false there is a good chance you'll end up with videos that are stretched and/or distorted. Here is the simplified formula used on iOS when `maintainAspectRatio` is true - 50 | ```objective-c 51 | aspectRatio = videoWidth / videoHeight; 52 | outputWidth = height * aspectRatio; 53 | outputHeight = outputWidth / aspectRatio; 54 | ``` 55 | 56 | Android will always use the aspect ratio of the input video to calculate the scaled output width and height. Setting `maintainAspectRatio` on android will make not make a difference. 57 | 58 | If you don't provide width and height to `transcodeVideo` the output video will have the same dimensions as the input video. 59 | 60 | #### transcodeVideo example - 61 | ```javascript 62 | // options used with transcodeVideo function 63 | // VideoEditorOptions is global, no need to declare it 64 | var VideoEditorOptions = { 65 | OptimizeForNetworkUse: { 66 | NO: 0, 67 | YES: 1 68 | }, 69 | OutputFileType: { 70 | M4V: 0, 71 | MPEG4: 1, 72 | M4A: 2, 73 | QUICK_TIME: 3 74 | } 75 | }; 76 | ``` 77 | ```javascript 78 | // this example uses the cordova media capture plugin 79 | navigator.device.capture.captureVideo( 80 | videoCaptureSuccess, 81 | videoCaptureError, 82 | { 83 | limit: 1, 84 | duration: 20 85 | } 86 | ); 87 | 88 | function videoCaptureSuccess(mediaFiles) { 89 | // Wrap this below in a ~100 ms timeout on Android if 90 | // you just recorded the video using the capture plugin. 91 | // For some reason it is not available immediately in the file system. 92 | 93 | var file = mediaFiles[0]; 94 | var videoFileName = 'video-name-here'; // I suggest a uuid 95 | 96 | VideoEditor.transcodeVideo( 97 | videoTranscodeSuccess, 98 | videoTranscodeError, 99 | { 100 | fileUri: file.fullPath, 101 | outputFileName: videoFileName, 102 | outputFileType: VideoEditorOptions.OutputFileType.MPEG4, 103 | optimizeForNetworkUse: VideoEditorOptions.OptimizeForNetworkUse.YES, 104 | saveToLibrary: true, 105 | maintainAspectRatio: true, 106 | width: 640, 107 | height: 640, 108 | videoBitrate: 1000000, // 1 megabit 109 | audioChannels: 2, 110 | audioSampleRate: 44100, 111 | audioBitrate: 128000, // 128 kilobits 112 | progress: function(info) { 113 | console.log('transcodeVideo progress callback, info: ' + info); 114 | } 115 | } 116 | ); 117 | } 118 | 119 | function videoTranscodeSuccess(result) { 120 | // result is the path to the transcoded video on the device 121 | console.log('videoTranscodeSuccess, result: ' + result); 122 | } 123 | 124 | function videoTranscodeError(err) { 125 | console.log('videoTranscodeError, err: ' + err); 126 | } 127 | ``` 128 | 129 | #### Windows Quirks 130 | Windows does not support any of the optional parameters at this time. Specifying them will not cause an error but, there is no functionality behind them. 131 | 132 | ### Trim a Video (iOS only) 133 | ```javascript 134 | VideoEditor.trim( 135 | trimSuccess, 136 | trimFail, 137 | { 138 | fileUri: 'file-uri-here', // path to input video 139 | trimStart: 5, // time to start trimming in seconds 140 | trimEnd: 15, // time to end trimming in seconds 141 | outputFileName: 'output-name', // output file name 142 | progress: function(info) {} // optional, see docs on progress 143 | } 144 | ); 145 | 146 | function trimSuccess(result) { 147 | // result is the path to the trimmed video on the device 148 | console.log('trimSuccess, result: ' + result); 149 | } 150 | 151 | function trimFail(err) { 152 | console.log('trimFail, err: ' + err); 153 | } 154 | ``` 155 | 156 | ### Create a JPEG thumbnail from a video 157 | ```javascript 158 | VideoEditor.createThumbnail( 159 | success, // success cb 160 | error, // error cb 161 | { 162 | fileUri: 'file-uri-here', // the path to the video on the device 163 | outputFileName: 'output-name', // the file name for the JPEG image 164 | atTime: 2, // optional, location in the video to create the thumbnail (in seconds) 165 | width: 320, // optional, width of the thumbnail 166 | height: 480, // optional, height of the thumbnail 167 | quality: 100 // optional, quality of the thumbnail (between 1 and 100) 168 | } 169 | ); 170 | // atTime will default to 0 if not provided 171 | // width and height will be the same as the video input if they are not provided 172 | // quality will default to 100 if not provided 173 | ``` 174 | 175 | ```javascript 176 | // this example uses the cordova media capture plugin 177 | navigator.device.capture.captureVideo( 178 | videoCaptureSuccess, 179 | videoCaptureError, 180 | { 181 | limit: 1, 182 | duration: 20 183 | } 184 | ); 185 | 186 | function videoCaptureSuccess(mediaFiles) { 187 | // Wrap this below in a ~100 ms timeout on Android if 188 | // you just recorded the video using the capture plugin. 189 | // For some reason it is not available immediately in the file system. 190 | 191 | var file = mediaFiles[0]; 192 | var videoFileName = 'video-name-here'; // I suggest a uuid 193 | 194 | VideoEditor.createThumbnail( 195 | createThumbnailSuccess, 196 | createThumbnailError, 197 | { 198 | fileUri: file.fullPath, 199 | outputFileName: videoFileName, 200 | atTime: 2, 201 | width: 320, 202 | height: 480, 203 | quality: 100 204 | } 205 | ); 206 | } 207 | 208 | function createThumbnailSuccess(result) { 209 | // result is the path to the jpeg image on the device 210 | console.log('createThumbnailSuccess, result: ' + result); 211 | } 212 | ``` 213 | 214 | #### A note on width and height used by createThumbnail 215 | The aspect ratio of the thumbnail created will match that of the video input. This means you may not get exactly the width and height dimensions you give to `createThumbnail` for the jpeg. This for your convenience but let us know if it is a problem. I am considering adding a `maintainAspectRatio` option to `createThumbnail` (and when this option is false you might have stretched, square thumbnails :laughing:). 216 | 217 | ### Get info on a video (width, height, orientation, duration, size, & bitrate) 218 | ```javascript 219 | VideoEditor.getVideoInfo( 220 | success, // success cb 221 | error, // error cb 222 | { 223 | fileUri: 'file-uri-here', // the path to the video on the device 224 | } 225 | ); 226 | ``` 227 | 228 | ```javascript 229 | VideoEditor.getVideoInfo( 230 | getVideoInfoSuccess, 231 | getVideoInfoError, 232 | { 233 | fileUri: file.fullPath 234 | } 235 | ); 236 | 237 | function getVideoInfoSuccess(info) { 238 | console.log('getVideoInfoSuccess, info: ' + JSON.stringify(info, null, 2)); 239 | // info is a JSON object with the following properties - 240 | { 241 | width: 1920, 242 | height: 1080, 243 | orientation: 'landscape', // will be portrait or landscape 244 | duration: 3.541, // duration in seconds 245 | size: 6830126, // size of the video in bytes 246 | bitrate: 15429777 // bitrate of the video in bits per second, 247 | videoMediaType: 'video/3gpp' // Media type of the video, android example: 'video/3gpp', ios example: 'avc1', 248 | audioMediaType: 'audio/mp4a-latm' // Media type of the audio track in video, android example: 'audio/mp4a-latm', ios example: 'aac', 249 | } 250 | } 251 | ``` 252 | 253 | ## Android & FFmpeg 254 | FFmpeg has been removed from android for several reasons but mainly for performance. If you still need the old functionality that FFmpeg provided [V1.09](https://github.com/jbavari/cordova-plugin-video-editor/tree/1.0.9) is the last version that will use it. 255 | 256 | ## On iOS 257 | 258 | [iOS Developer AVFoundation Documentation](https://developer.apple.com/library/ios/documentation/AudioVideo/Conceptual/AVFoundationPG/Articles/01_UsingAssets.html#//apple_ref/doc/uid/TP40010188-CH7-SW8) 259 | 260 | [Video compression in AVFoundation](http://www.iphonedevsdk.com/forum/iphone-sdk-development/110246-video-compression-avassetwriter-in-avfoundation.html) 261 | 262 | [AVFoundation slides - tips/tricks](https://speakerdeck.com/bobmccune/composing-and-editing-media-with-av-foundation) 263 | 264 | [AVFoundation slides #2](http://www.slideshare.net/bobmccune/learning-avfoundation) 265 | 266 | [Bob McCune's AVFoundation Editor - ios app example](https://github.com/tapharmonic/AVFoundationEditor) 267 | 268 | [Saving videos after recording videos](http://stackoverflow.com/questions/20902234/save-video-to-library-after-capturing-video-using-phonegap-capturevideo) 269 | 270 | 271 | 272 | ## On Android 273 | 274 | [Android Documentation](http://developer.android.com/guide/appendix/media-formats.html#recommendations) 275 | 276 | [Android Media Stores](http://developer.android.com/reference/android/provider/MediaStore.html#EXTRA_VIDEO_QUALITY) 277 | 278 | [How to Port ffmpeg (the Program) to Android–Ideas and Thoughts](http://www.roman10.net/how-to-port-ffmpeg-the-program-to-androidideas-and-thoughts/) 279 | 280 | [How to Build Android Applications Based on FFmpeg by An Example](http://www.roman10.net/how-to-build-android-applications-based-on-ffmpeg-by-an-example/) 281 | 282 | 283 | ## On Windows 284 | 285 | 286 | ## License 287 | 288 | Android: Apache 2.0 289 | 290 | iOS: MIT 291 | 292 | Windows: Apache 2.0 293 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cordova-plugin-video-editor", 3 | "version": "1.1.3", 4 | "description": "Cordova Video Editor Plugin", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "cordova": { 9 | "id": "cordova-plugin-video-editor", 10 | "platforms": [ 11 | "android", 12 | "ios" 13 | ] 14 | }, 15 | "keywords": [ 16 | "cordova", 17 | "video", 18 | "editing", 19 | "editor", 20 | "cordova", 21 | "ecosystem:cordova", 22 | "cordova-android", 23 | "cordova-ios" 24 | ], 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/jbavari/cordova-plugin-video-editor.git" 28 | }, 29 | "contributors": [ 30 | { 31 | "name": "Josh Bavari", 32 | "web": "https://twitter.com/jbavari", 33 | "email": "jbavari@gmail.com" 34 | }, 35 | { 36 | "name": "Ross Martin", 37 | "web": "https://twitter.com/MountainDoofus", 38 | "email": "rmartin311@gmail.com" 39 | } 40 | ], 41 | "keywords": [], 42 | "author": "Josh Bavari", 43 | "license": "MIT", 44 | "bugs": { 45 | "url": "https://github.com/jbavari/cordova-plugin-video-editor/issues" 46 | }, 47 | "homepage": "https://github.com/jbavari/cordova-plugin-video-editor#readme" 48 | } 49 | -------------------------------------------------------------------------------- /plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | VideoEditor 4 | A plugin to assist in video editing tasks 5 | cordova,video,editing,transcoding,encoding 6 | https://github.com/jbavari/cordova-plugin-video-editor.git 7 | MIT for iOS, GPL for Android, Apache 2.0 for Windows 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /src/android/CustomAndroidFormatStrategy.java: -------------------------------------------------------------------------------- 1 | package org.apache.cordova.videoeditor; 2 | 3 | import android.media.MediaCodecInfo; 4 | import android.media.MediaFormat; 5 | import android.util.Log; 6 | 7 | import net.ypresto.androidtranscoder.format.MediaFormatExtraConstants; 8 | import net.ypresto.androidtranscoder.format.MediaFormatStrategy; 9 | import net.ypresto.androidtranscoder.format.OutputFormatUnavailableException; 10 | 11 | /** 12 | * Created by ehmm on 02.05.2016. 13 | * 14 | * 15 | */ 16 | public class CustomAndroidFormatStrategy implements MediaFormatStrategy { 17 | public static final int AUDIO_BITRATE_AS_IS = -1; 18 | public static final int AUDIO_CHANNELS_AS_IS = -1; 19 | public static final int DEFAULT_VIDEO_BITRATE = 9000000; 20 | public static final int DEFAULT_FRAMERATE = 30; 21 | public static final int DEFAULT_WIDTH = 0; 22 | public static final int DEFAULT_HEIGHT = 0; 23 | public static final int DEFAULT_AUDIO_BITRATE = 128000; 24 | public static final boolean DEFAULT_SKIP_AVC_VIDEO_TRANSCODING = false; 25 | private static final String TAG = "CustomFormatStrategy"; 26 | private final int mVideoBitrate; 27 | private final int mFrameRate; 28 | private final int width; 29 | private final int height; 30 | private final int mAudioBitrate; 31 | private final int mAudioChannels; 32 | private final boolean mSkipVideoTranscodingIfAVC; 33 | 34 | public CustomAndroidFormatStrategy() { 35 | this.mVideoBitrate = DEFAULT_VIDEO_BITRATE; 36 | this.mFrameRate = DEFAULT_FRAMERATE; 37 | this.width = DEFAULT_WIDTH; 38 | this.height = DEFAULT_HEIGHT; 39 | this.mAudioBitrate = AUDIO_BITRATE_AS_IS; 40 | this.mAudioChannels = AUDIO_CHANNELS_AS_IS; 41 | this.mSkipVideoTranscodingIfAVC = DEFAULT_SKIP_AVC_VIDEO_TRANSCODING; 42 | } 43 | 44 | public CustomAndroidFormatStrategy(final int videoBitrate, 45 | final int frameRate, 46 | final int width, 47 | final int height, 48 | final int audioBitrate, 49 | final int audioChannels, 50 | final boolean skipVideoTranscodingIfAVC) { 51 | this.mVideoBitrate = videoBitrate; 52 | this.mFrameRate = frameRate; 53 | this.width = width; 54 | this.height = height; 55 | this.mAudioBitrate = audioBitrate; 56 | this.mAudioChannels = audioChannels; 57 | this.mSkipVideoTranscodingIfAVC = skipVideoTranscodingIfAVC; 58 | } 59 | 60 | public MediaFormat createVideoOutputFormat(MediaFormat inputFormat) { 61 | boolean isAVCVideoFormat = inputFormat.getString(MediaFormat.KEY_MIME).equals(MediaFormatExtraConstants.MIMETYPE_VIDEO_AVC); 62 | if (isAVCVideoFormat && mSkipVideoTranscodingIfAVC) { 63 | return null; 64 | } 65 | 66 | int inWidth = inputFormat.getInteger(MediaFormat.KEY_WIDTH); 67 | int inHeight = inputFormat.getInteger(MediaFormat.KEY_HEIGHT); 68 | int inLonger, inShorter, outWidth, outHeight, outLonger; 69 | double aspectRatio; 70 | 71 | if (this.width >= this.height) { 72 | outLonger = this.width; 73 | } else { 74 | outLonger = this.height; 75 | } 76 | 77 | if (inWidth >= inHeight) { 78 | inLonger = inWidth; 79 | inShorter = inHeight; 80 | 81 | } else { 82 | inLonger = inHeight; 83 | inShorter = inWidth; 84 | 85 | } 86 | 87 | if (inLonger > outLonger && outLonger > 0) { 88 | if (inWidth >= inHeight) { 89 | aspectRatio = (double) inLonger / (double) inShorter; 90 | outWidth = outLonger; 91 | outHeight = Double.valueOf(outWidth / aspectRatio).intValue(); 92 | 93 | } else { 94 | aspectRatio = (double) inLonger / (double) inShorter; 95 | outHeight = outLonger; 96 | outWidth = Double.valueOf(outHeight / aspectRatio).intValue(); 97 | } 98 | } else { 99 | outWidth = inWidth; 100 | outHeight = inHeight; 101 | } 102 | 103 | MediaFormat format = MediaFormat.createVideoFormat("video/avc", outWidth, outHeight); 104 | format.setInteger(MediaFormat.KEY_BIT_RATE, mVideoBitrate); 105 | format.setInteger(MediaFormat.KEY_FRAME_RATE, mFrameRate); 106 | format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 3); 107 | format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); 108 | 109 | return format; 110 | 111 | } 112 | 113 | public MediaFormat createAudioOutputFormat(MediaFormat inputFormat) { 114 | boolean isAACAudioFormat = inputFormat.getString(MediaFormat.KEY_MIME).equals(MediaFormatExtraConstants.MIMETYPE_AUDIO_AAC); 115 | if (mAudioBitrate == AUDIO_BITRATE_AS_IS && mAudioChannels == AUDIO_CHANNELS_AS_IS && isAACAudioFormat) return null; 116 | 117 | int audioBitrate = mAudioBitrate; 118 | if (audioBitrate == AUDIO_BITRATE_AS_IS) { 119 | audioBitrate = DEFAULT_AUDIO_BITRATE; 120 | } 121 | 122 | int audioChannels = mAudioChannels; 123 | if (audioChannels == AUDIO_CHANNELS_AS_IS) { 124 | audioChannels = inputFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT); 125 | } 126 | 127 | // Use original sample rate, as resampling is not supported yet. 128 | final MediaFormat format = MediaFormat.createAudioFormat(MediaFormatExtraConstants.MIMETYPE_AUDIO_AAC, 129 | inputFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE), audioChannels); 130 | format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC); 131 | format.setInteger(MediaFormat.KEY_BIT_RATE, audioBitrate); 132 | return format; 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /src/android/FileUtils.java: -------------------------------------------------------------------------------- 1 | package org.apache.cordova.videoeditor; 2 | 3 | /* https://github.com/coltoscosmin/FileUtils */ 4 | 5 | /* 6 | * Copyright (C) 2018 OpenIntents.org 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | 21 | import android.content.ContentUris; 22 | import android.content.Context; 23 | import android.content.Intent; 24 | import android.database.Cursor; 25 | import android.database.DatabaseUtils; 26 | import android.net.Uri; 27 | import android.os.Build; 28 | import android.os.Environment; 29 | import android.provider.DocumentsContract; 30 | import android.provider.MediaStore; 31 | import android.provider.OpenableColumns; 32 | 33 | import android.util.Log; 34 | import android.webkit.MimeTypeMap; 35 | 36 | import java.io.BufferedOutputStream; 37 | import java.io.File; 38 | import java.io.FileFilter; 39 | import java.io.FileInputStream; 40 | import java.io.FileOutputStream; 41 | import java.io.IOException; 42 | import java.io.InputStream; 43 | import java.io.OutputStream; 44 | import java.text.DecimalFormat; 45 | import java.util.Comparator; 46 | 47 | public class FileUtils { 48 | public static final String DOCUMENTS_DIR = "documents"; 49 | // configured android:authorities in AndroidManifest (https://developer.android.com/reference/androidx.core.content.FileProvider) 50 | public static final String AUTHORITY = "SAMPLE_AUTHORITY.provider"; 51 | public static final String HIDDEN_PREFIX = "."; 52 | /** 53 | * TAG for log messages. 54 | */ 55 | static final String TAG = "FileUtils"; 56 | private static final boolean DEBUG = false; // Set to true to enable logging 57 | /** 58 | * File and folder comparator. TODO Expose sorting option method 59 | */ 60 | public static Comparator sComparator = (f1, f2) -> { 61 | // Sort alphabetically by lower case, which is much cleaner 62 | return f1.getName().toLowerCase().compareTo( 63 | f2.getName().toLowerCase()); 64 | }; 65 | /** 66 | * File (not directories) filter. 67 | */ 68 | public static FileFilter sFileFilter = file -> { 69 | final String fileName = file.getName(); 70 | // Return files only (not directories) and skip hidden files 71 | return file.isFile() && !fileName.startsWith(HIDDEN_PREFIX); 72 | }; 73 | /** 74 | * Folder (directories) filter. 75 | */ 76 | public static FileFilter sDirFilter = file -> { 77 | final String fileName = file.getName(); 78 | // Return directories only and skip hidden directories 79 | return file.isDirectory() && !fileName.startsWith(HIDDEN_PREFIX); 80 | }; 81 | 82 | private FileUtils() { 83 | } //private constructor to enforce Singleton pattern 84 | 85 | /** 86 | * Gets the extension of a file name, like ".png" or ".jpg". 87 | * 88 | * @param uri 89 | * @return Extension including the dot("."); "" if there is no extension; 90 | * null if uri was null. 91 | */ 92 | public static String getExtension(String uri) { 93 | if (uri == null) { 94 | return null; 95 | } 96 | 97 | int dot = uri.lastIndexOf("."); 98 | if (dot >= 0) { 99 | return uri.substring(dot); 100 | } else { 101 | // No extension. 102 | return ""; 103 | } 104 | } 105 | 106 | /** 107 | * @return Whether the URI is a local one. 108 | */ 109 | public static boolean isLocal(String url) { 110 | return url != null && !url.startsWith("http://") && !url.startsWith("https://"); 111 | } 112 | 113 | /** 114 | * @return True if Uri is a MediaStore Uri. 115 | * @author paulburke 116 | */ 117 | public static boolean isMediaUri(Uri uri) { 118 | return "media".equalsIgnoreCase(uri.getAuthority()); 119 | } 120 | 121 | /** 122 | * Convert File into Uri. 123 | * 124 | * @param file 125 | * @return uri 126 | */ 127 | public static Uri getUri(File file) { 128 | return (file != null) ? Uri.fromFile(file) : null; 129 | } 130 | 131 | /** 132 | * Returns the path only (without file name). 133 | * 134 | * @param file 135 | * @return 136 | */ 137 | public static File getPathWithoutFilename(File file) { 138 | if (file != null) { 139 | if (file.isDirectory()) { 140 | // no file to be split off. Return everything 141 | return file; 142 | } else { 143 | String filename = file.getName(); 144 | String filepath = file.getAbsolutePath(); 145 | 146 | // Construct path without file name. 147 | String pathwithoutname = filepath.substring(0, 148 | filepath.length() - filename.length()); 149 | if (pathwithoutname.endsWith("/")) { 150 | pathwithoutname = pathwithoutname.substring(0, pathwithoutname.length() - 1); 151 | } 152 | return new File(pathwithoutname); 153 | } 154 | } 155 | return null; 156 | } 157 | 158 | /** 159 | * @return The MIME type for the given file. 160 | */ 161 | public static String getMimeType(File file) { 162 | 163 | String extension = getExtension(file.getName()); 164 | 165 | if (extension.length() > 0) 166 | return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.substring(1)); 167 | 168 | return "application/octet-stream"; 169 | } 170 | 171 | /** 172 | * @return The MIME type for the give Uri. 173 | */ 174 | public static String getMimeType(Context context, Uri uri) { 175 | File file = new File(getPath(context, uri)); 176 | return getMimeType(file); 177 | } 178 | 179 | /** 180 | * @return The MIME type for the give String Uri. 181 | */ 182 | public static String getMimeType(Context context, String url) { 183 | String type = context.getContentResolver().getType(Uri.parse(url)); 184 | if (type == null) { 185 | type = "application/octet-stream"; 186 | } 187 | return type; 188 | } 189 | 190 | /** 191 | * @param uri The Uri to check. 192 | * @return Whether the Uri authority is local. 193 | */ 194 | public static boolean isLocalStorageDocument(Uri uri) { 195 | return AUTHORITY.equals(uri.getAuthority()); 196 | } 197 | 198 | /** 199 | * @param uri The Uri to check. 200 | * @return Whether the Uri authority is ExternalStorageProvider. 201 | */ 202 | public static boolean isExternalStorageDocument(Uri uri) { 203 | return "com.android.externalstorage.documents".equals(uri.getAuthority()); 204 | } 205 | 206 | /** 207 | * @param uri The Uri to check. 208 | * @return Whether the Uri authority is DownloadsProvider. 209 | */ 210 | public static boolean isDownloadsDocument(Uri uri) { 211 | return "com.android.providers.downloads.documents".equals(uri.getAuthority()); 212 | } 213 | 214 | /** 215 | * @param uri The Uri to check. 216 | * @return Whether the Uri authority is MediaProvider. 217 | */ 218 | public static boolean isMediaDocument(Uri uri) { 219 | return "com.android.providers.media.documents".equals(uri.getAuthority()); 220 | } 221 | 222 | /** 223 | * @param uri The Uri to check. 224 | * @return Whether the Uri authority is Google Photos. 225 | */ 226 | public static boolean isGooglePhotosUri(Uri uri) { 227 | return "com.google.android.apps.photos.content".equals(uri.getAuthority()); 228 | } 229 | 230 | public static boolean isGoogleDriveUri(Uri uri) { 231 | return "com.google.android.apps.docs.storage.legacy".equals(uri.getAuthority()) || 232 | "com.google.android.apps.docs.storage".equals(uri.getAuthority()); 233 | } 234 | 235 | /** 236 | * Get the value of the data column for this Uri. This is useful for 237 | * MediaStore Uris, and other file-based ContentProviders. 238 | * 239 | * @param context The context. 240 | * @param uri The Uri to query. 241 | * @param selection (Optional) Filter used in the query. 242 | * @param selectionArgs (Optional) Selection arguments used in the query. 243 | * @return The value of the _data column, which is typically a file path. 244 | */ 245 | public static String getDataColumn(Context context, Uri uri, String selection, 246 | String[] selectionArgs) { 247 | 248 | Cursor cursor = null; 249 | final String column = MediaStore.Files.FileColumns.DATA; 250 | final String[] projection = { 251 | column 252 | }; 253 | 254 | try { 255 | cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, 256 | null); 257 | if (cursor != null && cursor.moveToFirst()) { 258 | if (DEBUG) 259 | DatabaseUtils.dumpCursor(cursor); 260 | 261 | final int column_index = cursor.getColumnIndexOrThrow(column); 262 | return cursor.getString(column_index); 263 | } 264 | } catch (Exception e) { 265 | // Timber.e(e); 266 | } finally { 267 | if (cursor != null) 268 | cursor.close(); 269 | } 270 | return null; 271 | } 272 | 273 | /** 274 | * Get a file path from a Uri. This will get the the path for Storage Access 275 | * Framework Documents, as well as the _data field for the MediaStore and 276 | * other file-based ContentProviders.
277 | *
278 | * Callers should check whether the path is local before assuming it 279 | * represents a local file. 280 | * 281 | * @param context The context. 282 | * @param uri The Uri to query. 283 | * @see #isLocal(String) 284 | * @see #getFile(Context, Uri) 285 | */ 286 | public static String getPath(final Context context, final Uri uri) { 287 | String absolutePath = getLocalPath(context, uri); 288 | return absolutePath != null ? absolutePath : uri.toString(); 289 | } 290 | 291 | private static String getLocalPath(final Context context, final Uri uri) { 292 | 293 | if (DEBUG) 294 | Log.d(TAG + " File -", 295 | "Authority: " + uri.getAuthority() + 296 | ", Fragment: " + uri.getFragment() + 297 | ", Port: " + uri.getPort() + 298 | ", Query: " + uri.getQuery() + 299 | ", Scheme: " + uri.getScheme() + 300 | ", Host: " + uri.getHost() + 301 | ", Segments: " + uri.getPathSegments().toString() 302 | ); 303 | 304 | final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; 305 | 306 | // DocumentProvider 307 | if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { 308 | // LocalStorageProvider 309 | if (isLocalStorageDocument(uri)) { 310 | // The path is the id 311 | return DocumentsContract.getDocumentId(uri); 312 | } 313 | // ExternalStorageProvider 314 | else if (isExternalStorageDocument(uri)) { 315 | final String docId = DocumentsContract.getDocumentId(uri); 316 | final String[] split = docId.split(":"); 317 | final String type = split[0]; 318 | 319 | if ("primary".equalsIgnoreCase(type)) { 320 | return Environment.getExternalStorageDirectory() + "/" + split[1]; 321 | } else if ("home".equalsIgnoreCase(type)) { 322 | return Environment.getExternalStorageDirectory() + "/documents/" + split[1]; 323 | } 324 | } 325 | // DownloadsProvider 326 | else if (isDownloadsDocument(uri)) { 327 | 328 | final String id = DocumentsContract.getDocumentId(uri); 329 | 330 | if (id != null && id.startsWith("raw:")) { 331 | return id.substring(4); 332 | } 333 | 334 | String[] contentUriPrefixesToTry = new String[]{ 335 | "content://downloads/public_downloads", 336 | "content://downloads/my_downloads" 337 | }; 338 | 339 | for (String contentUriPrefix : contentUriPrefixesToTry) { 340 | Uri contentUri = ContentUris.withAppendedId(Uri.parse(contentUriPrefix), Long.valueOf(id)); 341 | try { 342 | String path = getDataColumn(context, contentUri, null, null); 343 | if (path != null) { 344 | return path; 345 | } 346 | } catch (Exception e) {} 347 | } 348 | 349 | // path could not be retrieved using ContentResolver, therefore copy file to accessible cache using streams 350 | String fileName = getFileName(context, uri); 351 | File cacheDir = getDocumentCacheDir(context); 352 | File file = generateFileName(fileName, cacheDir); 353 | String destinationPath = null; 354 | if (file != null) { 355 | destinationPath = file.getAbsolutePath(); 356 | saveFileFromUri(context, uri, destinationPath); 357 | } 358 | 359 | return destinationPath; 360 | } 361 | // MediaProvider 362 | else if (isMediaDocument(uri)) { 363 | final String docId = DocumentsContract.getDocumentId(uri); 364 | final String[] split = docId.split(":"); 365 | final String type = split[0]; 366 | 367 | Uri contentUri = null; 368 | if ("image".equals(type)) { 369 | contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; 370 | } else if ("video".equals(type)) { 371 | contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; 372 | } else if ("audio".equals(type)) { 373 | contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; 374 | } 375 | 376 | final String selection = "_id=?"; 377 | final String[] selectionArgs = new String[]{ 378 | split[1] 379 | }; 380 | 381 | return getDataColumn(context, contentUri, selection, selectionArgs); 382 | } 383 | //GoogleDriveProvider 384 | else if (isGoogleDriveUri(uri)) { 385 | return getGoogleDriveFilePath(uri, context); 386 | } 387 | } 388 | // MediaStore (and general) 389 | else if ("content".equalsIgnoreCase(uri.getScheme())) { 390 | 391 | // Return the remote address 392 | if (isGooglePhotosUri(uri)) { 393 | return uri.getLastPathSegment(); 394 | } 395 | // Google drive legacy provider 396 | else if (isGoogleDriveUri(uri)) { 397 | return getGoogleDriveFilePath(uri, context); 398 | } 399 | 400 | return getDataColumn(context, uri, null, null); 401 | } 402 | // File 403 | else if ("file".equalsIgnoreCase(uri.getScheme())) { 404 | return uri.getPath(); 405 | } 406 | 407 | return null; 408 | } 409 | 410 | /** 411 | * Convert Uri into File, if possible. 412 | * 413 | * @return file A local file that the Uri was pointing to, or null if the 414 | * Uri is unsupported or pointed to a remote resource. 415 | * @author paulburke 416 | * @see #getPath(Context, Uri) 417 | */ 418 | public static File getFile(Context context, Uri uri) { 419 | if (uri != null) { 420 | String path = getPath(context, uri); 421 | if (path != null && isLocal(path)) { 422 | return new File(path); 423 | } 424 | } 425 | return null; 426 | } 427 | 428 | /** 429 | * Get the file size in a human-readable string. 430 | * 431 | * @param size 432 | * @return 433 | * @author paulburke 434 | */ 435 | public static String getReadableFileSize(int size) { 436 | final int BYTES_IN_KILOBYTES = 1024; 437 | final DecimalFormat dec = new DecimalFormat("###.#"); 438 | final String KILOBYTES = " KB"; 439 | final String MEGABYTES = " MB"; 440 | final String GIGABYTES = " GB"; 441 | float fileSize = 0; 442 | String suffix = KILOBYTES; 443 | 444 | if (size > BYTES_IN_KILOBYTES) { 445 | fileSize = size / BYTES_IN_KILOBYTES; 446 | if (fileSize > BYTES_IN_KILOBYTES) { 447 | fileSize = fileSize / BYTES_IN_KILOBYTES; 448 | if (fileSize > BYTES_IN_KILOBYTES) { 449 | fileSize = fileSize / BYTES_IN_KILOBYTES; 450 | suffix = GIGABYTES; 451 | } else { 452 | suffix = MEGABYTES; 453 | } 454 | } 455 | } 456 | return String.valueOf(dec.format(fileSize) + suffix); 457 | } 458 | 459 | /** 460 | * Get the Intent for selecting content to be used in an Intent Chooser. 461 | * 462 | * @return The intent for opening a file with Intent.createChooser() 463 | */ 464 | public static Intent createGetContentIntent() { 465 | // Implicitly allow the user to select a particular kind of data 466 | final Intent intent = new Intent(Intent.ACTION_GET_CONTENT); 467 | // The MIME data type filter 468 | intent.setType("*/*"); 469 | // Only return URIs that can be opened with ContentResolver 470 | intent.addCategory(Intent.CATEGORY_OPENABLE); 471 | return intent; 472 | } 473 | 474 | public static File getDownloadsDir() { 475 | return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); 476 | } 477 | 478 | public static File getDocumentCacheDir( Context context) { 479 | File dir = new File(context.getCacheDir(), DOCUMENTS_DIR); 480 | if (!dir.exists()) { 481 | dir.mkdirs(); 482 | } 483 | logDir(context.getCacheDir()); 484 | logDir(dir); 485 | 486 | return dir; 487 | } 488 | 489 | private static void logDir(File dir) { 490 | if(!DEBUG) return; 491 | Log.d(TAG, "Dir=" + dir); 492 | File[] files = dir.listFiles(); 493 | for (File file : files) { 494 | Log.d(TAG, "File=" + file.getPath()); 495 | } 496 | } 497 | 498 | public static File generateFileName(String name, File directory) { 499 | if (name == null) { 500 | return null; 501 | } 502 | 503 | File file = new File(directory, name); 504 | 505 | if (file.exists()) { 506 | String fileName = name; 507 | String extension = ""; 508 | int dotIndex = name.lastIndexOf('.'); 509 | if (dotIndex > 0) { 510 | fileName = name.substring(0, dotIndex); 511 | extension = name.substring(dotIndex); 512 | } 513 | 514 | int index = 0; 515 | 516 | while (file.exists()) { 517 | index++; 518 | name = fileName + '(' + index + ')' + extension; 519 | file = new File(directory, name); 520 | } 521 | } 522 | 523 | try { 524 | if (!file.createNewFile()) { 525 | return null; 526 | } 527 | } catch (IOException e) { 528 | Log.w(TAG, e); 529 | return null; 530 | } 531 | 532 | logDir(directory); 533 | 534 | return file; 535 | } 536 | 537 | private static void saveFileFromUri(Context context, Uri uri, String destinationPath) { 538 | InputStream is = null; 539 | BufferedOutputStream bos = null; 540 | try { 541 | is = context.getContentResolver().openInputStream(uri); 542 | bos = new BufferedOutputStream(new FileOutputStream(destinationPath, false)); 543 | byte[] buf = new byte[1024]; 544 | is.read(buf); 545 | do { 546 | bos.write(buf); 547 | } while (is.read(buf) != -1); 548 | } catch (IOException e) { 549 | e.printStackTrace(); 550 | } finally { 551 | try { 552 | if (is != null) is.close(); 553 | if (bos != null) bos.close(); 554 | } catch (IOException e) { 555 | e.printStackTrace(); 556 | } 557 | } 558 | } 559 | 560 | public static byte[] readBytesFromFile(String filePath) { 561 | 562 | FileInputStream fileInputStream = null; 563 | byte[] bytesArray = null; 564 | 565 | try { 566 | 567 | File file = new File(filePath); 568 | bytesArray = new byte[(int) file.length()]; 569 | 570 | //read file into bytes[] 571 | fileInputStream = new FileInputStream(file); 572 | fileInputStream.read(bytesArray); 573 | 574 | } catch (IOException e) { 575 | e.printStackTrace(); 576 | } finally { 577 | if (fileInputStream != null) { 578 | try { 579 | fileInputStream.close(); 580 | } catch (IOException e) { 581 | e.printStackTrace(); 582 | } 583 | } 584 | 585 | } 586 | 587 | return bytesArray; 588 | 589 | } 590 | 591 | public static File createTempImageFile(Context context, String fileName) throws IOException { 592 | // Create an image file name 593 | File storageDir = new File(context.getCacheDir(), DOCUMENTS_DIR); 594 | return File.createTempFile(fileName, ".jpg", storageDir); 595 | } 596 | 597 | public static String getFileName(Context context, Uri uri) { 598 | String mimeType = context.getContentResolver().getType(uri); 599 | String filename = null; 600 | 601 | if (mimeType == null && context != null) { 602 | String path = getPath(context, uri); 603 | if (path == null) { 604 | filename = getName(uri.toString()); 605 | } else { 606 | File file = new File(path); 607 | filename = file.getName(); 608 | } 609 | } else { 610 | Cursor returnCursor = context.getContentResolver().query(uri, null, 611 | null, null, null); 612 | if (returnCursor != null) { 613 | int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); 614 | returnCursor.moveToFirst(); 615 | filename = returnCursor.getString(nameIndex); 616 | returnCursor.close(); 617 | } 618 | } 619 | 620 | return filename; 621 | } 622 | 623 | public static String getName(String filename) { 624 | if (filename == null) { 625 | return null; 626 | } 627 | int index = filename.lastIndexOf('/'); 628 | return filename.substring(index + 1); 629 | } 630 | 631 | private static String getGoogleDriveFilePath(Uri uri, Context context) { 632 | Uri returnUri = uri; 633 | Cursor returnCursor = context.getContentResolver().query(returnUri, null, null, null, null); 634 | /* 635 | * Get the column indexes of the data in the Cursor, 636 | * * move to the first row in the Cursor, get the data, 637 | * * and display it. 638 | * */ 639 | int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); 640 | int sizeIndex = returnCursor.getColumnIndex(OpenableColumns.SIZE); 641 | returnCursor.moveToFirst(); 642 | 643 | String name = (returnCursor.getString(nameIndex)); 644 | String size = (Long.toString(returnCursor.getLong(sizeIndex))); 645 | File file = new File(context.getCacheDir(), name); 646 | try { 647 | InputStream inputStream = context.getContentResolver().openInputStream(uri); 648 | FileOutputStream outputStream = new FileOutputStream(file); 649 | int read = 0; 650 | int maxBufferSize = 1 * 1024 * 1024; 651 | int bytesAvailable = inputStream.available(); 652 | int bufferSize = Math.min(bytesAvailable, maxBufferSize); 653 | 654 | final byte[] buffers = new byte[bufferSize]; 655 | while ((read = inputStream.read(buffers)) != -1) { 656 | outputStream.write(buffers, 0, read); 657 | } 658 | inputStream.close(); 659 | outputStream.close(); 660 | } catch (Exception e) { 661 | e.printStackTrace(); 662 | } 663 | return file.getPath(); 664 | } 665 | } -------------------------------------------------------------------------------- /src/android/VideoEditor.java: -------------------------------------------------------------------------------- 1 | package org.apache.cordova.videoeditor; 2 | 3 | import java.io.*; 4 | import java.text.SimpleDateFormat; 5 | import java.util.Date; 6 | import java.util.Locale; 7 | 8 | import android.graphics.Bitmap; 9 | 10 | import org.apache.cordova.CordovaInterface; 11 | import org.apache.cordova.CordovaPlugin; 12 | import org.apache.cordova.CallbackContext; 13 | import org.apache.cordova.CordovaResourceApi; 14 | import org.apache.cordova.CordovaWebView; 15 | import org.apache.cordova.PluginResult; 16 | import org.json.JSONArray; 17 | import org.json.JSONException; 18 | import org.json.JSONObject; 19 | 20 | import android.content.Context; 21 | import android.content.Intent; 22 | import android.content.pm.ApplicationInfo; 23 | import android.content.pm.PackageManager; 24 | import android.content.pm.PackageManager.NameNotFoundException; 25 | import android.media.MediaExtractor; 26 | import android.media.MediaMetadataRetriever; 27 | import android.net.Uri; 28 | import android.os.Build; 29 | import android.os.Environment; 30 | import android.util.Log; 31 | 32 | import net.ypresto.androidtranscoder.MediaTranscoder; 33 | import net.ypresto.androidtranscoder.utils.MediaExtractorUtils; 34 | 35 | /** 36 | * VideoEditor plugin for Android 37 | * Created by Ross Martin 2-2-15 38 | */ 39 | public class VideoEditor extends CordovaPlugin { 40 | 41 | private static final String TAG = "VideoEditor"; 42 | 43 | private CallbackContext callback; 44 | private CordovaResourceApi resourceApi; 45 | 46 | /** 47 | * Initialization 48 | */ 49 | @Override 50 | public void initialize(CordovaInterface cordova, CordovaWebView webView) { 51 | super.initialize(cordova, webView); 52 | this.resourceApi = webView.getResourceApi(); 53 | } 54 | 55 | /** 56 | * Executes the request to the plugin. 57 | */ 58 | @Override 59 | public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { 60 | Log.d(TAG, "execute method starting"); 61 | 62 | this.callback = callbackContext; 63 | 64 | if (action.equals("transcodeVideo")) { 65 | try { 66 | this.transcodeVideo(args); 67 | } catch (IOException e) { 68 | callback.error(e.toString()); 69 | } 70 | return true; 71 | } else if (action.equals("createThumbnail")) { 72 | try { 73 | this.createThumbnail(args); 74 | } catch (IOException e) { 75 | callback.error(e.toString()); 76 | } 77 | return true; 78 | } else if (action.equals("getVideoInfo")) { 79 | try { 80 | this.getVideoInfo(args); 81 | } catch (IOException e) { 82 | callback.error(e.toString()); 83 | } 84 | return true; 85 | } 86 | 87 | return false; 88 | } 89 | 90 | /** 91 | * transcodeVideo 92 | * 93 | * Transcodes a video 94 | * 95 | * ARGUMENTS 96 | * ========= 97 | * 98 | * fileUri - path to input video 99 | * outputFileName - output file name 100 | * saveToLibrary - save to gallery 101 | * width - width for the output video 102 | * height - height for the output video 103 | * fps - fps the video 104 | * videoBitrate - video bitrate for the output video in bits 105 | * audioBitrate - audio bitrate for the output video in bits 106 | * audioChannels - number of audio channels 107 | * skipVideoTranscodingIfAVC - skip any transcoding actions (conversion/resizing/etc..) if the input video is avc video 108 | * 109 | * RESPONSE 110 | * ======== 111 | * 112 | * outputFilePath - path to output file 113 | * 114 | * @param args arguments 115 | */ 116 | private void transcodeVideo(JSONArray args) throws JSONException, IOException { 117 | Log.d(TAG, "transcodeVideo firing"); 118 | 119 | JSONObject options = args.optJSONObject(0); 120 | Log.d(TAG, "options: " + options.toString()); 121 | 122 | final ReadDataResult readResult = this.readDataFrom(options.getString("fileUri")); 123 | if (readResult == null) { 124 | return; 125 | } 126 | 127 | final String outputFileName = options.optString( 128 | "outputFileName", 129 | new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.ENGLISH).format(new Date()) 130 | ); 131 | 132 | final int width = options.optInt("width", CustomAndroidFormatStrategy.DEFAULT_WIDTH); 133 | final int height = options.optInt("height", CustomAndroidFormatStrategy.DEFAULT_HEIGHT); 134 | final int fps = options.optInt("fps", CustomAndroidFormatStrategy.DEFAULT_FRAMERATE); 135 | final int videoBitrate = options.optInt("videoBitrate", CustomAndroidFormatStrategy.DEFAULT_VIDEO_BITRATE); // default to 9 megabit 136 | final int audioBitrate = options.optInt("audioBitrate", CustomAndroidFormatStrategy.AUDIO_BITRATE_AS_IS); 137 | final int audioChannels = options.optInt("audioChannels", CustomAndroidFormatStrategy.AUDIO_CHANNELS_AS_IS); 138 | final boolean skipVideoTranscodingIfAVC = options.optBoolean("skipVideoTranscodingIfAVC", CustomAndroidFormatStrategy.DEFAULT_SKIP_AVC_VIDEO_TRANSCODING); 139 | 140 | final String outputExtension = ".mp4"; 141 | 142 | final Context appContext = cordova.getActivity().getApplicationContext(); 143 | final PackageManager pm = appContext.getPackageManager(); 144 | 145 | ApplicationInfo ai; 146 | try { 147 | ai = pm.getApplicationInfo(cordova.getActivity().getPackageName(), 0); 148 | } catch (final NameNotFoundException e) { 149 | ai = null; 150 | } 151 | final String appName = (String) (ai != null ? pm.getApplicationLabel(ai) : "Unknown"); 152 | 153 | final boolean saveToLibrary = options.optBoolean("saveToLibrary", true); 154 | File mediaStorageDir; 155 | 156 | if (saveToLibrary) { 157 | mediaStorageDir = new File( 158 | Environment.getExternalStorageDirectory() + "/Movies", 159 | appName 160 | ); 161 | } else { 162 | mediaStorageDir = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Android/data/" + cordova.getActivity().getPackageName() + "/files/files/videos"); 163 | } 164 | 165 | if (!mediaStorageDir.exists()) { 166 | if (!mediaStorageDir.mkdirs()) { 167 | callback.error("Can't access or make Movies directory"); 168 | readResult.close(); 169 | return; 170 | } 171 | } 172 | 173 | final String outputFilePath = new File( 174 | mediaStorageDir.getPath(), 175 | outputFileName + outputExtension 176 | ).getAbsolutePath(); 177 | 178 | Log.d(TAG, "outputFilePath: " + outputFilePath); 179 | 180 | cordova.getThreadPool().execute(() -> { 181 | 182 | try { 183 | MediaTranscoder.Listener listener = new MediaTranscoder.Listener() { 184 | @Override 185 | public void onTranscodeProgress(double progress) { 186 | Log.d(TAG, "transcode running " + progress); 187 | 188 | JSONObject jsonObj = new JSONObject(); 189 | try { 190 | jsonObj.put("progress", progress); 191 | } catch (JSONException e) { 192 | e.printStackTrace(); 193 | } 194 | 195 | PluginResult progressResult = new PluginResult(PluginResult.Status.OK, jsonObj); 196 | progressResult.setKeepCallback(true); 197 | callback.sendPluginResult(progressResult); 198 | } 199 | 200 | @Override 201 | public void onTranscodeCompleted() { 202 | 203 | File outFile = new File(outputFilePath); 204 | if (!outFile.exists()) { 205 | Log.d(TAG, "outputFile doesn't exist!"); 206 | readResult.close(); 207 | callback.error("an error ocurred during transcoding"); 208 | return; 209 | } 210 | 211 | // make the gallery display the new file if saving to library 212 | if (saveToLibrary) { 213 | Intent scanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); 214 | scanIntent.setData(readResult.result.uri); 215 | scanIntent.setData(Uri.fromFile(outFile)); 216 | appContext.sendBroadcast(scanIntent); 217 | } 218 | 219 | readResult.close(); 220 | callback.success(outputFilePath); 221 | } 222 | 223 | @Override 224 | public void onTranscodeCanceled() { 225 | readResult.close(); 226 | callback.error("transcode canceled"); 227 | Log.d(TAG, "transcode canceled"); 228 | } 229 | 230 | @Override 231 | public void onTranscodeFailed(Exception exception) { 232 | readResult.close(); 233 | callback.error(exception.toString()); 234 | Log.d(TAG, "transcode exception", exception); 235 | } 236 | }; 237 | 238 | final FileDescriptor fileDescriptor = readResult.getFD(); 239 | 240 | MediaMetadataRetriever mmr = new MediaMetadataRetriever(); 241 | mmr.setDataSource(fileDescriptor); 242 | 243 | MediaTranscoder.getInstance().transcodeVideo( 244 | fileDescriptor, 245 | outputFilePath, 246 | new CustomAndroidFormatStrategy(videoBitrate, fps, width, height, audioBitrate, audioChannels, skipVideoTranscodingIfAVC), 247 | listener 248 | ); 249 | 250 | } catch (Throwable e) { 251 | Log.d(TAG, "transcode exception ", e); 252 | readResult.close(); 253 | callback.error(e.toString()); 254 | } 255 | 256 | }); 257 | } 258 | 259 | /** 260 | * createThumbnail 261 | * 262 | * Creates a thumbnail from the start of a video. 263 | * 264 | * ARGUMENTS 265 | * ========= 266 | * fileUri - input file path 267 | * outputFileName - output file name 268 | * atTime - location in the video to create the thumbnail (in seconds) 269 | * width - width for the thumbnail (optional) 270 | * height - height for the thumbnail (optional) 271 | * quality - quality of the thumbnail (optional, between 1 and 100) 272 | * 273 | * RESPONSE 274 | * ======== 275 | * 276 | * outputFilePath - path to output file 277 | * 278 | * @param args arguments 279 | */ 280 | private void createThumbnail(JSONArray args) throws JSONException, IOException { 281 | Log.d(TAG, "createThumbnail firing"); 282 | 283 | JSONObject options = args.optJSONObject(0); 284 | Log.d(TAG, "options: " + options.toString()); 285 | 286 | final ReadDataResult readResult = this.readDataFrom(options.getString("fileUri")); 287 | if (readResult == null) { 288 | return; 289 | } 290 | 291 | String outputFileName = options.optString( 292 | "outputFileName", 293 | new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.ENGLISH).format(new Date()) 294 | ); 295 | 296 | final int quality = options.optInt("quality", 100); 297 | final int width = options.optInt("width", 0); 298 | final int height = options.optInt("height", 0); 299 | long atTimeOpt = options.optLong("atTime", 0); 300 | final long atTime = (atTimeOpt == 0) ? 0 : atTimeOpt * 1000000; 301 | 302 | File externalFilesDir = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Android/data/" + cordova.getActivity().getPackageName() + "/files/files/videos"); 303 | if (!externalFilesDir.exists()) { 304 | if (!externalFilesDir.mkdirs()) { 305 | callback.error("Can't access or make Movies directory"); 306 | readResult.close(); 307 | return; 308 | } 309 | } 310 | 311 | final File outputFile = new File( 312 | externalFilesDir.getPath(), 313 | outputFileName + ".jpg" 314 | ); 315 | final String outputFilePath = outputFile.getAbsolutePath(); 316 | 317 | // start task 318 | cordova.getThreadPool().execute(() -> { 319 | 320 | OutputStream outStream = null; 321 | 322 | try { 323 | final FileDescriptor fileDescriptor = readResult.getFD(); 324 | MediaMetadataRetriever mmr = new MediaMetadataRetriever(); 325 | mmr.setDataSource(fileDescriptor); 326 | 327 | Bitmap bitmap = mmr.getFrameAtTime(atTime); 328 | 329 | if (width > 0 || height > 0) { 330 | int videoWidth = bitmap.getWidth(); 331 | int videoHeight = bitmap.getHeight(); 332 | double aspectRatio = (double) videoWidth / (double) videoHeight; 333 | 334 | Log.d(TAG, "videoWidth: " + videoWidth); 335 | Log.d(TAG, "videoHeight: " + videoHeight); 336 | 337 | int scaleWidth = Double.valueOf(height * aspectRatio).intValue(); 338 | int scaleHeight = Double.valueOf(scaleWidth / aspectRatio).intValue(); 339 | 340 | Log.d(TAG, "scaleWidth: " + scaleWidth); 341 | Log.d(TAG, "scaleHeight: " + scaleHeight); 342 | 343 | final Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap, scaleWidth, scaleHeight, false); 344 | bitmap.recycle(); 345 | bitmap = resizedBitmap; 346 | } 347 | 348 | outStream = new FileOutputStream(outputFile); 349 | bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outStream); 350 | 351 | callback.success(outputFilePath); 352 | 353 | } catch (Throwable e) { 354 | if (outStream != null) { 355 | try { 356 | outStream.close(); 357 | } catch (IOException e1) { 358 | e1.printStackTrace(); 359 | } 360 | } 361 | 362 | Log.d(TAG, "exception on thumbnail creation", e); 363 | callback.error(e.toString()); 364 | 365 | } finally { 366 | readResult.close(); 367 | } 368 | 369 | }); 370 | } 371 | 372 | /** 373 | * getVideoInfo 374 | * 375 | * Gets info on a video 376 | * 377 | * ARGUMENTS 378 | * ========= 379 | * 380 | * fileUri: - path to input video 381 | * 382 | * RESPONSE 383 | * ======== 384 | * 385 | * width - width of the video 386 | * height - height of the video 387 | * orientation - orientation of the video 388 | * duration - duration of the video (in seconds) 389 | * size - size of the video (in bytes) 390 | * bitrate - bitrate of the video (in bits per second) 391 | * videoMediaType - Media type of the video 392 | * audioMediaType - Media type of the audio track in video 393 | * 394 | * @param args arguments 395 | */ 396 | private void getVideoInfo(JSONArray args) throws JSONException, IOException { 397 | Log.d(TAG, "getVideoInfo firing"); 398 | 399 | JSONObject options = args.optJSONObject(0); 400 | Log.d(TAG, "options: " + options.toString()); 401 | 402 | final ReadDataResult readResult = this.readDataFrom(options.getString("fileUri")); 403 | if (readResult == null) { 404 | return; 405 | } 406 | 407 | final FileDescriptor fileDescriptor = readResult.getFD(); 408 | MediaMetadataRetriever mmr = new MediaMetadataRetriever(); 409 | mmr.setDataSource(fileDescriptor); 410 | float videoWidth = Float.parseFloat(mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)); 411 | float videoHeight = Float.parseFloat(mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)); 412 | 413 | String orientation; 414 | if (Build.VERSION.SDK_INT >= 17) { 415 | String mmrOrientation = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION); 416 | Log.d(TAG, "mmrOrientation: " + mmrOrientation); // 0, 90, 180, or 270 417 | 418 | if (videoWidth < videoHeight) { 419 | if (mmrOrientation.equals("0") || mmrOrientation.equals("180")) { 420 | orientation = "portrait"; 421 | } else { 422 | orientation = "landscape"; 423 | } 424 | } else { 425 | if (mmrOrientation.equals("0") || mmrOrientation.equals("180")) { 426 | orientation = "landscape"; 427 | } else { 428 | orientation = "portrait"; 429 | } 430 | } 431 | } else { 432 | orientation = (videoWidth < videoHeight) ? "portrait" : "landscape"; 433 | } 434 | 435 | double duration = Double.parseDouble(mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)) / 1000.0; 436 | long bitrate = Long.parseLong(mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE)); 437 | 438 | String videoMediaType; 439 | String audioMediaType; 440 | try { 441 | final MediaExtractor mExtractor = new MediaExtractor(); 442 | mExtractor.setDataSource(fileDescriptor); 443 | MediaExtractorUtils.TrackResult trackResult = MediaExtractorUtils.getFirstVideoAndAudioTrack(mExtractor); 444 | 445 | // get types 446 | videoMediaType = trackResult.mVideoTrackMime; 447 | audioMediaType = trackResult.mAudioTrackMime; 448 | 449 | // release resources 450 | mExtractor.release(); 451 | trackResult = null; 452 | } catch (Throwable e) { 453 | Log.e(TAG, e.toString()); 454 | callback.error(e.toString()); 455 | readResult.close(); 456 | return; 457 | } 458 | 459 | JSONObject response = new JSONObject(); 460 | response.put("width", videoWidth); 461 | response.put("height", videoHeight); 462 | response.put("orientation", orientation); 463 | response.put("duration", duration); 464 | response.put("size", readResult.result.length); 465 | response.put("bitrate", bitrate); 466 | response.put("videoMediaType", videoMediaType); 467 | response.put("audioMediaType", audioMediaType); 468 | 469 | // release resources 470 | readResult.close(); 471 | 472 | callback.success(response); 473 | } 474 | 475 | /** 476 | * Reads the data by the given url 477 | * @param url the url to read the data 478 | * @return results of the reading 479 | */ 480 | private ReadDataResult readDataFrom(String url) throws IOException { 481 | if (!FileUtils.isLocal(url)) { 482 | final String msg = "The provided url is null or not local: " + url; 483 | Log.d(TAG, msg); 484 | callback.error(msg); 485 | return null; 486 | } 487 | 488 | final Context context = this.cordova.getActivity().getApplicationContext(); 489 | Uri uri = Uri.parse(url); 490 | if (uri.isRelative()) { 491 | uri = Uri.parse(FileUtils.getPath(context, uri)); 492 | } 493 | 494 | CordovaResourceApi.OpenForReadResult readResult = resourceApi.openForRead(uri, true); 495 | return new ReadDataResult(readResult); 496 | } 497 | 498 | /** 499 | * Simple wrapper over the CordovaResourceApi.OpenForReadResult with some util methods 500 | */ 501 | public static final class ReadDataResult { 502 | public final CordovaResourceApi.OpenForReadResult result; 503 | private FileDescriptor fileDescriptor; 504 | 505 | public ReadDataResult(CordovaResourceApi.OpenForReadResult result) { 506 | this.result = result; 507 | } 508 | 509 | /** 510 | * Returns file descriptor based on the OpenForReadResult 511 | * @return FileDescriptor for the given result 512 | */ 513 | public FileDescriptor getFD() throws IOException { 514 | if (this.fileDescriptor != null) { 515 | return this.fileDescriptor; 516 | } 517 | 518 | if (this.result.inputStream != null && 519 | this.result.inputStream instanceof FileInputStream) { 520 | this.fileDescriptor = ((FileInputStream) this.result.inputStream).getFD(); 521 | return this.fileDescriptor; 522 | } 523 | 524 | if (this.result.assetFd != null) { 525 | this.fileDescriptor = this.result.assetFd.getFileDescriptor(); 526 | return this.fileDescriptor; 527 | } 528 | 529 | return null; 530 | } 531 | 532 | /** 533 | * Closes the stream and descriptor if them exists 534 | */ 535 | public void close() { 536 | try { 537 | if (this.result.assetFd != null) { 538 | this.result.assetFd.close(); 539 | } 540 | if (this.result.inputStream != null) { 541 | this.result.inputStream.close(); 542 | } 543 | } catch (IOException e) { 544 | e.printStackTrace(); 545 | } 546 | } 547 | } 548 | 549 | } 550 | -------------------------------------------------------------------------------- /src/android/build.gradle: -------------------------------------------------------------------------------- 1 | 2 | repositories{ 3 | jcenter() 4 | maven { url "https://jitpack.io" } 5 | } 6 | 7 | dependencies { 8 | implementation 'com.github.AlexMiniApps:android-transcoder:56f5ec8821' 9 | } 10 | 11 | android { 12 | packagingOptions { 13 | exclude 'META-INF/NOTICE' 14 | exclude 'META-INF/LICENSE' 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/ios/SDAVAssetExportSession.h: -------------------------------------------------------------------------------- 1 | // 2 | // SDAVAssetExportSession.h 3 | // 4 | // This file is part of the SDAVAssetExportSession package. 5 | // 6 | // Created by Olivier Poitrey on 13/03/13. 7 | // Copyright 2013 Olivier Poitrey. All rights servered. 8 | // 9 | // For the full copyright and license information, please view the LICENSE 10 | // file that was distributed with this source code. 11 | // 12 | 13 | #import 14 | #import 15 | 16 | @protocol SDAVAssetExportSessionDelegate; 17 | 18 | 19 | /** 20 | * An `SDAVAssetExportSession` object transcodes the contents of an AVAsset source object to create an output 21 | * of the form described by specified video and audio settings. It implements most of the API of Apple provided 22 | * `AVAssetExportSession` but with the capability to provide you own video and audio settings instead of the 23 | * limited set of Apple provided presets. 24 | * 25 | * After you have initialized an export session with the asset that contains the source media, video and audio 26 | * settings, and the output file type (outputFileType), you can start the export running by invoking 27 | * `exportAsynchronouslyWithCompletionHandler:`. Because the export is performed asynchronously, this method 28 | * returns immediately — you can observe progress to check on the progress. 29 | * 30 | * The completion handler you supply to `exportAsynchronouslyWithCompletionHandler:` is called whether the export 31 | * fails, completes, or is cancelled. Upon completion, the status property indicates whether the export has 32 | * completed successfully. If it has failed, the value of the error property supplies additional information 33 | * about the reason for the failure. 34 | */ 35 | 36 | @interface SDAVAssetExportSession : NSObject 37 | 38 | @property (nonatomic, weak) id delegate; 39 | 40 | /** 41 | * The asset with which the export session was initialized. 42 | */ 43 | @property (nonatomic, strong, readonly) AVAsset *asset; 44 | 45 | /** 46 | * Indicates whether video composition is enabled for export, and supplies the instructions for video composition. 47 | * 48 | * You can observe this property using key-value observing. 49 | */ 50 | @property (nonatomic, copy) AVVideoComposition *videoComposition; 51 | 52 | /** 53 | * Indicates whether non-default audio mixing is enabled for export, and supplies the parameters for audio mixing. 54 | */ 55 | @property (nonatomic, copy) AVAudioMix *audioMix; 56 | 57 | /** 58 | * The type of file to be written by the session. 59 | * 60 | * The value is a UTI string corresponding to the file type to use when writing the asset. 61 | * For a list of constants specifying UTIs for standard file types, see `AV Foundation Constants Reference`. 62 | * 63 | * You can observe this property using key-value observing. 64 | */ 65 | @property (nonatomic, copy) NSString *outputFileType; 66 | 67 | /** 68 | * The URL of the export session’s output. 69 | * 70 | * You can observe this property using key-value observing. 71 | */ 72 | @property (nonatomic, copy) NSURL *outputURL; 73 | 74 | /** 75 | * The settings used for input video track. 76 | * 77 | * The dictionary’s keys are from . 78 | */ 79 | @property (nonatomic, copy) NSDictionary *videoInputSettings; 80 | 81 | /** 82 | * The settings used for encoding the video track. 83 | * 84 | * A value of nil specifies that appended output should not be re-encoded. 85 | * The dictionary’s keys are from . 86 | */ 87 | @property (nonatomic, copy) NSDictionary *videoSettings; 88 | 89 | /** 90 | * The settings used for encoding the audio track. 91 | * 92 | * A value of nil specifies that appended output should not be re-encoded. 93 | * The dictionary’s keys are from . 94 | */ 95 | @property (nonatomic, copy) NSDictionary *audioSettings; 96 | 97 | /** 98 | * The time range to be exported from the source. 99 | * 100 | * The default time range of an export session is `kCMTimeZero` to `kCMTimePositiveInfinity`, 101 | * meaning that (modulo a possible limit on file length) the full duration of the asset will be exported. 102 | * 103 | * You can observe this property using key-value observing. 104 | * 105 | */ 106 | @property (nonatomic, assign) CMTimeRange timeRange; 107 | 108 | /** 109 | * Indicates whether the movie should be optimized for network use. 110 | * 111 | * You can observe this property using key-value observing. 112 | */ 113 | @property (nonatomic, assign) BOOL shouldOptimizeForNetworkUse; 114 | 115 | /** 116 | * The metadata to be written to the output file by the export session. 117 | */ 118 | @property (nonatomic, copy) NSArray *metadata; 119 | 120 | /** 121 | * Describes the error that occurred if the export status is `AVAssetExportSessionStatusFailed` 122 | * or `AVAssetExportSessionStatusCancelled`. 123 | * 124 | * If there is no error to report, the value of this property is nil. 125 | */ 126 | @property (nonatomic, strong, readonly) NSError *error; 127 | 128 | /** 129 | * The progress of the export on a scale from 0 to 1. 130 | * 131 | * 132 | * A value of 0 means the export has not yet begun, 1 means the export is complete. 133 | * 134 | * Unlike Apple provided `AVAssetExportSession`, this property can be observed using key-value observing. 135 | */ 136 | @property (nonatomic, assign, readonly) float progress; 137 | 138 | /** 139 | * The status of the export session. 140 | * 141 | * For possible values, see “AVAssetExportSessionStatus.” 142 | * 143 | * You can observe this property using key-value observing. (TODO) 144 | */ 145 | @property (nonatomic, assign, readonly) AVAssetExportSessionStatus status; 146 | 147 | /** 148 | * Returns an asset export session configured with a specified asset. 149 | * 150 | * @param asset The asset you want to export 151 | * @return An asset export session initialized to export `asset`. 152 | */ 153 | + (id)exportSessionWithAsset:(AVAsset *)asset; 154 | 155 | /** 156 | * Initializes an asset export session with a specified asset. 157 | * 158 | * @param asset The asset you want to export 159 | * @return An asset export session initialized to export `asset`. 160 | */ 161 | - (id)initWithAsset:(AVAsset *)asset; 162 | 163 | /** 164 | * Starts the asynchronous execution of an export session. 165 | * 166 | * This method starts an asynchronous export operation and returns immediately. status signals the terminal 167 | * state of the export session, and if a failure occurs, error describes the problem. 168 | * 169 | * If internal preparation for export fails, handler is invoked synchronously. The handler may also be called 170 | * asynchronously, after the method returns, in the following cases: 171 | * 172 | * 1. If a failure occurs during the export, including failures of loading, re-encoding, or writing media data to the output. 173 | * 2. If cancelExport is invoked. 174 | * 3. After the export session succeeds, having completely written its output to the outputURL. 175 | * 176 | * @param handler A block that is invoked when writing is complete or in the event of writing failure. 177 | */ 178 | - (void)exportAsynchronouslyWithCompletionHandler:(void (^)())handler; 179 | 180 | /** 181 | * Cancels the execution of an export session. 182 | * 183 | * You can invoke this method when the export is running. 184 | */ 185 | - (void)cancelExport; 186 | 187 | @end 188 | 189 | 190 | @protocol SDAVAssetExportSessionDelegate 191 | 192 | - (void)exportSession:(SDAVAssetExportSession *)exportSession renderFrame:(CVPixelBufferRef)pixelBuffer withPresentationTime:(CMTime)presentationTime toBuffer:(CVPixelBufferRef)renderBuffer; 193 | 194 | @end 195 | -------------------------------------------------------------------------------- /src/ios/SDAVAssetExportSession.m: -------------------------------------------------------------------------------- 1 | // 2 | // SDAVAssetExportSession.m 3 | // 4 | // This file is part of the SDAVAssetExportSession package. 5 | // 6 | // Created by Olivier Poitrey on 13/03/13. 7 | // Copyright 2013 Olivier Poitrey. All rights servered. 8 | // 9 | // For the full copyright and license information, please view the LICENSE 10 | // file that was distributed with this source code. 11 | // 12 | 13 | 14 | #import "SDAVAssetExportSession.h" 15 | 16 | @interface SDAVAssetExportSession () 17 | 18 | @property (nonatomic, assign, readwrite) float progress; 19 | 20 | @property (nonatomic, strong) AVAssetReader *reader; 21 | @property (nonatomic, strong) AVAssetReaderVideoCompositionOutput *videoOutput; 22 | @property (nonatomic, strong) AVAssetReaderAudioMixOutput *audioOutput; 23 | @property (nonatomic, strong) AVAssetWriter *writer; 24 | @property (nonatomic, strong) AVAssetWriterInput *videoInput; 25 | @property (nonatomic, strong) AVAssetWriterInputPixelBufferAdaptor *videoPixelBufferAdaptor; 26 | @property (nonatomic, strong) AVAssetWriterInput *audioInput; 27 | @property (nonatomic, strong) dispatch_queue_t inputQueue; 28 | @property (nonatomic, strong) void (^completionHandler)(); 29 | 30 | @end 31 | 32 | @implementation SDAVAssetExportSession 33 | { 34 | NSError *_error; 35 | NSTimeInterval duration; 36 | CMTime lastSamplePresentationTime; 37 | } 38 | 39 | + (id)exportSessionWithAsset:(AVAsset *)asset 40 | { 41 | return [SDAVAssetExportSession.alloc initWithAsset:asset]; 42 | } 43 | 44 | - (id)initWithAsset:(AVAsset *)asset 45 | { 46 | if ((self = [super init])) 47 | { 48 | _asset = asset; 49 | _timeRange = CMTimeRangeMake(kCMTimeZero, kCMTimePositiveInfinity); 50 | } 51 | 52 | return self; 53 | } 54 | 55 | - (void)exportAsynchronouslyWithCompletionHandler:(void (^)())handler 56 | { 57 | NSParameterAssert(handler != nil); 58 | [self cancelExport]; 59 | self.completionHandler = handler; 60 | 61 | if (!self.outputURL) 62 | { 63 | _error = [NSError errorWithDomain:AVFoundationErrorDomain code:AVErrorExportFailed userInfo:@ 64 | { 65 | NSLocalizedDescriptionKey: @"Output URL not set" 66 | }]; 67 | handler(); 68 | return; 69 | } 70 | 71 | NSError *readerError; 72 | self.reader = [AVAssetReader.alloc initWithAsset:self.asset error:&readerError]; 73 | if (readerError) 74 | { 75 | _error = readerError; 76 | handler(); 77 | return; 78 | } 79 | 80 | NSError *writerError; 81 | self.writer = [AVAssetWriter assetWriterWithURL:self.outputURL fileType:self.outputFileType error:&writerError]; 82 | if (writerError) 83 | { 84 | _error = writerError; 85 | handler(); 86 | return; 87 | } 88 | 89 | self.reader.timeRange = self.timeRange; 90 | self.writer.shouldOptimizeForNetworkUse = self.shouldOptimizeForNetworkUse; 91 | self.writer.metadata = self.metadata; 92 | 93 | NSArray *videoTracks = [self.asset tracksWithMediaType:AVMediaTypeVideo]; 94 | 95 | 96 | if (CMTIME_IS_VALID(self.timeRange.duration) && !CMTIME_IS_POSITIVE_INFINITY(self.timeRange.duration)) 97 | { 98 | duration = CMTimeGetSeconds(self.timeRange.duration); 99 | } 100 | else 101 | { 102 | duration = CMTimeGetSeconds(self.asset.duration); 103 | } 104 | // 105 | // Video output 106 | // 107 | if (videoTracks.count > 0) { 108 | self.videoOutput = [AVAssetReaderVideoCompositionOutput assetReaderVideoCompositionOutputWithVideoTracks:videoTracks videoSettings:self.videoInputSettings]; 109 | self.videoOutput.alwaysCopiesSampleData = NO; 110 | if (self.videoComposition) 111 | { 112 | self.videoOutput.videoComposition = self.videoComposition; 113 | } 114 | else 115 | { 116 | self.videoOutput.videoComposition = [self buildDefaultVideoComposition]; 117 | } 118 | if ([self.reader canAddOutput:self.videoOutput]) 119 | { 120 | [self.reader addOutput:self.videoOutput]; 121 | } 122 | 123 | // 124 | // Video input 125 | // 126 | self.videoInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:self.videoSettings]; 127 | self.videoInput.expectsMediaDataInRealTime = NO; 128 | if ([self.writer canAddInput:self.videoInput]) 129 | { 130 | [self.writer addInput:self.videoInput]; 131 | } 132 | NSDictionary *pixelBufferAttributes = @ 133 | { 134 | (id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA), 135 | (id)kCVPixelBufferWidthKey: @(self.videoOutput.videoComposition.renderSize.width), 136 | (id)kCVPixelBufferHeightKey: @(self.videoOutput.videoComposition.renderSize.height), 137 | @"IOSurfaceOpenGLESTextureCompatibility": @YES, 138 | @"IOSurfaceOpenGLESFBOCompatibility": @YES, 139 | }; 140 | self.videoPixelBufferAdaptor = [AVAssetWriterInputPixelBufferAdaptor assetWriterInputPixelBufferAdaptorWithAssetWriterInput:self.videoInput sourcePixelBufferAttributes:pixelBufferAttributes]; 141 | } 142 | 143 | // 144 | //Audio output 145 | // 146 | NSArray *audioTracks = [self.asset tracksWithMediaType:AVMediaTypeAudio]; 147 | if (audioTracks.count > 0) { 148 | self.audioOutput = [AVAssetReaderAudioMixOutput assetReaderAudioMixOutputWithAudioTracks:audioTracks audioSettings:nil]; 149 | self.audioOutput.alwaysCopiesSampleData = NO; 150 | self.audioOutput.audioMix = self.audioMix; 151 | if ([self.reader canAddOutput:self.audioOutput]) 152 | { 153 | [self.reader addOutput:self.audioOutput]; 154 | } 155 | } else { 156 | // Just in case this gets reused 157 | self.audioOutput = nil; 158 | } 159 | 160 | // 161 | // Audio input 162 | // 163 | if (self.audioOutput) { 164 | self.audioInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio outputSettings:self.audioSettings]; 165 | self.audioInput.expectsMediaDataInRealTime = NO; 166 | if ([self.writer canAddInput:self.audioInput]) 167 | { 168 | [self.writer addInput:self.audioInput]; 169 | } 170 | } 171 | 172 | [self.writer startWriting]; 173 | [self.reader startReading]; 174 | [self.writer startSessionAtSourceTime:self.timeRange.start]; 175 | 176 | __block BOOL videoCompleted = NO; 177 | __block BOOL audioCompleted = NO; 178 | __weak SDAVAssetExportSession *wself = self; 179 | self.inputQueue = dispatch_queue_create("VideoEncoderInputQueue", DISPATCH_QUEUE_SERIAL); 180 | if (videoTracks.count > 0) { 181 | [self.videoInput requestMediaDataWhenReadyOnQueue:self.inputQueue usingBlock:^ 182 | { 183 | if (![wself encodeReadySamplesFromOutput:wself.videoOutput toInput:wself.videoInput]) 184 | { 185 | @synchronized(wself) 186 | { 187 | videoCompleted = YES; 188 | if (audioCompleted) 189 | { 190 | [wself finish]; 191 | } 192 | } 193 | } 194 | }]; 195 | } 196 | else { 197 | videoCompleted = YES; 198 | } 199 | 200 | if (!self.audioOutput) { 201 | audioCompleted = YES; 202 | } else { 203 | [self.audioInput requestMediaDataWhenReadyOnQueue:self.inputQueue usingBlock:^ 204 | { 205 | if (![wself encodeReadySamplesFromOutput:wself.audioOutput toInput:wself.audioInput]) 206 | { 207 | @synchronized(wself) 208 | { 209 | audioCompleted = YES; 210 | if (videoCompleted) 211 | { 212 | [wself finish]; 213 | } 214 | } 215 | } 216 | }]; 217 | } 218 | } 219 | 220 | - (BOOL)encodeReadySamplesFromOutput:(AVAssetReaderOutput *)output toInput:(AVAssetWriterInput *)input 221 | { 222 | while (input.isReadyForMoreMediaData) 223 | { 224 | CMSampleBufferRef sampleBuffer = [output copyNextSampleBuffer]; 225 | if (sampleBuffer) 226 | { 227 | BOOL handled = NO; 228 | BOOL error = NO; 229 | 230 | if (self.reader.status != AVAssetReaderStatusReading || self.writer.status != AVAssetWriterStatusWriting) 231 | { 232 | handled = YES; 233 | error = YES; 234 | } 235 | 236 | if (!handled && self.videoOutput == output) 237 | { 238 | // update the video progress 239 | lastSamplePresentationTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer); 240 | lastSamplePresentationTime = CMTimeSubtract(lastSamplePresentationTime, self.timeRange.start); 241 | self.progress = duration == 0 ? 1 : CMTimeGetSeconds(lastSamplePresentationTime) / duration; 242 | 243 | if ([self.delegate respondsToSelector:@selector(exportSession:renderFrame:withPresentationTime:toBuffer:)]) 244 | { 245 | CVPixelBufferRef pixelBuffer = (CVPixelBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer); 246 | CVPixelBufferRef renderBuffer = NULL; 247 | CVPixelBufferPoolCreatePixelBuffer(NULL, self.videoPixelBufferAdaptor.pixelBufferPool, &renderBuffer); 248 | [self.delegate exportSession:self renderFrame:pixelBuffer withPresentationTime:lastSamplePresentationTime toBuffer:renderBuffer]; 249 | if (![self.videoPixelBufferAdaptor appendPixelBuffer:renderBuffer withPresentationTime:lastSamplePresentationTime]) 250 | { 251 | error = YES; 252 | } 253 | CVPixelBufferRelease(renderBuffer); 254 | handled = YES; 255 | } 256 | } 257 | if (!handled && ![input appendSampleBuffer:sampleBuffer]) 258 | { 259 | error = YES; 260 | } 261 | CFRelease(sampleBuffer); 262 | 263 | if (error) 264 | { 265 | return NO; 266 | } 267 | } 268 | else 269 | { 270 | [input markAsFinished]; 271 | return NO; 272 | } 273 | } 274 | 275 | return YES; 276 | } 277 | 278 | - (AVMutableVideoComposition *)buildDefaultVideoComposition 279 | { 280 | AVMutableVideoComposition *videoComposition = [AVMutableVideoComposition videoComposition]; 281 | AVAssetTrack *videoTrack = [[self.asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0]; 282 | 283 | // get the frame rate from videoSettings, if not set then try to get it from the video track, 284 | // if not set (mainly when asset is AVComposition) then use the default frame rate of 30 285 | float trackFrameRate = 0; 286 | if (self.videoSettings) 287 | { 288 | NSDictionary *videoCompressionProperties = [self.videoSettings objectForKey:AVVideoCompressionPropertiesKey]; 289 | if (videoCompressionProperties) 290 | { 291 | NSNumber *maxKeyFrameInterval = [videoCompressionProperties objectForKey:AVVideoMaxKeyFrameIntervalKey]; 292 | if (maxKeyFrameInterval) 293 | { 294 | trackFrameRate = maxKeyFrameInterval.floatValue; 295 | } 296 | } 297 | } 298 | else 299 | { 300 | trackFrameRate = [videoTrack nominalFrameRate]; 301 | } 302 | 303 | if (trackFrameRate == 0) 304 | { 305 | trackFrameRate = 30; 306 | } 307 | 308 | videoComposition.frameDuration = CMTimeMake(1, trackFrameRate); 309 | CGSize targetSize = CGSizeMake([self.videoSettings[AVVideoWidthKey] floatValue], [self.videoSettings[AVVideoHeightKey] floatValue]); 310 | CGSize naturalSize = [videoTrack naturalSize]; 311 | CGAffineTransform transform = videoTrack.preferredTransform; 312 | CGFloat videoAngleInDegree = atan2(transform.b, transform.a) * 180 / M_PI; 313 | if (videoAngleInDegree == 90 || videoAngleInDegree == -90) { 314 | CGFloat width = naturalSize.width; 315 | naturalSize.width = naturalSize.height; 316 | naturalSize.height = width; 317 | } 318 | videoComposition.renderSize = naturalSize; 319 | // center inside 320 | { 321 | float ratio; 322 | float xratio = targetSize.width / naturalSize.width; 323 | float yratio = targetSize.height / naturalSize.height; 324 | ratio = MIN(xratio, yratio); 325 | 326 | float postWidth = naturalSize.width * ratio; 327 | float postHeight = naturalSize.height * ratio; 328 | float transx = (targetSize.width - postWidth) / 2; 329 | float transy = (targetSize.height - postHeight) / 2; 330 | 331 | CGAffineTransform matrix = CGAffineTransformMakeTranslation(transx / xratio, transy / yratio); 332 | matrix = CGAffineTransformScale(matrix, ratio / xratio, ratio / yratio); 333 | transform = CGAffineTransformConcat(transform, matrix); 334 | } 335 | 336 | // Make a "pass through video track" video composition. 337 | AVMutableVideoCompositionInstruction *passThroughInstruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction]; 338 | passThroughInstruction.timeRange = CMTimeRangeMake(kCMTimeZero, self.asset.duration); 339 | 340 | AVMutableVideoCompositionLayerInstruction *passThroughLayer = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:videoTrack]; 341 | 342 | [passThroughLayer setTransform:transform atTime:kCMTimeZero]; 343 | 344 | passThroughInstruction.layerInstructions = @[passThroughLayer]; 345 | videoComposition.instructions = @[passThroughInstruction]; 346 | 347 | return videoComposition; 348 | } 349 | 350 | - (void)finish 351 | { 352 | // Synchronized block to ensure we never cancel the writer before calling finishWritingWithCompletionHandler 353 | if (self.reader.status == AVAssetReaderStatusCancelled || self.writer.status == AVAssetWriterStatusCancelled) 354 | { 355 | return; 356 | } 357 | 358 | if (self.writer.status == AVAssetWriterStatusFailed) 359 | { 360 | [self complete]; 361 | } 362 | else 363 | { 364 | [self.writer endSessionAtSourceTime:lastSamplePresentationTime]; 365 | [self.writer finishWritingWithCompletionHandler:^ 366 | { 367 | [self complete]; 368 | }]; 369 | } 370 | } 371 | 372 | - (void)complete 373 | { 374 | if (self.writer.status == AVAssetWriterStatusFailed || self.writer.status == AVAssetWriterStatusCancelled) 375 | { 376 | [NSFileManager.defaultManager removeItemAtURL:self.outputURL error:nil]; 377 | } 378 | 379 | if (self.completionHandler) 380 | { 381 | self.completionHandler(); 382 | self.completionHandler = nil; 383 | } 384 | } 385 | 386 | - (NSError *)error 387 | { 388 | if (_error) 389 | { 390 | return _error; 391 | } 392 | else 393 | { 394 | return self.writer.error ? : self.reader.error; 395 | } 396 | } 397 | 398 | - (AVAssetExportSessionStatus)status 399 | { 400 | switch (self.writer.status) 401 | { 402 | default: 403 | case AVAssetWriterStatusUnknown: 404 | return AVAssetExportSessionStatusUnknown; 405 | case AVAssetWriterStatusWriting: 406 | return AVAssetExportSessionStatusExporting; 407 | case AVAssetWriterStatusFailed: 408 | return AVAssetExportSessionStatusFailed; 409 | case AVAssetWriterStatusCompleted: 410 | return AVAssetExportSessionStatusCompleted; 411 | case AVAssetWriterStatusCancelled: 412 | return AVAssetExportSessionStatusCancelled; 413 | } 414 | } 415 | 416 | - (void)cancelExport 417 | { 418 | if (self.inputQueue) 419 | { 420 | dispatch_async(self.inputQueue, ^ 421 | { 422 | [self.writer cancelWriting]; 423 | [self.reader cancelReading]; 424 | [self complete]; 425 | [self reset]; 426 | }); 427 | } 428 | } 429 | 430 | - (void)reset 431 | { 432 | _error = nil; 433 | self.progress = 0; 434 | self.reader = nil; 435 | self.videoOutput = nil; 436 | self.audioOutput = nil; 437 | self.writer = nil; 438 | self.videoInput = nil; 439 | self.videoPixelBufferAdaptor = nil; 440 | self.audioInput = nil; 441 | self.inputQueue = nil; 442 | self.completionHandler = nil; 443 | } 444 | 445 | @end 446 | -------------------------------------------------------------------------------- /src/ios/VideoEditor.h: -------------------------------------------------------------------------------- 1 | // 2 | // VideoEditor.h 3 | // 4 | // Created by Josh Bavari on 01-14-2014 5 | // Modified by Ross Martin on 01-29-2015 6 | // 7 | 8 | #import 9 | #import 10 | #import 11 | #import 12 | 13 | #import 14 | 15 | enum CDVOutputFileType { 16 | M4V = 0, 17 | MPEG4 = 1, 18 | M4A = 2, 19 | QUICK_TIME = 3 20 | }; 21 | typedef NSUInteger CDVOutputFileType; 22 | 23 | @interface VideoEditor : CDVPlugin { 24 | } 25 | 26 | - (void)transcodeVideo:(CDVInvokedUrlCommand*)command; 27 | - (void) createThumbnail:(CDVInvokedUrlCommand*)command; 28 | - (void) getVideoInfo:(CDVInvokedUrlCommand*)command; 29 | - (void) trim:(CDVInvokedUrlCommand*)command; 30 | 31 | @end 32 | -------------------------------------------------------------------------------- /src/ios/VideoEditor.m: -------------------------------------------------------------------------------- 1 | // 2 | // VideoEditor.m 3 | // 4 | // Created by Josh Bavari on 01-14-2014 5 | // Modified by Ross Martin on 01-29-2015 6 | // 7 | 8 | #import 9 | #import "VideoEditor.h" 10 | #import "SDAVAssetExportSession.h" 11 | 12 | @interface VideoEditor () 13 | 14 | @end 15 | 16 | @implementation VideoEditor 17 | 18 | /** 19 | * transcodeVideo 20 | * 21 | * Transcodes a video 22 | * 23 | * ARGUMENTS 24 | * ========= 25 | * 26 | * fileUri - path to input video 27 | * outputFileName - output file name 28 | * outputFileType - output file type 29 | * saveToLibrary - save to gallery 30 | * maintainAspectRatio - make the output aspect ratio match the input video 31 | * width - width for the output video 32 | * height - height for the output video 33 | * videoBitrate - video bitrate for the output video in bits 34 | * audioChannels - number of audio channels for the output video 35 | * audioSampleRate - sample rate for the audio (samples per second) 36 | * audioBitrate - audio bitrate for the output video in bits 37 | * 38 | * RESPONSE 39 | * ======== 40 | * 41 | * outputFilePath - path to output file 42 | * 43 | * @param CDVInvokedUrlCommand command 44 | * @return void 45 | */ 46 | - (void) transcodeVideo:(CDVInvokedUrlCommand*)command 47 | { 48 | NSDictionary* options = [command.arguments objectAtIndex:0]; 49 | 50 | if ([options isKindOfClass:[NSNull class]]) { 51 | options = [NSDictionary dictionary]; 52 | } 53 | 54 | NSString *inputFilePath = [options objectForKey:@"fileUri"]; 55 | NSURL *inputFileURL = [self getURLFromFilePath:inputFilePath]; 56 | NSString *videoFileName = [options objectForKey:@"outputFileName"]; 57 | CDVOutputFileType outputFileType = ([options objectForKey:@"outputFileType"]) ? [[options objectForKey:@"outputFileType"] intValue] : MPEG4; 58 | BOOL optimizeForNetworkUse = ([options objectForKey:@"optimizeForNetworkUse"]) ? [[options objectForKey:@"optimizeForNetworkUse"] intValue] : NO; 59 | BOOL saveToPhotoAlbum = [options objectForKey:@"saveToLibrary"] ? [[options objectForKey:@"saveToLibrary"] boolValue] : YES; 60 | //float videoDuration = [[options objectForKey:@"duration"] floatValue]; 61 | BOOL maintainAspectRatio = [options objectForKey:@"maintainAspectRatio"] ? [[options objectForKey:@"maintainAspectRatio"] boolValue] : YES; 62 | float width = [[options objectForKey:@"width"] floatValue]; 63 | float height = [[options objectForKey:@"height"] floatValue]; 64 | int videoBitrate = ([options objectForKey:@"videoBitrate"]) ? [[options objectForKey:@"videoBitrate"] intValue] : 1000000; // default to 1 megabit 65 | int audioChannels = ([options objectForKey:@"audioChannels"]) ? [[options objectForKey:@"audioChannels"] intValue] : 2; 66 | int audioSampleRate = ([options objectForKey:@"audioSampleRate"]) ? [[options objectForKey:@"audioSampleRate"] intValue] : 44100; 67 | int audioBitrate = ([options objectForKey:@"audioBitrate"]) ? [[options objectForKey:@"audioBitrate"] intValue] : 128000; // default to 128 kilobits 68 | 69 | NSString *stringOutputFileType = Nil; 70 | NSString *outputExtension = Nil; 71 | 72 | switch (outputFileType) { 73 | case QUICK_TIME: 74 | stringOutputFileType = AVFileTypeQuickTimeMovie; 75 | outputExtension = @".mov"; 76 | break; 77 | case M4A: 78 | stringOutputFileType = AVFileTypeAppleM4A; 79 | outputExtension = @".m4a"; 80 | break; 81 | case M4V: 82 | stringOutputFileType = AVFileTypeAppleM4V; 83 | outputExtension = @".m4v"; 84 | break; 85 | case MPEG4: 86 | default: 87 | stringOutputFileType = AVFileTypeMPEG4; 88 | outputExtension = @".mp4"; 89 | break; 90 | } 91 | 92 | // check if the video can be saved to photo album before going further 93 | if (saveToPhotoAlbum && !UIVideoAtPathIsCompatibleWithSavedPhotosAlbum([inputFileURL path])) 94 | { 95 | NSString *error = @"Video cannot be saved to photo album"; 96 | [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:error ] callbackId:command.callbackId]; 97 | return; 98 | } 99 | 100 | AVURLAsset *avAsset = [AVURLAsset URLAssetWithURL:inputFileURL options:nil]; 101 | 102 | NSString *cacheDir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0]; 103 | NSString *outputPath = [NSString stringWithFormat:@"%@/%@%@", cacheDir, videoFileName, outputExtension]; 104 | NSURL *outputURL = [NSURL fileURLWithPath:outputPath]; 105 | 106 | NSArray *tracks = [avAsset tracksWithMediaType:AVMediaTypeVideo]; 107 | AVAssetTrack *track = [tracks objectAtIndex:0]; 108 | CGSize mediaSize = track.naturalSize; 109 | 110 | float videoWidth = mediaSize.width; 111 | float videoHeight = mediaSize.height; 112 | int newWidth; 113 | int newHeight; 114 | 115 | if (maintainAspectRatio) { 116 | float aspectRatio = videoWidth / videoHeight; 117 | 118 | // for some portrait videos ios gives the wrong width and height, this fixes that 119 | NSString *videoOrientation = [self getOrientationForTrack:avAsset]; 120 | if ([videoOrientation isEqual: @"portrait"]) { 121 | if (videoWidth > videoHeight) { 122 | videoWidth = mediaSize.height; 123 | videoHeight = mediaSize.width; 124 | aspectRatio = videoWidth / videoHeight; 125 | } 126 | } 127 | 128 | newWidth = (width && height) ? height * aspectRatio : videoWidth; 129 | newHeight = (width && height) ? newWidth / aspectRatio : videoHeight; 130 | } else { 131 | newWidth = (width && height) ? width : videoWidth; 132 | newHeight = (width && height) ? height : videoHeight; 133 | } 134 | 135 | NSLog(@"input videoWidth: %f", videoWidth); 136 | NSLog(@"input videoHeight: %f", videoHeight); 137 | NSLog(@"output newWidth: %d", newWidth); 138 | NSLog(@"output newHeight: %d", newHeight); 139 | 140 | SDAVAssetExportSession *encoder = [SDAVAssetExportSession.alloc initWithAsset:avAsset]; 141 | encoder.outputFileType = stringOutputFileType; 142 | encoder.outputURL = outputURL; 143 | encoder.shouldOptimizeForNetworkUse = optimizeForNetworkUse; 144 | encoder.videoSettings = @ 145 | { 146 | AVVideoCodecKey: AVVideoCodecH264, 147 | AVVideoWidthKey: [NSNumber numberWithInt: newWidth], 148 | AVVideoHeightKey: [NSNumber numberWithInt: newHeight], 149 | AVVideoCompressionPropertiesKey: @ 150 | { 151 | AVVideoAverageBitRateKey: [NSNumber numberWithInt: videoBitrate], 152 | AVVideoProfileLevelKey: AVVideoProfileLevelH264High40 153 | } 154 | }; 155 | encoder.audioSettings = @ 156 | { 157 | AVFormatIDKey: @(kAudioFormatMPEG4AAC), 158 | AVNumberOfChannelsKey: [NSNumber numberWithInt: audioChannels], 159 | AVSampleRateKey: [NSNumber numberWithInt: audioSampleRate], 160 | AVEncoderBitRateKey: [NSNumber numberWithInt: audioBitrate] 161 | }; 162 | 163 | /* // setting timeRange is not possible due to a bug with SDAVAssetExportSession (https://github.com/rs/SDAVAssetExportSession/issues/28) 164 | if (videoDuration) { 165 | int32_t preferredTimeScale = 600; 166 | CMTime startTime = CMTimeMakeWithSeconds(0, preferredTimeScale); 167 | CMTime stopTime = CMTimeMakeWithSeconds(videoDuration, preferredTimeScale); 168 | CMTimeRange exportTimeRange = CMTimeRangeFromTimeToTime(startTime, stopTime); 169 | encoder.timeRange = exportTimeRange; 170 | } 171 | */ 172 | 173 | // Set up a semaphore for the completion handler and progress timer 174 | dispatch_semaphore_t sessionWaitSemaphore = dispatch_semaphore_create(0); 175 | 176 | void (^completionHandler)(void) = ^(void) 177 | { 178 | dispatch_semaphore_signal(sessionWaitSemaphore); 179 | }; 180 | 181 | // do it 182 | 183 | [self.commandDelegate runInBackground:^{ 184 | [encoder exportAsynchronouslyWithCompletionHandler:completionHandler]; 185 | 186 | do { 187 | dispatch_time_t dispatchTime = DISPATCH_TIME_FOREVER; // if we dont want progress, we will wait until it finishes. 188 | dispatchTime = getDispatchTimeFromSeconds((float)1.0); 189 | double progress = [encoder progress] * 100; 190 | 191 | NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init]; 192 | [dictionary setValue: [NSNumber numberWithDouble: progress] forKey: @"progress"]; 193 | 194 | CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary: dictionary]; 195 | 196 | [result setKeepCallbackAsBool:YES]; 197 | [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; 198 | dispatch_semaphore_wait(sessionWaitSemaphore, dispatchTime); 199 | } while( [encoder status] < AVAssetExportSessionStatusCompleted ); 200 | 201 | // this is kinda odd but must be done 202 | if ([encoder status] == AVAssetExportSessionStatusCompleted) { 203 | NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init]; 204 | // AVAssetExportSessionStatusCompleted will not always mean progress is 100 so hard code it below 205 | double progress = 100.00; 206 | [dictionary setValue: [NSNumber numberWithDouble: progress] forKey: @"progress"]; 207 | 208 | CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary: dictionary]; 209 | 210 | [result setKeepCallbackAsBool:YES]; 211 | [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; 212 | } 213 | 214 | if (encoder.status == AVAssetExportSessionStatusCompleted) 215 | { 216 | NSLog(@"Video export succeeded"); 217 | if (saveToPhotoAlbum) { 218 | UISaveVideoAtPathToSavedPhotosAlbum(outputPath, self, nil, nil); 219 | } 220 | [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:outputPath] callbackId:command.callbackId]; 221 | } 222 | else if (encoder.status == AVAssetExportSessionStatusCancelled) 223 | { 224 | NSLog(@"Video export cancelled"); 225 | [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"Video export cancelled"] callbackId:command.callbackId]; 226 | } 227 | else 228 | { 229 | NSString *error = [NSString stringWithFormat:@"Video export failed with error: %@ (%ld)", encoder.error.localizedDescription, (long)encoder.error.code]; 230 | [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:error] callbackId:command.callbackId]; 231 | } 232 | }]; 233 | } 234 | 235 | /** 236 | * createThumbnail 237 | * 238 | * Creates a thumbnail from the start of a video. 239 | * 240 | * ARGUMENTS 241 | * ========= 242 | * fileUri - input file path 243 | * outputFileName - output file name 244 | * atTime - location in the video to create the thumbnail (in seconds), 245 | * width - width of the thumbnail (optional) 246 | * height - height of the thumbnail (optional) 247 | * quality - quality of the thumbnail (between 1 and 100) 248 | * 249 | * RESPONSE 250 | * ======== 251 | * 252 | * outputFilePath - path to output file 253 | * 254 | * @param CDVInvokedUrlCommand command 255 | * @return void 256 | */ 257 | - (void) createThumbnail:(CDVInvokedUrlCommand*)command 258 | { 259 | NSLog(@"createThumbnail"); 260 | NSDictionary* options = [command.arguments objectAtIndex:0]; 261 | 262 | if ([options isKindOfClass:[NSNull class]]) { 263 | options = [NSDictionary dictionary]; 264 | } 265 | 266 | NSString* srcVideoPath = [options objectForKey:@"fileUri"]; 267 | NSString* outputFileName = [options objectForKey:@"outputFileName"]; 268 | float atTime = ([options objectForKey:@"atTime"]) ? [[options objectForKey:@"atTime"] floatValue] : 0; 269 | float width = [[options objectForKey:@"width"] floatValue]; 270 | float height = [[options objectForKey:@"height"] floatValue]; 271 | float quality = ([options objectForKey:@"quality"]) ? [[options objectForKey:@"quality"] floatValue] : 100; 272 | float thumbQuality = quality * 1.0 / 100; 273 | 274 | int32_t preferredTimeScale = 600; 275 | CMTime time = CMTimeMakeWithSeconds(atTime, preferredTimeScale); 276 | 277 | UIImage* thumbnail = [self generateThumbnailImage:srcVideoPath atTime:time]; 278 | 279 | if (width && height) { 280 | NSLog(@"got width and height, resizing image"); 281 | CGSize newSize = CGSizeMake(width, height); 282 | thumbnail = [self scaleImage:thumbnail toSize:newSize]; 283 | NSLog(@"new size of thumbnail, width x height = %f x %f", thumbnail.size.width, thumbnail.size.height); 284 | } 285 | 286 | NSString *cacheDir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0]; 287 | NSString *outputFilePath = [cacheDir stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.%@", outputFileName, @"jpg"]]; 288 | 289 | // write out the thumbnail 290 | if ([UIImageJPEGRepresentation(thumbnail, thumbQuality) writeToFile:outputFilePath atomically:YES]) 291 | { 292 | NSLog(@"path to your video thumbnail: %@", outputFilePath); 293 | [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:outputFilePath] callbackId:command.callbackId]; 294 | } 295 | else 296 | { 297 | [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"failed to create thumbnail file"] callbackId:command.callbackId]; 298 | } 299 | } 300 | 301 | /** 302 | * getVideoInfo 303 | * 304 | * Creates a thumbnail from the start of a video. 305 | * 306 | * ARGUMENTS 307 | * ========= 308 | * fileUri - input file path 309 | * 310 | * RESPONSE 311 | * ======== 312 | * 313 | * width - width of the video 314 | * height - height of the video 315 | * orientation - orientation of the video 316 | * duration - duration of the video (in seconds) 317 | * size - size of the video (in bytes) 318 | * bitrate - bitrate of the video (in bits per second) 319 | * videoMediaType - Media type of the video 320 | * audioMediaType - Media type of the audio track in video 321 | * 322 | * @param CDVInvokedUrlCommand command 323 | * @return void 324 | */ 325 | - (void) getVideoInfo:(CDVInvokedUrlCommand*)command 326 | { 327 | NSLog(@"getVideoInfo"); 328 | NSDictionary* options = [command.arguments objectAtIndex:0]; 329 | 330 | if ([options isKindOfClass:[NSNull class]]) { 331 | options = [NSDictionary dictionary]; 332 | } 333 | 334 | NSString *filePath = [options objectForKey:@"fileUri"]; 335 | NSURL *fileURL = [self getURLFromFilePath:filePath]; 336 | 337 | unsigned long long size = [[NSFileManager defaultManager] attributesOfItemAtPath:[fileURL path] error:nil].fileSize; 338 | 339 | AVURLAsset *avAsset = [AVURLAsset URLAssetWithURL:fileURL options:nil]; 340 | 341 | NSArray *videoTracks = [avAsset tracksWithMediaType:AVMediaTypeVideo]; 342 | NSArray *audioTracks = [avAsset tracksWithMediaType:AVMediaTypeAudio]; 343 | AVAssetTrack *videoTrack = [videoTracks objectAtIndex:0]; 344 | AVAssetTrack *audioTrack = nil; 345 | if (audioTracks.count > 0) { 346 | audioTrack = [audioTracks objectAtIndex:0]; 347 | } 348 | 349 | NSString *videoMediaType = nil; 350 | NSString *audioMediaType = nil; 351 | if (videoTrack.formatDescriptions.count > 0) { 352 | videoMediaType = getMediaTypeFromDescription(videoTrack.formatDescriptions[0]); 353 | } 354 | if (audioTrack != nil && audioTrack.formatDescriptions.count > 0) { 355 | audioMediaType = getMediaTypeFromDescription(audioTrack.formatDescriptions[0]); 356 | } 357 | 358 | CGSize mediaSize = videoTrack.naturalSize; 359 | float videoWidth = mediaSize.width; 360 | float videoHeight = mediaSize.height; 361 | float aspectRatio = videoWidth / videoHeight; 362 | 363 | // for some portrait videos ios gives the wrong width and height, this fixes that 364 | NSString *videoOrientation = [self getOrientationForTrack:avAsset]; 365 | if ([videoOrientation isEqual: @"portrait"]) { 366 | if (videoWidth > videoHeight) { 367 | videoWidth = mediaSize.height; 368 | videoHeight = mediaSize.width; 369 | aspectRatio = videoWidth / videoHeight; 370 | } 371 | } 372 | 373 | NSMutableDictionary *dict = [[NSMutableDictionary alloc] init]; 374 | [dict setObject:[NSNumber numberWithFloat:videoWidth] forKey:@"width"]; 375 | [dict setObject:[NSNumber numberWithFloat:videoHeight] forKey:@"height"]; 376 | [dict setValue:videoOrientation forKey:@"orientation"]; 377 | [dict setValue:[NSNumber numberWithFloat:videoTrack.timeRange.duration.value / 600.0] forKey:@"duration"]; 378 | [dict setObject:[NSNumber numberWithLongLong:size] forKey:@"size"]; 379 | [dict setObject:[NSNumber numberWithFloat:videoTrack.estimatedDataRate] forKey:@"bitrate"]; 380 | [dict setValue:videoMediaType forKey:@"videoMediaType"]; 381 | [dict setValue:audioMediaType forKey:@"audioMediaType"]; 382 | 383 | [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dict] callbackId:command.callbackId]; 384 | } 385 | 386 | /** 387 | * trim 388 | * 389 | * Performs a trim operation on a clip, while encoding it. 390 | * 391 | * ARGUMENTS 392 | * ========= 393 | * fileUri - input file path 394 | * trimStart - time to start trimming 395 | * trimEnd - time to end trimming 396 | * outputFileName - output file name 397 | * progress: - optional callback function that receives progress info 398 | * 399 | * RESPONSE 400 | * ======== 401 | * 402 | * outputFilePath - path to output file 403 | * 404 | * @param CDVInvokedUrlCommand command 405 | * @return void 406 | */ 407 | - (void) trim:(CDVInvokedUrlCommand*)command { 408 | NSLog(@"[Trim]: trim called"); 409 | 410 | // extract arguments 411 | NSDictionary* options = [command.arguments objectAtIndex:0]; 412 | if ([options isKindOfClass:[NSNull class]]) { 413 | options = [NSDictionary dictionary]; 414 | } 415 | NSString *inputFilePath = [options objectForKey:@"fileUri"]; 416 | NSURL *inputFileURL = [self getURLFromFilePath:inputFilePath]; 417 | float trimStart = [[options objectForKey:@"trimStart"] floatValue]; 418 | float trimEnd = [[options objectForKey:@"trimEnd"] floatValue]; 419 | NSString *outputName = [options objectForKey:@"outputFileName"]; 420 | 421 | NSFileManager *fileMgr = [NSFileManager defaultManager]; 422 | NSString *cacheDir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0]; 423 | 424 | // videoDir 425 | NSString *videoDir = [cacheDir stringByAppendingPathComponent:@"mp4"]; 426 | if ([fileMgr createDirectoryAtPath:videoDir withIntermediateDirectories:YES attributes:nil error: NULL] == NO){ 427 | [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"failed to create video dir"] callbackId:command.callbackId]; 428 | return; 429 | } 430 | NSString *videoOutput = [videoDir stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.%@", outputName, @"mp4"]]; 431 | 432 | NSLog(@"[Trim]: inputFilePath: %@", inputFilePath); 433 | NSLog(@"[Trim]: outputPath: %@", videoOutput); 434 | 435 | // run in background 436 | [self.commandDelegate runInBackground:^{ 437 | 438 | AVURLAsset *avAsset = [AVURLAsset URLAssetWithURL:inputFileURL options:nil]; 439 | 440 | AVAssetExportSession *exportSession = [[AVAssetExportSession alloc]initWithAsset:avAsset presetName: AVAssetExportPresetHighestQuality]; 441 | exportSession.outputURL = [NSURL fileURLWithPath:videoOutput]; 442 | exportSession.outputFileType = AVFileTypeQuickTimeMovie; 443 | exportSession.shouldOptimizeForNetworkUse = YES; 444 | 445 | int32_t preferredTimeScale = 600; 446 | CMTime startTime = CMTimeMakeWithSeconds(trimStart, preferredTimeScale); 447 | CMTime stopTime = CMTimeMakeWithSeconds(trimEnd, preferredTimeScale); 448 | CMTimeRange exportTimeRange = CMTimeRangeFromTimeToTime(startTime, stopTime); 449 | exportSession.timeRange = exportTimeRange; 450 | 451 | // debug timings 452 | NSString *trimStart = (NSString *) CFBridgingRelease(CMTimeCopyDescription(NULL, startTime)); 453 | NSString *trimEnd = (NSString *) CFBridgingRelease(CMTimeCopyDescription(NULL, stopTime)); 454 | NSLog(@"[Trim]: duration: %lld, trimStart: %@, trimEnd: %@", avAsset.duration.value, trimStart, trimEnd); 455 | 456 | // Set up a semaphore for the completion handler and progress timer 457 | dispatch_semaphore_t sessionWaitSemaphore = dispatch_semaphore_create(0); 458 | 459 | void (^completionHandler)(void) = ^(void) 460 | { 461 | dispatch_semaphore_signal(sessionWaitSemaphore); 462 | }; 463 | 464 | // do it 465 | [exportSession exportAsynchronouslyWithCompletionHandler:completionHandler]; 466 | 467 | do { 468 | dispatch_time_t dispatchTime = DISPATCH_TIME_FOREVER; // if we dont want progress, we will wait until it finishes. 469 | dispatchTime = getDispatchTimeFromSeconds((float)1.0); 470 | double progress = [exportSession progress] * 100; 471 | 472 | NSLog([NSString stringWithFormat:@"AVAssetExport running progress=%3.2f%%", progress]); 473 | 474 | NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init]; 475 | [dictionary setValue: [NSNumber numberWithDouble: progress] forKey: @"progress"]; 476 | 477 | CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary: dictionary]; 478 | 479 | [result setKeepCallbackAsBool:YES]; 480 | [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; 481 | dispatch_semaphore_wait(sessionWaitSemaphore, dispatchTime); 482 | } while( [exportSession status] < AVAssetExportSessionStatusCompleted ); 483 | 484 | // this is kinda odd but must be done 485 | if ([exportSession status] == AVAssetExportSessionStatusCompleted) { 486 | NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init]; 487 | // AVAssetExportSessionStatusCompleted will not always mean progress is 100 so hard code it below 488 | double progress = 100.00; 489 | [dictionary setValue: [NSNumber numberWithDouble: progress] forKey: @"progress"]; 490 | 491 | CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary: dictionary]; 492 | 493 | [result setKeepCallbackAsBool:YES]; 494 | [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; 495 | } 496 | 497 | switch ([exportSession status]) { 498 | case AVAssetExportSessionStatusCompleted: 499 | NSLog(@"[Trim]: Export Complete %d %@", exportSession.status, exportSession.error); 500 | [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:videoOutput] callbackId:command.callbackId]; 501 | break; 502 | case AVAssetExportSessionStatusFailed: 503 | NSLog(@"[Trim]: Export failed: %@", [[exportSession error] localizedDescription]); 504 | [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:[[exportSession error] localizedDescription]] callbackId:command.callbackId]; 505 | break; 506 | case AVAssetExportSessionStatusCancelled: 507 | NSLog(@"[Trim]: Export canceled"); 508 | break; 509 | default: 510 | NSLog(@"[Trim]: Export default in switch"); 511 | break; 512 | } 513 | 514 | }]; 515 | } 516 | 517 | // modified version of http://stackoverflow.com/a/21230645/1673842 518 | - (UIImage *)generateThumbnailImage: (NSString *)srcVideoPath atTime:(CMTime)time 519 | { 520 | NSURL *url = [NSURL fileURLWithPath:srcVideoPath]; 521 | 522 | if ([srcVideoPath rangeOfString:@"://"].location == NSNotFound) 523 | { 524 | url = [NSURL URLWithString:[[@"file://localhost" stringByAppendingString:srcVideoPath] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; 525 | } 526 | else 527 | { 528 | url = [NSURL URLWithString:[srcVideoPath stringByAddingPercentEscapesUsingEncoding: NSUTF8StringEncoding]]; 529 | } 530 | 531 | AVAsset *asset = [AVAsset assetWithURL:url]; 532 | AVAssetImageGenerator *imageGenerator = [[AVAssetImageGenerator alloc] initWithAsset:asset]; 533 | imageGenerator.requestedTimeToleranceAfter = kCMTimeZero; // needed to get a precise time (http://stackoverflow.com/questions/5825990/i-cannot-get-a-precise-cmtime-for-generating-still-image-from-1-8-second-video) 534 | imageGenerator.requestedTimeToleranceBefore = kCMTimeZero; // ^^ 535 | imageGenerator.appliesPreferredTrackTransform = YES; // crucial to have the right orientation for the image (http://stackoverflow.com/questions/9145968/getting-video-snapshot-for-thumbnail) 536 | CGImageRef imageRef = [imageGenerator copyCGImageAtTime:time actualTime:NULL error:NULL]; 537 | UIImage *thumbnail = [UIImage imageWithCGImage:imageRef]; 538 | CGImageRelease(imageRef); // CGImageRef won't be released by ARC 539 | 540 | return thumbnail; 541 | } 542 | 543 | // to scale images without changing aspect ratio (http://stackoverflow.com/a/8224161/1673842) 544 | - (UIImage*)scaleImage:(UIImage*)image 545 | toSize:(CGSize)newSize; 546 | { 547 | float oldWidth = image.size.width; 548 | float scaleFactor = newSize.width / oldWidth; 549 | 550 | float newHeight = image.size.height * scaleFactor; 551 | float newWidth = oldWidth * scaleFactor; 552 | 553 | UIGraphicsBeginImageContext(CGSizeMake(newWidth, newHeight)); 554 | [image drawInRect:CGRectMake(0, 0, newWidth, newHeight)]; 555 | UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext(); 556 | UIGraphicsEndImageContext(); 557 | return newImage; 558 | } 559 | 560 | // inspired by http://stackoverflow.com/a/6046421/1673842 561 | - (NSString*)getOrientationForTrack:(AVAsset *)asset 562 | { 563 | AVAssetTrack *videoTrack = [[asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0]; 564 | CGSize size = [videoTrack naturalSize]; 565 | CGAffineTransform txf = [videoTrack preferredTransform]; 566 | 567 | if (size.width == txf.tx && size.height == txf.ty) 568 | return @"landscape"; 569 | else if (txf.tx == 0 && txf.ty == 0) 570 | return @"landscape"; 571 | else if (txf.tx == 0 && txf.ty == size.width) 572 | return @"portrait"; 573 | else 574 | return @"portrait"; 575 | } 576 | 577 | - (NSURL*)getURLFromFilePath:(NSString*)filePath 578 | { 579 | if ([filePath containsString:@"assets-library://"]) { 580 | return [NSURL URLWithString:[filePath stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; 581 | } else if ([filePath containsString:@"file://"]) { 582 | return [NSURL URLWithString:[filePath stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; 583 | } 584 | 585 | return [NSURL fileURLWithPath:[filePath stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; 586 | } 587 | 588 | static NSString* getMediaTypeFromDescription(id description) { 589 | CMFormatDescriptionRef desc = (__bridge CMFormatDescriptionRef)description; 590 | FourCharCode code = CMFormatDescriptionGetMediaSubType(desc); 591 | 592 | NSString *result = [NSString stringWithFormat:@"%c%c%c%c", 593 | (code >> 24) & 0xff, 594 | (code >> 16) & 0xff, 595 | (code >> 8) & 0xff, 596 | code & 0xff]; 597 | NSCharacterSet *characterSet = [NSCharacterSet whitespaceCharacterSet]; 598 | return [result stringByTrimmingCharactersInSet:characterSet]; 599 | } 600 | 601 | static dispatch_time_t getDispatchTimeFromSeconds(float seconds) { 602 | long long milliseconds = seconds * 1000.0; 603 | dispatch_time_t waitTime = dispatch_time( DISPATCH_TIME_NOW, 1000000LL * milliseconds ); 604 | return waitTime; 605 | } 606 | 607 | @end 608 | -------------------------------------------------------------------------------- /src/windows/VideoEditorProxy.js: -------------------------------------------------------------------------------- 1 |  2 | module.exports = { 3 | 4 | trim: function (win, fail, args) { 5 | //get args from cordova app 6 | var options = args[0]; 7 | var comp, outboundFilePath; 8 | 9 | //look up the video file 10 | Windows.Storage.StorageFile.getFileFromApplicationUriAsync(new Windows.Foundation.Uri(options.fileUri)).then(function (file) { 11 | //create a clip from the video 12 | return Windows.Media.Editing.MediaClip.createFromFileAsync(file); 13 | }).then(function (clip) { 14 | //apply the trims, which are in milliseconds 15 | clip.trimTimeFromStart = options.trimStart * 1000; 16 | clip.trimTimeFromEnd = (options.trimEnd * 1000); 17 | 18 | //setup a comp 19 | comp = new Windows.Media.Editing.MediaComposition(); 20 | comp.clips.push(clip); 21 | 22 | //create an outbound file location 23 | return Windows.Storage.ApplicationData.current.localFolder.createFileAsync(options.outputFileName, Windows.Storage.CreationCollisionOption.replaceExisting); 24 | }).then(function (outboundFile) { 25 | outboundFilePath = outboundFile.path; 26 | //render the trimmed video to file 27 | return comp.renderToFileAsync(outboundFile); 28 | }).done(function (file) { 29 | //return the path to the success method 30 | win(outboundFilePath); 31 | }); 32 | }, 33 | 34 | createThumbnail: function (win, fail, args) { 35 | //get args from cordova app 36 | var options = args[0]; 37 | var comp, outputStream, writer, reader, thumbnailStream, outboundFilePath; 38 | 39 | //look up the video file 40 | Windows.Storage.StorageFile.getFileFromApplicationUriAsync(new Windows.Foundation.Uri(options.fileUri)).then(function (file) { 41 | //create clip from video 42 | return Windows.Media.Editing.MediaClip.createFromFileAsync(file); 43 | }).then(function (clip) { 44 | //setup a comp 45 | comp = new Windows.Media.Editing.MediaComposition(); 46 | comp.clips.push(clip); 47 | 48 | //create an outbound file location 49 | return Windows.Storage.ApplicationData.current.localFolder.createFileAsync(options.outputFileName, Windows.Storage.CreationCollisionOption.replaceExisting); 50 | }).then(function (outboundFile) { 51 | //store outbound file location in a temp variable 52 | outboundFilePath = outboundFile.path; 53 | //open the file for writing 54 | return outboundFile.openAsync(Windows.Storage.FileAccessMode.readWrite); 55 | }).then(function (outboundStream) { 56 | //prepare the output stream 57 | outputStream = outboundStream.getOutputStreamAt(0); 58 | //take a snip from the video 59 | return comp.getThumbnailAsync(0, 1080, 920, Windows.Media.Editing.VideoFramePrecision.nearestFrame); 60 | }).then(function (thumbnail) { 61 | //keep reference so we can dispose later 62 | thumbnailStream = thumbnail; 63 | //keep reference so we can dispose later 64 | reader = new Windows.Storage.Streams.DataReader(thumbnailStream.getInputStreamAt(0)); 65 | 66 | //load the thumbprint 67 | return reader.loadAsync(thumbnailStream.size); 68 | }).then(function () { 69 | //keep reference so we can dispose later 70 | writer = new Windows.Storage.Streams.DataWriter(outputStream); 71 | //writer to the buffer 72 | while (reader.unconsumedBufferLength > 0) { 73 | writer.writeBuffer(reader.readBuffer(((reader.unconsumedBufferLength > 64) ? 64 : reader.unconsumedBufferLength))); 74 | } 75 | //transfer the buffer and write 76 | return outputStream.writeAsync(writer.detachBuffer()); 77 | }).then(function (bytesWritten) { 78 | console.log('Bytes written ' + bytesWritten); 79 | //clear the stream 80 | return outputStream.flushAsync(); 81 | }).done(function (outboundFile) { 82 | //dispose all references 83 | writer.close(); 84 | reader.close(); 85 | thumbnailStream.close(); 86 | //call win function for cordova 87 | win(outboundFilePath); 88 | }); 89 | }, 90 | 91 | transcodeVideo: function (win, fail, args) { 92 | var videoQualities = Windows.Media.MediaProperties.VideoEncodingQuality; 93 | 94 | //get args from cordova app 95 | var options = args[0]; 96 | //easier way to access the quality 97 | var qualities = [videoQualities.hd1080p, videoQualities.hd720p, videoQualities.wvga]; 98 | //chosen quality 99 | var quality = qualities[options.quality]; 100 | var mediaProfile, sourceFile, destinationFile, duration = options.duration, optimize = options.optimizeForNetworkUse; 101 | 102 | switch (options.outputFileType) { 103 | //both m4v and mpeg4 transcode to mp4 104 | case 0: 105 | case 1:{ 106 | mediaProfile = Windows.Media.MediaProperties.MediaEncodingProfile.createMp4(quality); 107 | break; 108 | } 109 | //m4a transcoded with m4a 110 | case 2:{ 111 | mediaProfile = Windows.Media.MediaProperties.MediaEncodingProfile.createM4a(quality); 112 | break; 113 | } 114 | //we don't support anything more 115 | default:{ 116 | throw 'output file type not supported on windows with this format'; 117 | break; 118 | } 119 | 120 | } 121 | 122 | //get the source file 123 | Windows.Storage.StorageFile.getFileFromApplicationUriAsync(new Windows.Foundation.Uri(options.fileUri)).then(function (source) { 124 | sourceFile = source; 125 | //create the destination file 126 | return Windows.Storage.ApplicationData.current.localFolder.createFileAsync(options.outputFileName, Windows.Storage.CreationCollisionOption.replaceExisting); 127 | }).then(function (destination) { 128 | destinationFile = destination; 129 | 130 | var transcoder = new Windows.Media.Transcoding.MediaTranscoder(); 131 | //quality over speed and performance 132 | if (!optimize) { 133 | transcoder.videoProcessingAlgorithm = Windows.Media.Transcoding.MediaVideoProcessingAlgorithm.mrfCrf444 134 | } 135 | //prepare transcoding 136 | return transcoder.prepareFileTranscodeAsync(sourceFile, destinationFile, mediaProfile); 137 | }).then(function (preparedTranscode) { 138 | //perform the transcode 139 | return preparedTranscode.transcodeAsync(); 140 | }).done(function () { 141 | //return the destination file path 142 | win(destinationFile.path); 143 | }, function (details) { 144 | //failed 145 | fail(details); 146 | }); 147 | }, 148 | 149 | /** 150 | * getVideoInfo 151 | * 152 | * Get common video info for the uri passed in 153 | * 154 | * ARGUMENTS 155 | * ========= 156 | * fileUri - input file path 157 | * 158 | * RESPONSE 159 | * ======== 160 | * 161 | * width - width of the video 162 | * height - height of the video 163 | * orientation - orientation of the video 164 | * duration - duration of the video (in seconds) 165 | * size - size of the video (in bytes) 166 | * bitrate - bitrate of the video (in bits per second) 167 | * 168 | * @param promise win 169 | * @param promise fail 170 | * @param object args 171 | * @return void 172 | */ 173 | getVideoInfo: function (win, fail, args) { 174 | //get args from cordova app 175 | var options = args[0]; 176 | 177 | var file, basicProps; 178 | 179 | //look up the video file 180 | Windows.Storage.StorageFile.getFileFromApplicationUriAsync(new Windows.Foundation.Uri(options.fileUri)).then(function (storageFile) { 181 | //assign storage file to global variable 182 | file = storageFile; 183 | //get basic properties for size 184 | return file.getBasicPropertiesAsync(); 185 | }).then(function (basicProperties) { 186 | basicProps = basicProperties; 187 | //get video properties for the rest of info 188 | return file.properties.getVideoPropertiesAsync(); 189 | }).done(function (videoProps) { 190 | //resolve the video info 191 | win({ 192 | width: videoProps.width, 193 | height: videoProps.height, 194 | orientation: (videoProps.height > videoProps.width) ? 'portrait' :'landscape', 195 | duration: videoProps.duration, 196 | size: basicProps.size, 197 | bitrate: videoProps.bitrate 198 | }); 199 | }, function (details) { 200 | //failed 201 | fail(details); 202 | }); 203 | } 204 | } 205 | 206 | require("cordova/exec/proxy").add("VideoEditor", module.exports); -------------------------------------------------------------------------------- /src/windows8/VideoEditorProxy.js: -------------------------------------------------------------------------------- 1 |  2 | module.exports = { 3 | 4 | trim: function (win, fail, args) { 5 | //get args from cordova app 6 | var options = args[0]; 7 | var comp, outboundFilePath; 8 | 9 | //look up the video file 10 | Windows.Storage.StorageFile.getFileFromApplicationUriAsync(new Windows.Foundation.Uri(options.fileUri)).then(function (file) { 11 | //create a clip from the video 12 | return Windows.Media.Editing.MediaClip.createFromFileAsync(file); 13 | }).then(function (clip) { 14 | //apply the trims, which are in milliseconds 15 | clip.trimTimeFromStart = options.trimStart * 1000; 16 | clip.trimTimeFromEnd = (options.trimEnd * 1000); 17 | 18 | //setup a comp 19 | comp = new Windows.Media.Editing.MediaComposition(); 20 | comp.clips.push(clip); 21 | 22 | //create an outbound file location 23 | return Windows.Storage.ApplicationData.current.localFolder.createFileAsync(options.outputFileName, Windows.Storage.CreationCollisionOption.replaceExisting); 24 | }).then(function (outboundFile) { 25 | outboundFilePath = outboundFile.path; 26 | //render the trimmed video to file 27 | return comp.renderToFileAsync(outboundFile); 28 | }).done(function (file) { 29 | //return the path to the success method 30 | win(outboundFilePath); 31 | }); 32 | }, 33 | 34 | createThumbnail: function (win, fail, args) { 35 | //get args from cordova app 36 | var options = args[0]; 37 | var comp, outputStream, writer, reader, thumbnailStream, outboundFilePath; 38 | 39 | //look up the video file 40 | Windows.Storage.StorageFile.getFileFromApplicationUriAsync(new Windows.Foundation.Uri(options.fileUri)).then(function (file) { 41 | //create clip from video 42 | return Windows.Media.Editing.MediaClip.createFromFileAsync(file); 43 | }).then(function (clip) { 44 | //setup a comp 45 | comp = new Windows.Media.Editing.MediaComposition(); 46 | comp.clips.push(clip); 47 | 48 | //create an outbound file location 49 | return Windows.Storage.ApplicationData.current.localFolder.createFileAsync(options.outputFileName, Windows.Storage.CreationCollisionOption.replaceExisting); 50 | }).then(function (outboundFile) { 51 | //store outbound file location in a temp variable 52 | outboundFilePath = outboundFile.path; 53 | //open the file for writing 54 | return outboundFile.openAsync(Windows.Storage.FileAccessMode.readWrite); 55 | }).then(function (outboundStream) { 56 | //prepare the output stream 57 | outputStream = outboundStream.getOutputStreamAt(0); 58 | //take a snip from the video 59 | return comp.getThumbnailAsync(0, 1080, 920, Windows.Media.Editing.VideoFramePrecision.nearestFrame); 60 | }).then(function (thumbnail) { 61 | //keep reference so we can dispose later 62 | thumbnailStream = thumbnail; 63 | //keep reference so we can dispose later 64 | reader = new Windows.Storage.Streams.DataReader(thumbnailStream.getInputStreamAt(0)); 65 | 66 | //load the thumbprint 67 | return reader.loadAsync(thumbnailStream.size); 68 | }).then(function () { 69 | //keep reference so we can dispose later 70 | writer = new Windows.Storage.Streams.DataWriter(outputStream); 71 | //writer to the buffer 72 | while (reader.unconsumedBufferLength > 0) { 73 | writer.writeBuffer(reader.readBuffer(((reader.unconsumedBufferLength > 64) ? 64 : reader.unconsumedBufferLength))); 74 | } 75 | //transfer the buffer and write 76 | return outputStream.writeAsync(writer.detachBuffer()); 77 | }).then(function (bytesWritten) { 78 | console.log('Bytes written ' + bytesWritten); 79 | //clear the stream 80 | return outputStream.flushAsync(); 81 | }).done(function (outboundFile) { 82 | //dispose all references 83 | writer.close(); 84 | reader.close(); 85 | thumbnailStream.close(); 86 | //call win function for cordova 87 | win(outboundFilePath); 88 | }); 89 | }, 90 | 91 | transcodeVideo: function (win, fail, args) { 92 | var videoQualities = Windows.Media.MediaProperties.VideoEncodingQuality; 93 | 94 | //get args from cordova app 95 | var options = args[0]; 96 | //easier way to access the quality 97 | var qualities = [videoQualities.hd1080p, videoQualities.hd720p, videoQualities.wvga]; 98 | //chosen quality 99 | var quality = qualities[options.quality]; 100 | var mediaProfile, sourceFile, destinationFile, duration = options.duration, optimize = options.optimizeForNetworkUse; 101 | 102 | switch (options.outputFileType) { 103 | //both m4v and mpeg4 transcode to mp4 104 | case 0: 105 | case 1:{ 106 | mediaProfile = Windows.Media.MediaProperties.MediaEncodingProfile.createMp4(quality); 107 | break; 108 | } 109 | //m4a transcoded with m4a 110 | case 2:{ 111 | mediaProfile = Windows.Media.MediaProperties.MediaEncodingProfile.createM4a(quality); 112 | break; 113 | } 114 | //we don't support anything more 115 | default:{ 116 | throw 'output file type not supported on windows with this format'; 117 | break; 118 | } 119 | 120 | } 121 | 122 | //get the source file 123 | Windows.Storage.StorageFile.getFileFromApplicationUriAsync(new Windows.Foundation.Uri(options.fileUri)).then(function (source) { 124 | sourceFile = source; 125 | //create the destination file 126 | return Windows.Storage.ApplicationData.current.localFolder.createFileAsync(options.outputFileName, Windows.Storage.CreationCollisionOption.replaceExisting); 127 | }).then(function (destination) { 128 | destinationFile = destination; 129 | 130 | var transcoder = new Windows.Media.Transcoding.MediaTranscoder(); 131 | //quality over speed and performance 132 | if (!optimize) { 133 | transcoder.videoProcessingAlgorithm = Windows.Media.Transcoding.MediaVideoProcessingAlgorithm.mrfCrf444 134 | } 135 | //prepare transcoding 136 | return transcoder.prepareFileTranscodeAsync(sourceFile, destinationFile, mediaProfile); 137 | }).then(function (preparedTranscode) { 138 | //perform the transcode 139 | return preparedTranscode.transcodeAsync(); 140 | }).done(function () { 141 | //return the destination file path 142 | win(destinationFile.path); 143 | }, function (details) { 144 | //failed 145 | fail(details); 146 | }); 147 | }, 148 | 149 | /** 150 | * getVideoInfo 151 | * 152 | * Get common video info for the uri passed in 153 | * 154 | * ARGUMENTS 155 | * ========= 156 | * fileUri - input file path 157 | * 158 | * RESPONSE 159 | * ======== 160 | * 161 | * width - width of the video 162 | * height - height of the video 163 | * orientation - orientation of the video 164 | * duration - duration of the video (in seconds) 165 | * size - size of the video (in bytes) 166 | * bitrate - bitrate of the video (in bits per second) 167 | * 168 | * @param promise win 169 | * @param promise fail 170 | * @param object args 171 | * @return void 172 | */ 173 | getVideoInfo: function (win, fail, args) { 174 | //get args from cordova app 175 | var options = args[0]; 176 | 177 | var file, basicProps; 178 | 179 | //look up the video file 180 | Windows.Storage.StorageFile.getFileFromApplicationUriAsync(new Windows.Foundation.Uri(options.fileUri)).then(function (storageFile) { 181 | //assign storage file to global variable 182 | file = storageFile; 183 | //get basic properties for size 184 | return file.getBasicPropertiesAsync(); 185 | }).then(function (basicProperties) { 186 | basicProps = basicProperties; 187 | //get video properties for the rest of info 188 | return file.properties.getVideoPropertiesAsync(); 189 | }).done(function (videoProps) { 190 | //resolve the video info 191 | win({ 192 | width: videoProps.width, 193 | height: videoProps.height, 194 | orientation: (videoProps.height > videoProps.width) ? 'portrait' :'landscape', 195 | duration: videoProps.duration, 196 | size: basicProps.size, 197 | bitrate: videoProps.bitrate 198 | }); 199 | }, function (details) { 200 | //failed 201 | fail(details); 202 | }); 203 | } 204 | } 205 | 206 | require("cordova/exec/proxy").add("VideoEditor", module.exports); -------------------------------------------------------------------------------- /typings/VideoEditor.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Enumerations for transcoding 3 | */ 4 | declare module VideoEditorOptions { 5 | //output quailty 6 | enum Quality { 7 | HIGH_QUALITY, 8 | MEDIUM_QUALITY, 9 | LOW_QUALITY 10 | } 11 | //speed over quailty, maybe should be a bool 12 | enum OptimizeForNetworkUse { 13 | NO, 14 | YES 15 | } 16 | //type of encoding to do 17 | enum OutputFileType { 18 | M4V, 19 | MPEG4, 20 | M4A, 21 | QUICK_TIME 22 | } 23 | } 24 | 25 | /** 26 | * Transcode options that are required to reencode or change the coding of the video. 27 | */ 28 | declare interface VideoEditorTranscodeProperties { 29 | /** A well-known location where the editable video lives. */ 30 | fileUri: string, 31 | /** A string that indicates what type of field this is, home for example. */ 32 | outputFileName: string, 33 | /** Instructions on how to encode the video. */ 34 | outputFileType: VideoEditorOptions.OutputFileType, 35 | /** Should the video be processed with quailty or speed in mind. iOS only. */ 36 | optimizeForNetworkUse: VideoEditorOptions.OptimizeForNetworkUse, 37 | /** Not supported in windows, save into the device library*/ 38 | saveToLibrary?: boolean, 39 | /** iOS only. Defaults to true */ 40 | maintainAspectRatio?: boolean, 41 | /** Width of the result */ 42 | width?: number, 43 | /** Height of the result */ 44 | height?: number, 45 | /** Bitrate in bits. Defaults to 9 megabit (9000000). */ 46 | videoBitrate?: number, 47 | /** Frames per second of the result. Android only. Defaults to 30. */ 48 | fps?: number, 49 | /** Number of audio channels. iOS, Android. Defaults: iOS - 2, Android - as is */ 50 | audioChannels?: number, 51 | /** Sample rate for the audio, defaults to 44100. iOS only. */ 52 | audioSampleRate?: number, 53 | /** Audio bitrate for the video in bits, defaults: iOS - 128000 (128 kilobits), Android - as is or 128000 */ 54 | audioBitrate?: number, 55 | /** Skip any transcoding actions (conversion/resizing/etc..) if the input video is avc video, defaults to false. Android only. */ 56 | skipVideoTranscodingIfAVC?: boolean, 57 | /** Not supported in windows, progress on the transcode*/ 58 | progress?: (info: any) => void 59 | } 60 | 61 | /** 62 | * Trim options that are required to locate, reduce start/ end and save the video. 63 | */ 64 | declare interface VideoEditorTrimProperties { 65 | /** A well-known location where the editable video lives. */ 66 | fileUri: string, 67 | /** A number of seconds to trim the front of the video. */ 68 | trimStart: number, 69 | /** A number of seconds to trim the front of the video. */ 70 | trimEnd: number, 71 | /** A string that indicates what type of field this is, home for example. */ 72 | outputFileName: string, 73 | /** Progress on transcode. */ 74 | progress?: (info: any) => void 75 | } 76 | 77 | /** 78 | * Trim options that are required to locate, reduce start/ end and save the video. 79 | */ 80 | declare interface VideoEditorThumbnailProperties { 81 | /** A well-known location where the editable video lives. */ 82 | fileUri: string, 83 | /** A string that indicates what type of field this is, home for example. */ 84 | outputFileName: string, 85 | /** Location in video to create the thumbnail (in seconds). */ 86 | atTime?: number, 87 | /** Width of the thumbnail. */ 88 | width?: number, 89 | /** Height of the thumbnail. */ 90 | height?: number, 91 | /** Quality of the thumbnail (between 1 and 100). */ 92 | quality?: number 93 | } 94 | 95 | declare interface VideoEditorVideoInfoOptions { 96 | /** Path to the video on the device. */ 97 | fileUri: string 98 | } 99 | 100 | declare interface VideoEditorVideoInfoDetails { 101 | /** Width of the video. */ 102 | width: number, 103 | /** Height of the video. */ 104 | height: number, 105 | /** Orientation of the video. Will be either portrait or landscape. */ 106 | orientation: 'portrait' | 'landscape', 107 | /** Duration of the video in seconds. */ 108 | duration: number, 109 | /** Size of the video in bytes. */ 110 | size: number, 111 | /** Bitrate of the video in bits per second. */ 112 | bitrate: number, 113 | /** Media type of the video, android example: 'video/3gpp', ios example: 'avc1'. */ 114 | videoMediaType: string, 115 | /** Media type of the audio track in video, android example: 'audio/mp4a-latm', ios example: 'aac'. */ 116 | audioMediaType: string 117 | } 118 | 119 | /** 120 | * The VideoEditor object represents a tool for editing videos. Videos can only be trimmed, so far. 121 | */ 122 | interface VideoEditor { 123 | /** 124 | * The VideoEditor.transcode method executes asynchronously, encoding a video at a location 125 | * and returning the full path. Options can be set to change how the video is encoded. The resulting string 126 | * is passed to the onSuccess callback function specified by the onSuccess parameter. 127 | * @param onSuccess Success callback function invoked with the full path of the video returned from successly saving the video 128 | * @param onError Error callback function, invoked when an error occurs. 129 | * @param transcodeOptions Transcode options that are required to reencode or change the coding of the video. 130 | */ 131 | transcodeVideo(onSuccess: (path: string) => void, 132 | onError: (error: any) => void, 133 | options: VideoEditorTranscodeProperties): void; 134 | 135 | /** 136 | * The VideoEditor.trim method executes asynchronously, taking a video location and trimming the beginning and end of the video 137 | * and returning the full path of the trimmed video. The resulting string is passed to the onSuccess 138 | * callback function specified by the onSuccess parameter. 139 | * @param onSuccess Success callback function invoked with the full path of the video returned from successly saving the video 140 | * @param onError Error callback function, invoked when an error occurs. 141 | * @param trimOptions Trim options that are required to locate, reduce start/end and save the video. 142 | */ 143 | trim(onSuccess: (path: string) => void, 144 | onError: (error: any) => void, 145 | trimOptions: VideoEditorTrimProperties): void; 146 | 147 | /** 148 | * The VideoEditor.trim method executes asynchronously, taking a video location and trimming the beginning and end of the video 149 | * and returning the full path of the trimmed video. The resulting string is passed to the onSuccess 150 | * callback function specified by the onSuccess parameter. 151 | * @param onSuccess Success callback function invoked with the full path of the video returned from successly saving the video 152 | * @param onError Error callback function, invoked when an error occurs. 153 | * @param trimOptions Trim options that are required to locate, reduce start/end and save the video. 154 | */ 155 | createThumbnail(onSuccess: (path: string) => void, 156 | onError: (error: any) => void, 157 | options: VideoEditorThumbnailProperties): void; 158 | 159 | /** 160 | * The VideoEditor.getVideoInfo method executes asynchronously, taking a video location and returning the details of the video. 161 | * The resulting info object is passed to the onSuccess callback function specified by the onSuccess parameter. 162 | * @param onSuccess Success callback function invoked with the details of the video. 163 | * @param onError Error callback function, invoked when an error occurs. 164 | * @param infoOptions Info options that are required to locate the video. 165 | */ 166 | getVideoInfo(onSuccess: (info: VideoEditorVideoInfoDetails) => void, 167 | onError: (error: any) => void, 168 | options: VideoEditorVideoInfoOptions): void; 169 | } 170 | 171 | declare var VideoEditor: VideoEditor; -------------------------------------------------------------------------------- /www/VideoEditor.js: -------------------------------------------------------------------------------- 1 | // 2 | // VideoEditor.js 3 | // 4 | // Created by Josh Bavari on 01-14-2014 5 | // Modified by Ross Martin on 01-29-15 6 | // 7 | 8 | var exec = require('cordova/exec'); 9 | var pluginName = 'VideoEditor'; 10 | 11 | function VideoEditor() {} 12 | 13 | VideoEditor.prototype.transcodeVideo = function(success, error, options) { 14 | var self = this; 15 | var win = function(result) { 16 | if (typeof result.progress !== 'undefined') { 17 | if (typeof options.progress === 'function') { 18 | options.progress(result.progress); 19 | } 20 | } else { 21 | success(result); 22 | } 23 | }; 24 | exec(win, error, pluginName, 'transcodeVideo', [options]); 25 | }; 26 | 27 | VideoEditor.prototype.trim = function(success, error, options) { 28 | var self = this; 29 | var win = function(result) { 30 | if (typeof result.progress !== 'undefined') { 31 | if (typeof options.progress === 'function') { 32 | options.progress(result.progress); 33 | } 34 | } else { 35 | success(result); 36 | } 37 | }; 38 | exec(win, error, pluginName, 'trim', [options]); 39 | }; 40 | 41 | VideoEditor.prototype.createThumbnail = function(success, error, options) { 42 | exec(success, error, pluginName, 'createThumbnail', [options]); 43 | }; 44 | 45 | VideoEditor.prototype.getVideoInfo = function(success, error, options) { 46 | exec(success, error, pluginName, 'getVideoInfo', [options]); 47 | }; 48 | 49 | VideoEditor.prototype.execFFMPEG = function(success, error, options) { 50 | var msg = 'execFFMPEG has been removed as of v1.1.0'; 51 | console.log(msg); 52 | error(msg); 53 | }; 54 | 55 | VideoEditor.prototype.execFFPROBE = function(success, error, options) { 56 | var msg = 'ffprobe has been removed as of v1.0.9'; 57 | console.log(msg); 58 | error(msg); 59 | }; 60 | 61 | module.exports = new VideoEditor(); 62 | -------------------------------------------------------------------------------- /www/VideoEditorOptions.js: -------------------------------------------------------------------------------- 1 | // 2 | // VideoEditorOptions.js 3 | // 4 | // Created by Josh Bavari on 01-14-2014 5 | // Modified by Ross Martin on 01-29-15 6 | // 7 | 8 | var VideoEditorOptions = { 9 | OptimizeForNetworkUse: { 10 | NO: 0, 11 | YES: 1 12 | }, 13 | OutputFileType: { 14 | M4V: 0, 15 | MPEG4: 1, 16 | M4A: 2, 17 | QUICK_TIME: 3 18 | } 19 | }; 20 | 21 | module.exports = VideoEditorOptions; 22 | --------------------------------------------------------------------------------