├── .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 | 
33 |
34 | During recording, landscape mode:
35 |
36 | 
37 |
38 | Reviewing the recording, portrait mode:
39 |
40 | 
41 |
42 | After recording you can extract the metadata, [see the demo folder for the code of this example](demo):
43 |
44 | 
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 |
14 |
Connecting to Device
15 |
Device is Ready
16 |
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);
--------------------------------------------------------------------------------