├── .gitignore ├── README.md ├── demo ├── css │ └── index.css ├── img │ ├── cameraoverlays │ │ ├── overlay-iPad-landscape.png │ │ ├── overlay-iPad-portrait.png │ │ ├── overlay-iPhone-landscape.png │ │ └── overlay-iPhone-portrait.png │ └── logo.png ├── index.html └── js │ ├── index.js │ └── videocaptureplus-demo.js ├── plugin.xml ├── screenshots ├── screenshot-after-recording.png ├── screenshot-before-recording-portrait.png ├── screenshot-during-recording-landscape.png └── screenshot-reviewing-recording-landscape.png ├── src ├── android │ └── nl │ │ └── xservices │ │ └── plugins │ │ └── videocaptureplus │ │ ├── FileHelper.java │ │ ├── VideoCapturePlus.java │ │ └── xml │ │ └── provider_paths.xml └── ios │ ├── VideoCapturePlus.h │ └── VideoCapturePlus.m └── www └── VideoCapturePlus.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VideoCapturePlus PhoneGap plugin 2 | 3 | > PR's are welcome, but I'm no longer actively maintaining this plugin. 4 | 5 | ## 0. Index 6 | 7 | 1. [Description](https://github.com/EddyVerbruggen/VideoCapturePlus-PhoneGap-Plugin#1-description) 8 | 2. [Screenshots](https://github.com/EddyVerbruggen/VideoCapturePlus-PhoneGap-Plugin#2-screenshots) 9 | 3. [Installation](https://github.com/EddyVerbruggen/VideoCapturePlus-PhoneGap-Plugin#3-installation) 10 | 3. [Automatically (CLI / Plugman)](https://github.com/EddyVerbruggen/VideoCapturePlus-PhoneGap-Plugin#automatically-cli--plugman) 11 | 3. [Manually](https://github.com/EddyVerbruggen/VideoCapturePlus-PhoneGap-Plugin#manually) 12 | 3. [PhoneGap Build](https://github.com/EddyVerbruggen/VideoCapturePlus-PhoneGap-Plugin#phonegap-build) 13 | 4. [Usage](https://github.com/EddyVerbruggen/VideoCapturePlus-PhoneGap-Plugin#4-usage) 14 | 5. [Credits](https://github.com/EddyVerbruggen/VideoCapturePlus-PhoneGap-Plugin#5-credits) 15 | 6. [License](https://github.com/EddyVerbruggen/VideoCapturePlus-PhoneGap-Plugin#6-license) 16 | 17 | ## 1. Description 18 | 19 | * This plugin offers some useful extras on top of the [default PhoneGap Video Capture capabilities](http://docs.phonegap.com/en/3.3.0/cordova_media_capture_capture.md.html#capture.captureVideo): 20 | * HD recording. 21 | * Starting with the front camera. 22 | * A custom overlay (currently iOS only). 23 | * For PhoneGap 3.0.0 and up. 24 | * Works on the same Android and iOS versions as the [original plugin](http://docs.phonegap.com/en/3.3.0/cordova_media_capture_capture.md.html#capture.captureVideo). 25 | * Compatible with [Cordova Plugman](https://github.com/apache/cordova-plugman). 26 | * Pending official support at [PhoneGap Build](https://build.phonegap.com/plugins). 27 | 28 | ## 2. Screenshots 29 | 30 | Before recording, portrait mode (the 'Please rotate' text is part of the [overlay png file](demo/img/cameraoverlays/overlay-iPhone-portrait.png)): 31 | 32 | ![ScreenShot](screenshots/screenshot-before-recording-portrait.png) 33 | 34 | During recording, landscape mode: 35 | 36 | ![ScreenShot](screenshots/screenshot-during-recording-landscape.png) 37 | 38 | Reviewing the recording, portrait mode: 39 | 40 | ![ScreenShot](screenshots/screenshot-reviewing-recording-landscape.png) 41 | 42 | After recording you can extract the metadata, [see the demo folder for the code of this example](demo): 43 | 44 | ![ScreenShot](screenshots/screenshot-after-recording.png) 45 | 46 | ## 3. Installation 47 | 48 | 49 | IMPORTANT NOTE for plugin version < 1.2: if you currently use the org.apache.cordova.media-capture plugin, remove it (otherwise your build will fail). 50 | 51 | 52 | ### Automatically (CLI / Plugman) 53 | VideoCapturePlus is compatible with [Cordova Plugman](https://github.com/apache/cordova-plugman), compatible with [PhoneGap 3.0 CLI](http://docs.phonegap.com/en/3.0.0/guide_cli_index.md.html#The%20Command-line%20Interface_add_features), here's how it works with the CLI: 54 | 55 | ``` 56 | $ phonegap local plugin add https://github.com/EddyVerbruggen/VideoCapturePlus-PhoneGap-Plugin.git 57 | ``` 58 | or 59 | ``` 60 | $ cordova plugin add https://github.com/EddyVerbruggen/VideoCapturePlus-PhoneGap-Plugin.git 61 | ``` 62 | run this command afterwards: 63 | ``` 64 | $ cordova prepare 65 | ``` 66 | 67 | VideoCapturePlus.js is brought in automatically. There is no need to change or add anything in your html. 68 | 69 | ### Manually 70 | 71 | 1\. Add the following xml to your `config.xml` in the root directory of your `www` folder: 72 | ```xml 73 | 74 | 75 | 76 | 77 | ``` 78 | ```xml 79 | 80 | 81 | 82 | 83 | ``` 84 | 85 | For Android, add these to your `AndroidManifest.xml`: 86 | ```xml 87 | 88 | 89 | 90 | ``` 91 | 92 | For iOS, you'll need to add the `CoreGraphics.framework` and `MobileCoreServices.framework` to your project. 93 | 94 | 2\. Grab a copy of VideoCapturePlus.js, add it to your project and reference it in `index.html`: 95 | ```html 96 | 97 | ``` 98 | 99 | 3\. Download the source files for iOS and/or Android and copy them to your project. 100 | 101 | iOS: Copy `VideoCapturePlus.h` and `VideoCapturePlus.m` to `platforms/ios//Plugins` 102 | 103 | Android: Copy `VideoCapturePlus.java` and `FileHelper.java` to `platforms/android/src/nl/xservices/plugins/videocaptureplus` (create the folders). 104 | 105 | ### PhoneGap Build 106 | 107 | VideoCapturePlus is pending approval at [PhoneGap Build](http://build.phonegap.com/plugins). Once it's approved, just add the following xml to your `config.xml` to always use the latest version of this plugin: 108 | ```xml 109 | 110 | ``` 111 | or to use this exact version: 112 | ```xml 113 | 114 | ``` 115 | 116 | VideoCapturePlus.js is brought in automatically. There is no need to change or add anything in your html. 117 | 118 | ## 4. Usage 119 | See the [demo project](demo) for all details, but the most interesting part is this: 120 | ```javascript 121 | window.plugins.videocaptureplus.captureVideo( 122 | captureSuccess, // your success callback 123 | captureError, // your error callback 124 | { 125 | limit: 1, // the nr of videos to record, default 1 (on iOS always 1) 126 | duration: duration, // max duration in seconds, default 0, which is 'forever' 127 | highquality: highquality, // set to true to override the default low quality setting 128 | frontcamera: frontcamera, // set to true to override the default backfacing camera setting. iOS: works fine, Android: YMMV (#18) 129 | // you'll want to sniff the useragent/device and pass the best overlay based on that.. assuming iphone here 130 | portraitOverlay: 'www/img/cameraoverlays/overlay-iPhone-portrait.png', // put the png in your www folder 131 | landscapeOverlay: 'www/img/cameraoverlays/overlay-iPhone-landscape.png', // not passing an overlay means no image is shown for the landscape orientation 132 | overlayText: 'Please rotate to landscape for the best result' // iOS only 133 | } 134 | ); 135 | ``` 136 | 137 | ## 5. CREDITS ## 138 | 139 | Cordova, for [the original plugin repository](https://github.com/apache/cordova-plugin-media-capture), which is the basis for this one. 140 | 141 | (James Gillmore)[https://github.com/faceyspacey] for the overlayText feature on iOS. 142 | 143 | ## 6. License 144 | 145 | [The MIT License (MIT)](http://www.opensource.org/licenses/mit-license.html) 146 | 147 | Permission is hereby granted, free of charge, to any person obtaining a copy 148 | of this software and associated documentation files (the "Software"), to deal 149 | in the Software without restriction, including without limitation the rights 150 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 151 | copies of the Software, and to permit persons to whom the Software is 152 | furnished to do so, subject to the following conditions: 153 | 154 | The above copyright notice and this permission notice shall be included in 155 | all copies or substantial portions of the Software. 156 | 157 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 158 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 159 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 160 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 161 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 162 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 163 | THE SOFTWARE. 164 | -------------------------------------------------------------------------------- /demo/css/index.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | * { 20 | -webkit-tap-highlight-color: rgba(0,0,0,0); /* make transparent link selection, adjust last value opacity 0 to 1.0 */ 21 | } 22 | 23 | body { 24 | -webkit-touch-callout: none; /* prevent callout to copy image, etc when tap to hold */ 25 | -webkit-text-size-adjust: none; /* prevent webkit from resizing text to fit */ 26 | -webkit-user-select: none; /* prevent copy paste, to allow, change 'none' to 'text' */ 27 | background-color:#E4E4E4; 28 | background-image:linear-gradient(top, #A7A7A7 0%, #E4E4E4 51%); 29 | background-image:-webkit-linear-gradient(top, #A7A7A7 0%, #E4E4E4 51%); 30 | background-image:-ms-linear-gradient(top, #A7A7A7 0%, #E4E4E4 51%); 31 | background-image:-webkit-gradient( 32 | linear, 33 | left top, 34 | left bottom, 35 | color-stop(0, #A7A7A7), 36 | color-stop(0.51, #E4E4E4) 37 | ); 38 | background-attachment:fixed; 39 | font-family:'HelveticaNeue-Light', 'HelveticaNeue', Helvetica, Arial, sans-serif; 40 | font-size:12px; 41 | height:100%; 42 | margin:0px; 43 | padding:0px; 44 | text-transform:uppercase; 45 | width:100%; 46 | } 47 | 48 | /* Portrait layout (default) */ 49 | .app { 50 | background:url(../img/logo.png) no-repeat center top; /* 170px x 200px */ 51 | position:absolute; /* position in the center of the screen */ 52 | left:50%; 53 | top:50%; 54 | height:50px; /* text area height */ 55 | width:225px; /* text area width */ 56 | text-align:center; 57 | padding:180px 0px 0px 0px; /* image height is 200px (bottom 20px are overlapped with text) */ 58 | margin:-115px 0px 0px -112px; /* offset vertical: half of image height and text area height */ 59 | /* offset horizontal: half of text area width */ 60 | } 61 | 62 | /* Landscape layout (with min-width) */ 63 | @media screen and (min-aspect-ratio: 1/1) and (min-width:400px) { 64 | .app { 65 | background-position:left center; 66 | padding:75px 0px 75px 170px; /* padding-top + padding-bottom + text area = image height */ 67 | margin:-90px 0px 0px -198px; /* offset vertical: half of image height */ 68 | /* offset horizontal: half of image width and text area width */ 69 | } 70 | } 71 | 72 | h1 { 73 | font-size:24px; 74 | font-weight:normal; 75 | margin:0px; 76 | overflow:visible; 77 | padding:0px; 78 | text-align:center; 79 | } 80 | 81 | .event { 82 | border-radius:4px; 83 | -webkit-border-radius:4px; 84 | color:#FFFFFF; 85 | font-size:12px; 86 | margin:0px 30px; 87 | padding:2px 0px; 88 | } 89 | 90 | .event.listening { 91 | background-color:#333333; 92 | display:block; 93 | } 94 | 95 | .event.received { 96 | background-color:#4B946A; 97 | display:none; 98 | } 99 | 100 | @keyframes fade { 101 | from { opacity: 1.0; } 102 | 50% { opacity: 0.4; } 103 | to { opacity: 1.0; } 104 | } 105 | 106 | @-webkit-keyframes fade { 107 | from { opacity: 1.0; } 108 | 50% { opacity: 0.4; } 109 | to { opacity: 1.0; } 110 | } 111 | 112 | .blink { 113 | animation:fade 3000ms infinite; 114 | -webkit-animation:fade 3000ms infinite; 115 | } 116 | -------------------------------------------------------------------------------- /demo/img/cameraoverlays/overlay-iPad-landscape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EddyVerbruggen/VideoCapturePlus-PhoneGap-Plugin/ded20c28d375db9aa9574bd0d50e7fa9648c0121/demo/img/cameraoverlays/overlay-iPad-landscape.png -------------------------------------------------------------------------------- /demo/img/cameraoverlays/overlay-iPad-portrait.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EddyVerbruggen/VideoCapturePlus-PhoneGap-Plugin/ded20c28d375db9aa9574bd0d50e7fa9648c0121/demo/img/cameraoverlays/overlay-iPad-portrait.png -------------------------------------------------------------------------------- /demo/img/cameraoverlays/overlay-iPhone-landscape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EddyVerbruggen/VideoCapturePlus-PhoneGap-Plugin/ded20c28d375db9aa9574bd0d50e7fa9648c0121/demo/img/cameraoverlays/overlay-iPhone-landscape.png -------------------------------------------------------------------------------- /demo/img/cameraoverlays/overlay-iPhone-portrait.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EddyVerbruggen/VideoCapturePlus-PhoneGap-Plugin/ded20c28d375db9aa9574bd0d50e7fa9648c0121/demo/img/cameraoverlays/overlay-iPhone-portrait.png -------------------------------------------------------------------------------- /demo/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EddyVerbruggen/VideoCapturePlus-PhoneGap-Plugin/ded20c28d375db9aa9574bd0d50e7fa9648c0121/demo/img/logo.png -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Hello VideoCapturePlus 9 | 10 | 11 |
12 |

