├── .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 | [](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 |
--------------------------------------------------------------------------------