VideoCapturePlus

13 | 17 |
18 |

19 | 20 |

21 | 22 |

23 | 24 |

25 | 26 |

27 |
28 |
29 |
30 |
31 |
32 | 33 | 34 | 35 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /demo/js/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | var app = { 20 | // Application Constructor 21 | initialize: function() { 22 | this.bindEvents(); 23 | }, 24 | // Bind Event Listeners 25 | // 26 | // Bind any events that are required on startup. Common events are: 27 | // 'load', 'deviceready', 'offline', and 'online'. 28 | bindEvents: function() { 29 | document.addEventListener('deviceready', this.onDeviceReady, false); 30 | }, 31 | // deviceready Event Handler 32 | // 33 | // The scope of 'this' is the event. In order to call the 'receivedEvent' 34 | // function, we must explicity call 'app.receivedEvent(...);' 35 | onDeviceReady: function() { 36 | app.receivedEvent('deviceready'); 37 | }, 38 | // Update DOM on a Received Event 39 | receivedEvent: function(id) { 40 | var parentElement = document.getElementById(id); 41 | var listeningElement = parentElement.querySelector('.listening'); 42 | var receivedElement = parentElement.querySelector('.received'); 43 | 44 | listeningElement.setAttribute('style', 'display:none;'); 45 | receivedElement.setAttribute('style', 'display:block;'); 46 | 47 | console.log('Received Event: ' + id); 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /demo/js/videocaptureplus-demo.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function videoCapturePlusDemo(highquality, frontcamera, duration) { 4 | window.plugins.videocaptureplus.captureVideo( 5 | captureSuccess, 6 | captureError, 7 | { 8 | limit: 1, 9 | duration: duration, 10 | highquality: highquality, 11 | frontcamera: frontcamera, 12 | // you'll want to sniff the useragent/device and pass the best overlay based on that.. assuming iphone here 13 | portraitOverlay: 'www/img/cameraoverlays/overlay-iPhone-portrait.png', 14 | landscapeOverlay: 'www/img/cameraoverlays/overlay-iPhone-landscape.png' 15 | } 16 | ); 17 | } 18 | 19 | function captureSuccess(mediaFiles) { 20 | var i, len; 21 | for (i = 0, len = mediaFiles.length; i < len; i++) { 22 | var mediaFile = mediaFiles[i]; 23 | mediaFile.getFormatData(getFormatDataSuccess, getFormatDataError); 24 | 25 | var vid = document.createElement('video'); 26 | vid.id = "theVideo"; 27 | vid.width = "240"; 28 | vid.height = "160"; 29 | vid.controls = "controls"; 30 | var source_vid = document.createElement('source'); 31 | source_vid.id = "theSource"; 32 | source_vid.src = mediaFile.fullPath; 33 | vid.appendChild(source_vid); 34 | document.getElementById('video_container').innerHTML = ''; 35 | document.getElementById('video_container').appendChild(vid); 36 | document.getElementById('video_meta_container2').innerHTML = parseInt(mediaFile.size / 1000) + 'KB ' + mediaFile.type; 37 | } 38 | } 39 | 40 | function getFormatDataSuccess(mediaFileData) { 41 | document.getElementById('video_meta_container').innerHTML = mediaFileData.duration + ' seconds, ' + mediaFileData.width + ' x ' + mediaFileData.height; 42 | } 43 | 44 | function captureError(error) { 45 | // code 3 = cancel by user 46 | alert('Returncode: ' + JSON.stringify(error.code)); 47 | } 48 | 49 | function getFormatDataError(error) { 50 | alert('A Format Data Error occurred during getFormatData: ' + error.code); 51 | } -------------------------------------------------------------------------------- /plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | VideoCapturePlus 8 | 9 | 10 | If you want HD video recording, starting with the front camera by default, or 11 | use an (transparent PNG) overlay during recording (iOS only feature)? Look no further! 12 | All options of the default plugin are available as well, so you can still set a 13 | duration limit etc. 14 | 15 | 16 | MIT 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 | 63 | 66 | 67 | 68 | 69 | 70 | 72 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /screenshots/screenshot-after-recording.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EddyVerbruggen/VideoCapturePlus-PhoneGap-Plugin/ded20c28d375db9aa9574bd0d50e7fa9648c0121/screenshots/screenshot-after-recording.png -------------------------------------------------------------------------------- /screenshots/screenshot-before-recording-portrait.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EddyVerbruggen/VideoCapturePlus-PhoneGap-Plugin/ded20c28d375db9aa9574bd0d50e7fa9648c0121/screenshots/screenshot-before-recording-portrait.png -------------------------------------------------------------------------------- /screenshots/screenshot-during-recording-landscape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EddyVerbruggen/VideoCapturePlus-PhoneGap-Plugin/ded20c28d375db9aa9574bd0d50e7fa9648c0121/screenshots/screenshot-during-recording-landscape.png -------------------------------------------------------------------------------- /screenshots/screenshot-reviewing-recording-landscape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EddyVerbruggen/VideoCapturePlus-PhoneGap-Plugin/ded20c28d375db9aa9574bd0d50e7fa9648c0121/screenshots/screenshot-reviewing-recording-landscape.png -------------------------------------------------------------------------------- /src/android/nl/xservices/plugins/videocaptureplus/FileHelper.java: -------------------------------------------------------------------------------- 1 | package nl.xservices.plugins.videocaptureplus; 2 | 3 | import android.net.Uri; 4 | import android.webkit.MimeTypeMap; 5 | 6 | import org.apache.cordova.CordovaInterface; 7 | 8 | import java.util.Locale; 9 | 10 | // TODO: Replace with CordovaResourceApi.getMimeType() post 3.1. 11 | public class FileHelper { 12 | public static String getMimeTypeForExtension(String path) { 13 | String extension = path; 14 | int lastDot = extension.lastIndexOf('.'); 15 | if (lastDot != -1) { 16 | extension = extension.substring(lastDot + 1); 17 | } 18 | // Convert the URI string to lower case to ensure compatibility with MimeTypeMap (see CB-2185). 19 | extension = extension.toLowerCase(Locale.getDefault()); 20 | if (extension.equals("3ga")) { 21 | return "audio/3gpp"; 22 | } 23 | return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); 24 | } 25 | 26 | /** 27 | * Returns the mime type of the data specified by the given URI string. 28 | * 29 | * @param uri the URI string of the data 30 | * @return the mime type of the specified data 31 | */ 32 | public static String getMimeType(Uri uri, CordovaInterface cordova) { 33 | String mimeType = null; 34 | if ("content".equals(uri.getScheme())) { 35 | mimeType = cordova.getActivity().getContentResolver().getType(uri); 36 | } else { 37 | mimeType = getMimeTypeForExtension(uri.getPath()); 38 | } 39 | 40 | return mimeType; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/android/nl/xservices/plugins/videocaptureplus/VideoCapturePlus.java: -------------------------------------------------------------------------------- 1 | package nl.xservices.plugins.videocaptureplus; 2 | 3 | import android.app.Activity; 4 | import android.content.ContentResolver; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.content.pm.PackageManager; 8 | import android.content.pm.PackageManager.NameNotFoundException; 9 | import android.database.Cursor; 10 | import android.media.MediaPlayer; 11 | import android.net.Uri; 12 | import android.os.Build; 13 | import android.os.Environment; 14 | import android.provider.MediaStore; 15 | import android.util.Log; 16 | import android.Manifest; 17 | 18 | import org.apache.cordova.CallbackContext; 19 | import org.apache.cordova.CordovaPlugin; 20 | import org.apache.cordova.PluginResult; 21 | import org.apache.cordova.PermissionHelper; 22 | 23 | import org.json.JSONArray; 24 | import org.json.JSONException; 25 | import org.json.JSONObject; 26 | 27 | import java.io.File; 28 | import java.io.IOException; 29 | import java.util.concurrent.Callable; 30 | import java.util.concurrent.ExecutionException; 31 | import java.util.concurrent.Future; 32 | import java.util.Arrays; 33 | 34 | public class VideoCapturePlus extends CordovaPlugin { 35 | 36 | private static final String VIDEO_3GPP = "video/3gpp"; 37 | private static final String VIDEO_MP4 = "video/mp4"; 38 | 39 | private static final int PERMISSION_DENIED_ERROR = 20; 40 | private static final int CAPTURE_VIDEO = 2; // Constant for capture video 41 | private static final String LOG_TAG = "VideoCapturePlus"; 42 | private static final int CAPTURE_NO_MEDIA_FILES = 3; 43 | 44 | protected final static String[] permissions = { Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.RECORD_AUDIO }; 45 | 46 | private CallbackContext callbackContext; // The callback context from which we were invoked. 47 | private long limit; // the number of pics/vids/clips to take 48 | private int duration; // optional max duration of video recording in seconds 49 | private boolean highquality; // optional setting for controlling the video quality 50 | private boolean frontcamera; // optional setting for starting video capture with the frontcamera 51 | private JSONArray results; // The array of results to be returned to the user 52 | 53 | @Override 54 | public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { 55 | this.callbackContext = callbackContext; 56 | this.limit = 1; 57 | this.duration = 0; 58 | this.highquality = false; 59 | this.frontcamera = false; 60 | this.results = new JSONArray(); 61 | 62 | JSONObject options = args.optJSONObject(0); 63 | if (options != null) { 64 | limit = options.optLong("limit", 1); 65 | duration = options.optInt("duration", 0); 66 | highquality = options.optBoolean("highquality", false); 67 | frontcamera = options.optBoolean("frontcamera", false); 68 | } 69 | 70 | if (action.equals("getFormatData")) { 71 | JSONObject obj = getFormatData(args.getString(0), args.getString(1)); 72 | callbackContext.success(obj); 73 | return true; 74 | } else if (action.equals("captureVideo")) { 75 | this.callCaptureVideo(duration, highquality, frontcamera); 76 | } else { 77 | return false; 78 | } 79 | return true; 80 | } 81 | 82 | /** 83 | * Provides the media data file data depending on it's mime type 84 | * 85 | * @param filePath path to the file 86 | * @param mimeType of the file 87 | * @return a MediaFileData object 88 | */ 89 | private JSONObject getFormatData(String filePath, String mimeType) throws JSONException { 90 | Uri fileUrl = filePath.startsWith("file:") ? Uri.parse(filePath) : Uri.fromFile(new File(filePath)); 91 | JSONObject obj = new JSONObject(); 92 | // setup defaults 93 | obj.put("height", 0); 94 | obj.put("width", 0); 95 | obj.put("bitrate", 0); 96 | obj.put("duration", 0); 97 | obj.put("codecs", ""); 98 | 99 | // If the mimeType isn't set the rest will fail, so let's see if we can determine it. 100 | if (mimeType == null || "".equals(mimeType) || "null".equals(mimeType)) { 101 | mimeType = FileHelper.getMimeType(fileUrl, cordova); 102 | } 103 | Log.d(LOG_TAG, "Mime type = " + mimeType); 104 | 105 | if (mimeType.equals(VIDEO_3GPP) || mimeType.equals(VIDEO_MP4)) { 106 | obj = getAudioVideoData(filePath, obj); 107 | } 108 | return obj; 109 | } 110 | 111 | private JSONObject getAudioVideoData(String filePath, JSONObject obj) throws JSONException { 112 | MediaPlayer player = new MediaPlayer(); 113 | try { 114 | player.setDataSource(filePath); 115 | player.prepare(); 116 | obj.put("duration", player.getDuration() / 1000); 117 | obj.put("height", player.getVideoHeight()); 118 | obj.put("width", player.getVideoWidth()); 119 | } catch (IOException e) { 120 | Log.d(LOG_TAG, "Error: loading video file"); 121 | } 122 | return obj; 123 | } 124 | 125 | private String getTempDirectoryPath() { 126 | File cache; 127 | if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { 128 | // SD card 129 | cache = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Android/data/" + cordova.getActivity().getPackageName() + "/cache/"); 130 | } else { 131 | // internal storage 132 | cache = cordova.getActivity().getCacheDir(); 133 | } 134 | // Create the cache directory if it doesn't exist 135 | cache.mkdirs(); 136 | return cache.getAbsolutePath(); 137 | } 138 | 139 | /** 140 | * Take a video with the camera. 141 | * Permissions checks 142 | */ 143 | private void callCaptureVideo(int duration, boolean highquality, boolean frontcamera) { 144 | 145 | String[] missingPermissions = determineMissingPermissions(); 146 | 147 | if(missingPermissions.length == 0) { 148 | captureVideo(duration,highquality,frontcamera); 149 | } else { 150 | PermissionHelper.requestPermissions(this, CAPTURE_VIDEO, missingPermissions); 151 | } 152 | } 153 | /** 154 | * Sets up an intent to capture video. Result handled by onActivityResult() 155 | */ 156 | private String[] determineMissingPermissions() { 157 | boolean writePermission = PermissionHelper.hasPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE); 158 | boolean cameraPermission = PermissionHelper.hasPermission(this, Manifest.permission.CAMERA); 159 | boolean recordAudioPermission = PermissionHelper.hasPermission(this, Manifest.permission.RECORD_AUDIO); 160 | 161 | String[] missingPermissions = new String[] {}; 162 | if (writePermission && !cameraPermission && !recordAudioPermission) { 163 | missingPermissions = new String[] {Manifest.permission.CAMERA,Manifest.permission.RECORD_AUDIO}; 164 | } else if (writePermission && cameraPermission && !recordAudioPermission) { 165 | missingPermissions = new String[] {Manifest.permission.RECORD_AUDIO}; 166 | } else if (writePermission && !cameraPermission && recordAudioPermission) { 167 | missingPermissions = new String[] {Manifest.permission.CAMERA}; 168 | } else if (!writePermission && cameraPermission && !recordAudioPermission) { 169 | missingPermissions = new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.RECORD_AUDIO}; 170 | } else if (!writePermission && !cameraPermission && recordAudioPermission) { 171 | missingPermissions = new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE,Manifest.permission.CAMERA}; 172 | } else if (!writePermission && cameraPermission && recordAudioPermission) { 173 | missingPermissions = new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}; 174 | } else if (!writePermission && !cameraPermission && !recordAudioPermission) { 175 | missingPermissions = permissions; 176 | } 177 | 178 | return missingPermissions; 179 | } 180 | 181 | 182 | private void captureVideo(int duration, boolean highquality, boolean frontcamera) { 183 | Intent intent = new Intent(android.provider.MediaStore.ACTION_VIDEO_CAPTURE); 184 | String videoUri = getVideoContentUriFromFilePath(this.cordova.getActivity(), getTempDirectoryPath()); 185 | intent.putExtra(MediaStore.EXTRA_OUTPUT, videoUri); 186 | 187 | if (highquality) { 188 | intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1); 189 | } else { 190 | // If high quality set to false, force low quality for devices that default to high quality 191 | intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0); 192 | } 193 | 194 | if (frontcamera) { 195 | intent.putExtra("android.intent.extras.CAMERA_FACING", android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT); 196 | } 197 | 198 | // consider adding an allowflash param, setting Camera.Parameters.FLASH_MODE_ON/OFF/AUTO 199 | 200 | if (Build.VERSION.SDK_INT > 7) { 201 | intent.putExtra("android.intent.extra.durationLimit", duration); 202 | } 203 | 204 | this.cordova.startActivityForResult(this, intent, CAPTURE_VIDEO); 205 | } 206 | 207 | public static String getVideoContentUriFromFilePath(Context ctx, String filePath) { 208 | 209 | ContentResolver contentResolver = ctx.getContentResolver(); 210 | String videoUriStr = null; 211 | 212 | // This returns us content://media/external/videos/media (or something like that) 213 | // I pass in "external" because that's the MediaStore's name for the external 214 | // storage on my device (the other possibility is "internal") 215 | Uri videosUri = MediaStore.Video.Media.getContentUri("external"); 216 | 217 | String[] projection = {MediaStore.Video.VideoColumns._ID}; 218 | 219 | Cursor cursor = contentResolver.query(videosUri, projection, MediaStore.Video.VideoColumns.DATA + " LIKE ?", new String[]{filePath}, null); 220 | long videoId = -1; 221 | if (cursor.getCount() > 0) { 222 | cursor.moveToFirst(); 223 | int columnIndex = cursor.getColumnIndex(projection[0]); 224 | videoId = cursor.getLong(columnIndex); 225 | } 226 | cursor.close(); 227 | if (videoId != -1) videoUriStr = videosUri.toString() + "/" + videoId; 228 | return videoUriStr; 229 | } 230 | 231 | /** 232 | * Called when the user grants permissions that the app needs 233 | * 234 | * @param requestCode The request code originally supplied to startActivityForResult(), 235 | * allowing you to identify who this result came from. 236 | * @param permissions List of requested permissions 237 | * @param grantResults List of grant results (permissions accepted or denied) 238 | */ 239 | public void onRequestPermissionResult(int requestCode, String[] permissions, 240 | int[] grantResults) throws JSONException { 241 | 242 | for (int r : grantResults) { 243 | if (r == PackageManager.PERMISSION_DENIED) { 244 | this.callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, PERMISSION_DENIED_ERROR)); 245 | return; 246 | } 247 | } 248 | switch (requestCode) { 249 | case CAPTURE_VIDEO: 250 | captureVideo(this.duration, this.highquality, this.frontcamera); 251 | break; 252 | } 253 | } 254 | 255 | /** 256 | * Called when the video view exits. 257 | * 258 | * @param requestCode The request code originally supplied to startActivityForResult(), 259 | * allowing you to identify who this result came from. 260 | * @param resultCode The integer result code returned by the child activity through its setResult(). 261 | * @param intent An Intent, which can return result data to the caller (various data can be attached to Intent "extras"). 262 | */ 263 | public void onActivityResult(int requestCode, int resultCode, Intent intent) { 264 | if (resultCode == Activity.RESULT_OK) { 265 | if (requestCode == CAPTURE_VIDEO) { 266 | Uri data = null; 267 | if (intent != null) { 268 | // Get the uri of the video clip 269 | data = intent.getData(); 270 | } 271 | 272 | if (data == null) { 273 | File movie = new File(getTempDirectoryPath(), "VideoCapturePlus.avi"); 274 | data = Uri.fromFile(movie); 275 | } 276 | 277 | // create a file object from the uri 278 | if (data == null) { 279 | this.fail(createErrorObject(CAPTURE_NO_MEDIA_FILES, "Error: data is null")); 280 | } else { 281 | results.put(createMediaFile(data)); 282 | if (results.length() >= limit) { 283 | // Send Uri back to JavaScript for viewing video 284 | this.callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, results)); 285 | } else { 286 | // still need to capture more video clips 287 | callCaptureVideo(duration, highquality, frontcamera); 288 | } 289 | } 290 | } 291 | } else if (resultCode == Activity.RESULT_CANCELED) { 292 | // If we have partial results send them back to the user 293 | if (results.length() > 0) { 294 | this.callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, results)); 295 | } else { 296 | this.fail(createErrorObject(CAPTURE_NO_MEDIA_FILES, "Canceled.")); 297 | } 298 | } else { 299 | // If we have partial results send them back to the user 300 | if (results.length() > 0) { 301 | this.callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, results)); 302 | } else { 303 | this.fail(createErrorObject(CAPTURE_NO_MEDIA_FILES, "Did not complete!")); 304 | } 305 | } 306 | } 307 | 308 | private JSONObject createMediaFile(final Uri data) { 309 | Future result = cordova.getThreadPool().submit(new Callable() { 310 | @Override 311 | public JSONObject call() throws Exception { 312 | File fp = webView.getResourceApi().mapUriToFile(data); 313 | JSONObject obj = new JSONObject(); 314 | try { 315 | // File properties 316 | obj.put("name", fp.getName()); 317 | obj.put("fullPath", fp.toURI().toString()); 318 | // Because of an issue with MimeTypeMap.getMimeTypeFromExtension() all .3gpp files 319 | // are reported as video/3gpp. I'm doing this hacky check of the URI to see if it 320 | // is stored in the audio or video content store. 321 | if (fp.getAbsoluteFile().toString().endsWith(".3gp") || fp.getAbsoluteFile().toString().endsWith(".3gpp")) { 322 | obj.put("type", VIDEO_3GPP); 323 | } else { 324 | obj.put("type", FileHelper.getMimeType(Uri.fromFile(fp), cordova)); 325 | } 326 | obj.put("lastModifiedDate", fp.lastModified()); 327 | obj.put("size", fp.length()); 328 | } catch (JSONException e) { 329 | // this will never happen 330 | e.printStackTrace(); 331 | } 332 | return obj; 333 | } 334 | }); 335 | try { 336 | return result.get(); 337 | } catch (InterruptedException e) { 338 | e.printStackTrace(); 339 | } catch (ExecutionException e) { 340 | e.printStackTrace(); 341 | } 342 | return null; 343 | } 344 | 345 | private JSONObject createErrorObject(int code, String message) { 346 | JSONObject obj = new JSONObject(); 347 | try { 348 | obj.put("code", code); 349 | obj.put("message", message); 350 | } catch (JSONException ignore) { 351 | // This will never happen 352 | } 353 | return obj; 354 | } 355 | 356 | public void fail(JSONObject err) { 357 | this.callbackContext.error(err); 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /src/android/nl/xservices/plugins/videocaptureplus/xml/provider_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/ios/VideoCapturePlus.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | #import 5 | 6 | enum CDVCaptureError { 7 | CAPTURE_INTERNAL_ERR = 0, 8 | CAPTURE_APPLICATION_BUSY = 1, 9 | CAPTURE_INVALID_ARGUMENT = 2, 10 | CAPTURE_NO_MEDIA_FILES = 3, 11 | CAPTURE_NOT_SUPPORTED = 20 12 | }; 13 | typedef NSUInteger CDVCaptureError; 14 | 15 | typedef struct { 16 | BOOL iPhone; 17 | BOOL iPad; 18 | BOOL iPhone4; 19 | BOOL iPhone5; 20 | BOOL iPhone6; 21 | BOOL iPhone6Plus; 22 | BOOL retina; 23 | 24 | } CDV_iOSDevice; 25 | 26 | @interface CDVImagePickerPlus : UIImagePickerController { 27 | } 28 | @property (copy) NSString* callbackId; 29 | 30 | @end 31 | 32 | @interface VideoCapturePlus : CDVPlugin 33 | { 34 | CDVImagePickerPlus* pickerController; 35 | BOOL inUse; 36 | NSTimer* timer; 37 | AVCaptureSession *CaptureSession; 38 | AVCaptureMovieFileOutput *MovieFileOutput; 39 | UIImage* portraitOverlay; 40 | UIImage* landscapeOverlay; 41 | } 42 | @property BOOL inUse; 43 | @property (nonatomic, strong) NSTimer* timer; 44 | @property (strong, nonatomic) UILabel *overlayBox; 45 | @property (strong, nonatomic) UILabel *stopwatchLabel; 46 | @property (strong, nonatomic) UILabel *progressbarLabel; 47 | - (void)captureVideo:(CDVInvokedUrlCommand*)command; 48 | - (CDVPluginResult*)processVideo:(NSString*)moviePath forCallbackId:(NSString*)callbackId; 49 | - (void)getFormatData:(CDVInvokedUrlCommand*)command; 50 | - (NSDictionary*)getMediaDictionaryFromPath:(NSString*)fullPath ofType:(NSString*)type; 51 | - (void)imagePickerController:(UIImagePickerController*)picker didFinishPickingMediaWithInfo:(NSDictionary*)info; 52 | - (void)imagePickerController:(UIImagePickerController*)picker didFinishPickingImage:(UIImage*)image editingInfo:(NSDictionary*)editingInfo; 53 | 54 | @end 55 | -------------------------------------------------------------------------------- /src/ios/VideoCapturePlus.m: -------------------------------------------------------------------------------- 1 | #import "VideoCapturePlus.h" 2 | #import 3 | 4 | #define kW3CMediaFormatHeight @"height" 5 | #define kW3CMediaFormatWidth @"width" 6 | #define kW3CMediaFormatCodecs @"codecs" 7 | #define kW3CMediaFormatBitrate @"bitrate" 8 | #define kW3CMediaFormatDuration @"duration" 9 | 10 | @implementation CDVImagePickerPlus 11 | 12 | @synthesize callbackId; 13 | 14 | - (uint64_t)accessibilityTraits 15 | { 16 | NSString* systemVersion = [[UIDevice currentDevice] systemVersion]; 17 | 18 | if (([systemVersion compare:@"4.0" options:NSNumericSearch] != NSOrderedAscending)) { // this means system version is not less than 4.0 19 | return UIAccessibilityTraitStartsMediaSession; 20 | } 21 | 22 | return UIAccessibilityTraitNone; 23 | } 24 | 25 | - (BOOL)prefersStatusBarHidden { 26 | return YES; 27 | } 28 | 29 | - (UIViewController*)childViewControllerForStatusBarHidden { 30 | return nil; 31 | } 32 | 33 | - (void)viewWillAppear:(BOOL)animated { 34 | SEL sel = NSSelectorFromString(@"setNeedsStatusBarAppearanceUpdate"); 35 | if ([self respondsToSelector:sel]) { 36 | [self performSelector:sel withObject:nil afterDelay:0]; 37 | } 38 | 39 | [super viewWillAppear:animated]; 40 | } 41 | 42 | @end 43 | 44 | @implementation VideoCapturePlus 45 | @synthesize inUse, timer; 46 | 47 | - (void)pluginInitialize 48 | { 49 | self.inUse = NO; 50 | } 51 | 52 | -(void)rotateOverlayIfNeeded:(UIView*) overlayView { 53 | UIDeviceOrientation deviceOrientation = [UIDevice currentDevice].orientation; 54 | 55 | float rotation = 0; 56 | if (deviceOrientation == UIDeviceOrientationPortraitUpsideDown) { 57 | rotation = M_PI; 58 | } else if (deviceOrientation == UIDeviceOrientationLandscapeLeft) { 59 | rotation = M_PI_2; 60 | } else if (deviceOrientation == UIDeviceOrientationLandscapeRight) { 61 | rotation = -M_PI_2; 62 | } 63 | 64 | if (rotation != 0) { 65 | CGAffineTransform transform = overlayView.transform; 66 | transform = CGAffineTransformRotate(transform, rotation); 67 | overlayView.transform = transform; 68 | } 69 | } 70 | 71 | -(void)alignOverlayDimensionsWithOrientation { 72 | if (portraitOverlay == nil && landscapeOverlay == nil) { 73 | return; 74 | } 75 | 76 | UIView* overlayView = [[UIView alloc] initWithFrame:pickerController.view.frame]; 77 | 78 | // png transparency 79 | [overlayView.layer setOpaque:NO]; 80 | overlayView.opaque = NO; 81 | 82 | UIDeviceOrientation deviceOrientation = [UIDevice currentDevice].orientation; 83 | 84 | UIImage* overlayImage; 85 | if (UIDeviceOrientationIsLandscape(deviceOrientation)) { 86 | overlayImage = landscapeOverlay; 87 | } else { 88 | overlayImage = portraitOverlay; 89 | } 90 | // may be null if no image was passed for this orientation 91 | if (overlayImage != nil) { 92 | overlayView.backgroundColor = [UIColor colorWithPatternImage:overlayImage]; 93 | [overlayView setFrame:CGRectMake(0, 0, overlayImage.size.width, overlayImage.size.height)]; // x, y, width, height 94 | 95 | // regardless the orientation, these are the width and height in portrait mode 96 | float width = CGRectGetWidth(pickerController.view.frame); 97 | float height = CGRectGetHeight(pickerController.view.frame); 98 | 99 | CDV_iOSDevice device = [self getCurrentDevice]; 100 | if (device.iPad || device.iPhone6Plus) { 101 | if (UIDeviceOrientationIsLandscape(deviceOrientation)) { 102 | [overlayView setCenter:CGPointMake(height/2,width/2)]; 103 | } else { 104 | [overlayView setCenter:CGPointMake(width/2,height/2)]; 105 | } 106 | } else { 107 | // on iPad, the image rotates with the orientation, but on iPhone it doesn't - so we have to manually rotate the overlay on iPhone 108 | [self rotateOverlayIfNeeded:overlayView]; 109 | [overlayView setCenter:CGPointMake(width/2,height/2)]; 110 | } 111 | pickerController.cameraOverlayView = overlayView; 112 | } 113 | } 114 | 115 | - (void) orientationChanged:(NSNotification *)notification { 116 | [self alignOverlayDimensionsWithOrientation]; 117 | } 118 | 119 | - (void)captureVideo:(CDVInvokedUrlCommand*)command { 120 | 121 | NSString* callbackId = command.callbackId; 122 | NSDictionary* options = [command.arguments objectAtIndex:0]; 123 | 124 | // emit and capture changes to the deviceOrientation 125 | [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications]; 126 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(orientationChanged:) name:@"UIDeviceOrientationDidChangeNotification" object:nil]; 127 | 128 | // enable this line of code if you want to do stuff when the capture session is started 129 | // [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didStartRunning:) name:AVCaptureSessionDidStartRunningNotification object:nil]; 130 | 131 | // TODO try this: self.timer = [NSTimer scheduledTimerWithTimeInterval:0.5f target:self selector:@selector(updateStopwatchLabel) userInfo:nil repeats:YES]; 132 | // timer en session.running property gebruiken? 133 | 134 | if ([options isKindOfClass:[NSNull class]]) { 135 | options = [NSDictionary dictionary]; 136 | } 137 | 138 | // options could contain limit, duration, highquality, frontcamera and mode 139 | // taking more than one video (limit) is only supported if provide own controls via cameraOverlayView property 140 | NSNumber* duration = [options objectForKey:@"duration"]; 141 | BOOL highquality = [[options objectForKey:@"highquality"] boolValue]; 142 | BOOL frontcamera = [[options objectForKey:@"frontcamera"] boolValue]; 143 | portraitOverlay = [self getImage:[options objectForKey:@"portraitOverlay"]]; 144 | landscapeOverlay = [self getImage:[options objectForKey:@"landscapeOverlay"]]; 145 | NSString* overlayText = [options objectForKey:@"overlayText"]; 146 | NSString* mediaType = nil; 147 | 148 | if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) { 149 | // there is a camera, it is available, make sure it can do movies 150 | pickerController = [[CDVImagePickerPlus alloc] init]; 151 | 152 | NSArray* types = nil; 153 | if ([UIImagePickerController respondsToSelector:@selector(availableMediaTypesForSourceType:)]) { 154 | types = [UIImagePickerController availableMediaTypesForSourceType:UIImagePickerControllerSourceTypeCamera]; 155 | // NSLog(@"MediaTypes: %@", [types description]); 156 | 157 | if ([types containsObject:(NSString*)kUTTypeMovie]) { 158 | mediaType = (NSString*)kUTTypeMovie; 159 | } else if ([types containsObject:(NSString*)kUTTypeVideo]) { 160 | mediaType = (NSString*)kUTTypeVideo; 161 | } 162 | } 163 | } 164 | if (!mediaType) { 165 | // don't have video camera return error 166 | NSLog(@"Capture.captureVideo: video mode not available."); 167 | CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageToErrorObject:CAPTURE_NOT_SUPPORTED]; 168 | [self.commandDelegate sendPluginResult:result callbackId:callbackId]; 169 | pickerController = nil; 170 | } else { 171 | pickerController.delegate = self; 172 | 173 | pickerController.sourceType = UIImagePickerControllerSourceTypeCamera; 174 | pickerController.allowsEditing = NO; 175 | // iOS 3.0 176 | pickerController.mediaTypes = [NSArray arrayWithObjects:mediaType, nil]; 177 | 178 | if ([mediaType isEqualToString:(NSString*)kUTTypeMovie]){ 179 | if (duration) { 180 | pickerController.videoMaximumDuration = [duration doubleValue]; 181 | } 182 | } 183 | 184 | // iOS 4.0 185 | if ([pickerController respondsToSelector:@selector(cameraCaptureMode)]) { 186 | pickerController.cameraCaptureMode = UIImagePickerControllerCameraCaptureModeVideo; 187 | if (highquality) { 188 | pickerController.videoQuality = UIImagePickerControllerQualityTypeHigh; 189 | } 190 | if (frontcamera) { 191 | pickerController.cameraDevice = UIImagePickerControllerCameraDeviceFront; 192 | } 193 | 194 | pickerController.delegate = self; 195 | [self alignOverlayDimensionsWithOrientation]; 196 | 197 | 198 | 199 | if(overlayText != nil) { 200 | NSUInteger txtLength = overlayText.length; 201 | 202 | CGRect labelFrame = CGRectMake(10, 40, CGRectGetWidth(pickerController.view.frame) - 20, 40 + (20*(txtLength/25))); 203 | 204 | self.overlayBox = [[UILabel alloc] initWithFrame:labelFrame]; 205 | 206 | self.overlayBox.textColor = [UIColor colorWithRed:3/255.0f green:211/255.0f blue:255/255.0f alpha:1.0f]; 207 | self.overlayBox.backgroundColor = [UIColor colorWithRed:0/255.0f green:0/255.0f blue:0/255.0f alpha:0.7f]; 208 | self.overlayBox.font = [UIFont systemFontOfSize:16]; 209 | self.overlayBox.lineBreakMode = NSLineBreakByWordWrapping; 210 | self.overlayBox.numberOfLines = 10; 211 | self.overlayBox.alpha = 0.90; 212 | self.overlayBox.textAlignment = NSTextAlignmentCenter; 213 | self.overlayBox.text = overlayText; 214 | [pickerController.view addSubview:self.overlayBox]; 215 | } 216 | 217 | 218 | // trying to add a progressbar to the bottom 219 | /* 220 | CGRect progressbarLabelFrame = CGRectMake(0, 0, pickerController.cameraOverlayView.frame.size.width/2, 4); 221 | self.progressbarLabel = [[UILabel alloc] initWithFrame:progressbarLabelFrame]; 222 | self.progressbarLabel.backgroundColor = [UIColor redColor]; 223 | [pickerController.cameraOverlayView addSubview:self.progressbarLabel]; 224 | 225 | self.timer = [NSTimer scheduledTimerWithTimeInterval:0.5f target:self selector:@selector(updateStopwatchLabel) userInfo:nil repeats:YES]; 226 | */ 227 | 228 | // TODO make this configurable via the API (but only if Android supports it) 229 | // pickerController.cameraFlashMode = UIImagePickerControllerCameraFlashModeAuto; 230 | } 231 | 232 | // CDVImagePickerPlus specific property 233 | pickerController.callbackId = callbackId; 234 | [self.viewController presentViewController:pickerController animated:YES completion:nil]; 235 | } 236 | } 237 | 238 | -(UIImage*)getImage: (NSString *)imageName { 239 | UIImage *image = nil; 240 | if (imageName != (id)[NSNull null]) { 241 | if ([imageName rangeOfString:@"http"].location == 0) { // from the internet? 242 | image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:imageName]]]; 243 | } else if ([imageName rangeOfString:@"www/"].location == 0) { // www folder? 244 | image = [UIImage imageNamed:imageName]; 245 | } else if ([imageName rangeOfString:@"file://"].location == 0) { 246 | // using file: protocol? then strip the file:// part 247 | image = [UIImage imageWithData:[NSData dataWithContentsOfFile:[[NSURL URLWithString:imageName] path]]]; 248 | } else { 249 | // assume anywhere else, on the local filesystem 250 | image = [UIImage imageWithData:[NSData dataWithContentsOfFile:imageName]]; 251 | } 252 | } 253 | return image; 254 | } 255 | 256 | //- (void)updateStopwatchLabel { 257 | // update the label with the elapsed time 258 | // [self.stopwatchLabel setText:[self.timer.timeInterval]]; 259 | // [self.timerLabel setText:[self formatTime:self.avRecorder.currentTime]]; 260 | //} 261 | 262 | - (CDVPluginResult*)processVideo:(NSString*)moviePath forCallbackId:(NSString*)callbackId { 263 | // save the movie to photo album (only avail as of iOS 3.1) 264 | NSDictionary* fileDict = [self getMediaDictionaryFromPath:moviePath ofType:nil]; 265 | NSArray* fileArray = [NSArray arrayWithObject:fileDict]; 266 | return [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:fileArray]; 267 | } 268 | 269 | - (NSString*)getMimeTypeFromFullPath:(NSString*)fullPath { 270 | NSString* mimeType = nil; 271 | 272 | if (fullPath) { 273 | CFStringRef typeId = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)[fullPath pathExtension], NULL); 274 | if (typeId) { 275 | mimeType = (__bridge_transfer NSString*)UTTypeCopyPreferredTagWithClass(typeId, kUTTagClassMIMEType); 276 | if (!mimeType) { 277 | // special case for m4a 278 | if ([(__bridge NSString*)typeId rangeOfString : @"m4a-audio"].location != NSNotFound) { 279 | mimeType = @"audio/mp4"; 280 | } else if ([[fullPath pathExtension] rangeOfString:@"wav"].location != NSNotFound) { 281 | mimeType = @"audio/wav"; 282 | } 283 | } 284 | CFRelease(typeId); 285 | } 286 | } 287 | return mimeType; 288 | } 289 | 290 | - (void)getFormatData:(CDVInvokedUrlCommand*)command { 291 | NSString* callbackId = command.callbackId; 292 | // existence of fullPath checked on JS side 293 | NSString* fullPath = [command.arguments objectAtIndex:0]; 294 | // mimeType could be null 295 | NSString* mimeType = nil; 296 | 297 | if ([command.arguments count] > 1) { 298 | mimeType = [command.arguments objectAtIndex:1]; 299 | } 300 | BOOL bError = NO; 301 | CDVCaptureError errorCode = CAPTURE_INTERNAL_ERR; 302 | CDVPluginResult* result = nil; 303 | 304 | if (!mimeType || [mimeType isKindOfClass:[NSNull class]]) { 305 | // try to determine mime type if not provided 306 | mimeType = [self getMimeTypeFromFullPath:fullPath]; 307 | if (!mimeType) { 308 | // can't do much without mimeType, return error 309 | bError = YES; 310 | errorCode = CAPTURE_INVALID_ARGUMENT; 311 | } 312 | } 313 | if (!bError) { 314 | // create and initialize return dictionary 315 | NSMutableDictionary* formatData = [NSMutableDictionary dictionaryWithCapacity:5]; 316 | [formatData setObject:[NSNull null] forKey:kW3CMediaFormatCodecs]; 317 | [formatData setObject:[NSNumber numberWithInt:0] forKey:kW3CMediaFormatBitrate]; 318 | [formatData setObject:[NSNumber numberWithInt:0] forKey:kW3CMediaFormatHeight]; 319 | [formatData setObject:[NSNumber numberWithInt:0] forKey:kW3CMediaFormatWidth]; 320 | [formatData setObject:[NSNumber numberWithInt:0] forKey:kW3CMediaFormatDuration]; 321 | 322 | if (([mimeType rangeOfString:@"video/"].location != NSNotFound) && (NSClassFromString(@"AVURLAsset") != nil)) { 323 | NSURL* movieURL = [NSURL fileURLWithPath:fullPath]; 324 | AVURLAsset* movieAsset = [[AVURLAsset alloc] initWithURL:movieURL options:nil]; 325 | CMTime duration = [movieAsset duration]; 326 | [formatData setObject:[NSNumber numberWithFloat:CMTimeGetSeconds(duration)] forKey:kW3CMediaFormatDuration]; 327 | 328 | NSArray* allVideoTracks = [movieAsset tracksWithMediaType:AVMediaTypeVideo]; 329 | if ([allVideoTracks count] > 0) { 330 | AVAssetTrack* track = [[movieAsset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0]; 331 | CGSize size = [track naturalSize]; 332 | 333 | [formatData setObject:[NSNumber numberWithFloat:size.height] forKey:kW3CMediaFormatHeight]; 334 | [formatData setObject:[NSNumber numberWithFloat:size.width] forKey:kW3CMediaFormatWidth]; 335 | } else { 336 | NSLog(@"No video tracks found for %@", fullPath); 337 | } 338 | } 339 | result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:formatData]; 340 | } 341 | if (bError) { 342 | result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR]; 343 | } 344 | if (result) { 345 | [self.commandDelegate sendPluginResult:result callbackId:callbackId]; 346 | } 347 | } 348 | 349 | - (NSDictionary*)getMediaDictionaryFromPath:(NSString*)fullPath ofType:(NSString*)type { 350 | NSFileManager* fileMgr = [[NSFileManager alloc] init]; 351 | NSMutableDictionary* fileDict = [NSMutableDictionary dictionaryWithCapacity:5]; 352 | 353 | [fileDict setObject:[fullPath lastPathComponent] forKey:@"name"]; 354 | [fileDict setObject:fullPath forKey:@"fullPath"]; 355 | // determine type 356 | if (!type) { 357 | NSString* mimeType = [self getMimeTypeFromFullPath:fullPath]; 358 | [fileDict setObject:(mimeType != nil ? (NSObject*)mimeType : [NSNull null]) forKey:@"type"]; 359 | } 360 | NSDictionary* fileAttrs = [fileMgr attributesOfItemAtPath:fullPath error:nil]; 361 | [fileDict setObject:[NSNumber numberWithUnsignedLongLong:[fileAttrs fileSize]] forKey:@"size"]; 362 | NSDate* modDate = [fileAttrs fileModificationDate]; 363 | NSNumber* msDate = [NSNumber numberWithDouble:[modDate timeIntervalSince1970] * 1000]; 364 | [fileDict setObject:msDate forKey:@"lastModifiedDate"]; 365 | 366 | return fileDict; 367 | } 368 | 369 | - (void)imagePickerController:(UIImagePickerController*)picker didFinishPickingImage:(UIImage*)image editingInfo:(NSDictionary*)editingInfo { 370 | // older api calls new one 371 | [self imagePickerController:picker didFinishPickingMediaWithInfo:editingInfo]; 372 | } 373 | 374 | /* Called when movie is finished recording. 375 | * Calls success or error code as appropriate 376 | * if successful, result contains an array (with just one entry since can only get one image unless build own camera UI) of MediaFile object representing the image 377 | * name 378 | * fullPath 379 | * type 380 | * lastModifiedDate 381 | * size 382 | */ 383 | - (void)imagePickerController:(UIImagePickerController*)picker didFinishPickingMediaWithInfo:(NSDictionary*)info { 384 | CDVImagePickerPlus* cameraPicker = (CDVImagePickerPlus*)picker; 385 | NSString* callbackId = cameraPicker.callbackId; 386 | 387 | if ([picker respondsToSelector:@selector(presentingViewController)]) { 388 | [[picker presentingViewController] dismissViewControllerAnimated:YES completion:nil]; 389 | } else { 390 | [[picker parentViewController] dismissViewControllerAnimated:YES completion:nil]; 391 | } 392 | 393 | CDVPluginResult* result = nil; 394 | NSString* moviePath = [[info objectForKey:UIImagePickerControllerMediaURL] path]; 395 | if (moviePath) { 396 | result = [self processVideo:moviePath forCallbackId:callbackId]; 397 | } 398 | if (!result) { 399 | result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageToErrorObject:CAPTURE_INTERNAL_ERR]; 400 | } 401 | [self.commandDelegate sendPluginResult:result callbackId:callbackId]; 402 | pickerController = nil; 403 | } 404 | 405 | - (void)imagePickerControllerDidCancel:(UIImagePickerController*)picker { 406 | CDVImagePickerPlus* cameraPicker = (CDVImagePickerPlus*)picker; 407 | NSString* callbackId = cameraPicker.callbackId; 408 | 409 | if ([picker respondsToSelector:@selector(presentingViewController)]) { 410 | [[picker presentingViewController] dismissViewControllerAnimated:YES completion:nil]; 411 | } else { 412 | [[picker parentViewController] dismissViewControllerAnimated:YES completion:nil]; 413 | } 414 | 415 | CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageToErrorObject:CAPTURE_NO_MEDIA_FILES]; 416 | [self.commandDelegate sendPluginResult:result callbackId:callbackId]; 417 | pickerController = nil; 418 | } 419 | 420 | - (CDV_iOSDevice) getCurrentDevice 421 | { 422 | CDV_iOSDevice device; 423 | 424 | UIScreen* mainScreen = [UIScreen mainScreen]; 425 | CGFloat mainScreenHeight = mainScreen.bounds.size.height; 426 | CGFloat mainScreenWidth = mainScreen.bounds.size.width; 427 | 428 | int limit = MAX(mainScreenHeight,mainScreenWidth); 429 | 430 | device.iPad = (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad); 431 | device.iPhone = (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone); 432 | device.retina = ([mainScreen scale] == 2.0); 433 | device.iPhone4 = (device.iPhone && limit == 480.0); 434 | device.iPhone5 = (device.iPhone && limit == 568.0); 435 | // note these below is not a true device detect, for example if you are on an 436 | // iPhone 6/6+ but the app is scaled it will prob set iPhone5 as true, but 437 | // this is appropriate for detecting the runtime screen environment 438 | device.iPhone6 = (device.iPhone && limit == 667.0); 439 | device.iPhone6Plus = (device.iPhone && limit == 736.0); 440 | 441 | return device; 442 | } 443 | 444 | @end 445 | -------------------------------------------------------------------------------- /www/VideoCapturePlus.js: -------------------------------------------------------------------------------- 1 | function VideoCapturePlus() { 2 | } 3 | 4 | VideoCapturePlus.prototype.captureVideo = function (successCallback, errorCallback, options) { 5 | var win = function(pluginResult) { 6 | var mediaFiles = []; 7 | var i; 8 | for (i = 0; i < pluginResult.length; i++) { 9 | mediaFiles.push(new MediaFile( 10 | pluginResult[i].name, 11 | pluginResult[i].fullPath, 12 | pluginResult[i].type, 13 | pluginResult[i].lastModifiedDate, 14 | pluginResult[i].size)); 15 | } 16 | successCallback(mediaFiles); 17 | }; 18 | cordova.exec(win, errorCallback, "VideoCapturePlus", "captureVideo", [options]); 19 | }; 20 | 21 | var MediaFile = function(name, fullPath, type, lastModifiedDate, size) { 22 | this.name = name; 23 | this.fullPath = fullPath; 24 | this.type = type; 25 | this.lastModifiedDate = lastModifiedDate; 26 | this.size = size; 27 | }; 28 | 29 | MediaFile.prototype.getFormatData = function(successCallback, errorCallback) { 30 | if (typeof this.fullPath === "undefined" || this.fullPath === null) { 31 | errorCallback("invalid argument"); 32 | } else { 33 | cordova.exec(successCallback, errorCallback, "VideoCapturePlus", "getFormatData", [this.fullPath, this.type]); 34 | } 35 | }; 36 | 37 | VideoCapturePlus.install = function () { 38 | if (!window.plugins) { 39 | window.plugins = {}; 40 | } 41 | 42 | window.plugins.videocaptureplus = new VideoCapturePlus(); 43 | return window.plugins.videocaptureplus; 44 | }; 45 | 46 | cordova.addConstructor(VideoCapturePlus.install); --------------------------------------------------------------------------------