Commit bb332447 authored by 李晓兵's avatar 李晓兵

'录视频插件'

parent b7b6406b
<!--
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
-->
# Contributing to Apache Cordova
Anyone can contribute to Cordova. And we need your contributions.
There are multiple ways to contribute: report bugs, improve the docs, and
contribute code.
For instructions on this, start with the
[contribution overview](http://cordova.apache.org/contribute/).
The details are explained there, but the important items are:
- Sign and submit an Apache ICLA (Contributor License Agreement).
- Have a Jira issue open that corresponds to your contribution.
- Run the tests so your patch doesn't break existing functionality.
We look forward to your contributions!
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
\ No newline at end of file
Apache Cordova
Copyright 2012 The Apache Software Foundation
This product includes software developed at
The Apache Software Foundation (http://www.apache.org/).
---
title: Media Capture
description: Capture audio, video, and images.
---
<!--
# license: Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
-->
|AppVeyor|Travis CI|
|:-:|:-:|
|[![Build status](https://ci.appveyor.com/api/projects/status/github/apache/cordova-plugin-media-capture?branch=master)](https://ci.appveyor.com/project/ApacheSoftwareFoundation/cordova-plugin-media-capture)|[![Build Status](https://travis-ci.org/apache/cordova-plugin-media-capture.svg?branch=master)](https://travis-ci.org/apache/cordova-plugin-media-capture)|
# cordova-plugin-media-capture
This plugin provides access to the device's audio, image, and video capture capabilities.
__WARNING__: Collection and use of images, video, or
audio from the device's camera or microphone raises important privacy
issues. Your app's privacy policy should discuss how the app uses
such sensors and whether the data recorded is shared with any other
parties. In addition, if the app's use of the camera or microphone is
not apparent in the user interface, you should provide a just-in-time
notice before the app accesses the camera or microphone (if the
device operating system doesn't do so already). That notice should
provide the same information noted above, as well as obtaining the
user's permission (e.g., by presenting choices for __OK__ and __No
Thanks__). Note that some app marketplaces may require your app to
provide just-in-time notice and obtain permission from the user prior
to accessing the camera or microphone. For more information, please
see the Privacy Guide.
This plugin defines global `navigator.device.capture` object.
Although in the global scope, it is not available until after the `deviceready` event.
document.addEventListener("deviceready", onDeviceReady, false);
function onDeviceReady() {
console.log(navigator.device.capture);
}
## Installation
cordova plugin add cordova-plugin-media-capture
## Supported Platforms
- Android
- Browser
- iOS
- Windows
## Objects
- Capture
- CaptureAudioOptions
- CaptureImageOptions
- CaptureVideoOptions
- CaptureCallback
- CaptureErrorCB
- ConfigurationData
- MediaFile
- MediaFileData
## Methods
- capture.captureAudio
- capture.captureImage
- capture.captureVideo
- MediaFile.getFormatData
## Properties
- __supportedAudioModes__: The audio recording formats supported by the device. (ConfigurationData[])
- __supportedImageModes__: The recording image sizes and formats supported by the device. (ConfigurationData[])
- __supportedVideoModes__: The recording video resolutions and formats supported by the device. (ConfigurationData[])
## capture.captureAudio
> Start the audio recorder application and return information about captured audio clip files.
navigator.device.capture.captureAudio(
CaptureCB captureSuccess, CaptureErrorCB captureError, [CaptureAudioOptions options]
);
### Description
Starts an asynchronous operation to capture audio recordings using the
device's default audio recording application. The operation allows
the device user to capture multiple recordings in a single session.
The capture operation ends when either the user exits the audio
recording application, or the maximum number of recordings specified
by `CaptureAudioOptions.limit` is reached. If no `limit` parameter
value is specified, it defaults to one (1), and the capture operation
terminates after the user records a single audio clip.
When the capture operation finishes, the `CaptureCallback` executes
with an array of `MediaFile` objects describing each captured audio
clip file. If the user terminates the operation before an audio clip
is captured, the `CaptureErrorCallback` executes with a `CaptureError`
object, featuring the `CaptureError.CAPTURE_NO_MEDIA_FILES` error
code.
### Supported Platforms
- Android
- iOS
- Windows
### Example
// capture callback
var captureSuccess = function(mediaFiles) {
var i, path, len;
for (i = 0, len = mediaFiles.length; i < len; i += 1) {
path = mediaFiles[i].fullPath;
// do something interesting with the file
}
};
// capture error callback
var captureError = function(error) {
navigator.notification.alert('Error code: ' + error.code, null, 'Capture Error');
};
// start audio capture
navigator.device.capture.captureAudio(captureSuccess, captureError, {limit:2});
### iOS Quirks
- iOS does not have a default audio recording application, so a simple user interface is provided.
### Windows Phone 7 and 8 Quirks
- Windows Phone 7 does not have a default audio recording application, so a simple user interface is provided.
## capture.captureImage
> Start the camera application and return information about captured image files.
navigator.device.capture.captureImage(
CaptureCB captureSuccess, CaptureErrorCB captureError, [CaptureImageOptions options]
);
### Description
Starts an asynchronous operation to capture images using the device's
camera application. The operation allows users to capture more than
one image in a single session.
The capture operation ends either when the user closes the camera
application, or the maximum number of recordings specified by
`CaptureImageOptions.limit` is reached. If no `limit` value is
specified, it defaults to one (1), and the capture operation
terminates after the user captures a single image.
When the capture operation finishes, it invokes the `CaptureCB`
callback with an array of `MediaFile` objects describing each captured
image file. If the user terminates the operation before capturing an
image, the `CaptureErrorCB` callback executes with a `CaptureError`
object featuring a `CaptureError.CAPTURE_NO_MEDIA_FILES` error code.
### Supported Platforms
- Android
- Browser
- iOS
- Windows
### iOS Quirks
Since iOS 10 it's mandatory to provide an usage description in the `info.plist` if trying to access privacy-sensitive data. When the system prompts the user to allow access, this usage description string will displayed as part of the permission dialog box, but if you didn't provide the usage description, the app will crash before showing the dialog. Also, Apple will reject apps that access private data but don't provide an usage description.
This plugins requires the following usage descriptions:
* `NSCameraUsageDescription` describes the reason the app accesses the user's camera.
* `NSMicrophoneUsageDescription` describes the reason the app accesses the user's microphone.
* `NSPhotoLibraryUsageDescriptionentry` describes the reason the app accesses the user's photo library.
To add these entries into the `info.plist`, you can use the `edit-config` tag in the `config.xml` like this:
```
<edit-config target="NSCameraUsageDescription" file="*-Info.plist" mode="merge">
<string>need camera access to take pictures</string>
</edit-config>
```
```
<edit-config target="NSMicrophoneUsageDescription" file="*-Info.plist" mode="merge">
<string>need microphone access to record sounds</string>
</edit-config>
```
```
<edit-config target="NSPhotoLibraryUsageDescription" file="*-Info.plist" mode="merge">
<string>need to photo library access to get pictures from there</string>
</edit-config>
```
### Browser Quirks
Works in Chrome, Firefox and Opera only (since IE and Safari doesn't supports
navigator.getUserMedia API)
Displaying images using captured file's URL available in Chrome/Opera only.
Firefox stores captured images in IndexedDB storage (see File plugin documentation),
and due to this the only way to show captured image is to read it and show using its DataURL.
### Example
// capture callback
var captureSuccess = function(mediaFiles) {
var i, path, len;
for (i = 0, len = mediaFiles.length; i < len; i += 1) {
path = mediaFiles[i].fullPath;
// do something interesting with the file
}
};
// capture error callback
var captureError = function(error) {
navigator.notification.alert('Error code: ' + error.code, null, 'Capture Error');
};
// start image capture
navigator.device.capture.captureImage(captureSuccess, captureError, {limit:2});
## capture.captureVideo
> Start the video recorder application and return information about captured video clip files.
navigator.device.capture.captureVideo(
CaptureCB captureSuccess, CaptureErrorCB captureError, [CaptureVideoOptions options]
);
### Description
Starts an asynchronous operation to capture video recordings using the
device's video recording application. The operation allows the user
to capture more than one recordings in a single session.
The capture operation ends when either the user exits the video
recording application, or the maximum number of recordings specified
by `CaptureVideoOptions.limit` is reached. If no `limit` parameter
value is specified, it defaults to one (1), and the capture operation
terminates after the user records a single video clip.
When the capture operation finishes, it the `CaptureCB` callback
executes with an array of `MediaFile` objects describing each captured
video clip file. If the user terminates the operation before
capturing a video clip, the `CaptureErrorCB` callback executes with a
`CaptureError` object featuring a
`CaptureError.CAPTURE_NO_MEDIA_FILES` error code.
### Supported Platforms
- Android
- iOS
- Windows
### Example
// capture callback
var captureSuccess = function(mediaFiles) {
var i, path, len;
for (i = 0, len = mediaFiles.length; i < len; i += 1) {
path = mediaFiles[i].fullPath;
// do something interesting with the file
}
};
// capture error callback
var captureError = function(error) {
navigator.notification.alert('Error code: ' + error.code, null, 'Capture Error');
};
// start video capture
navigator.device.capture.captureVideo(captureSuccess, captureError, {limit:2});
## CaptureAudioOptions
> Encapsulates audio capture configuration options.
### Properties
- __limit__: The maximum number of audio clips the device user can record in a single capture operation. The value must be greater than or equal to 1 (defaults to 1).
- __duration__: The maximum duration of an audio sound clip, in seconds.
### Example
// limit capture operation to 3 media files, no longer than 10 seconds each
var options = { limit: 3, duration: 10 };
navigator.device.capture.captureAudio(captureSuccess, captureError, options);
### Android Quirks
- The `duration` parameter is not supported. Recording lengths can't be limited programmatically.
### iOS Quirks
- The `limit` parameter is not supported, so only one recording can be created for each invocation.
## CaptureImageOptions
> Encapsulates image capture configuration options.
### Properties
- __limit__: The maximum number of images the user can capture in a single capture operation. The value must be greater than or equal to 1 (defaults to 1).
### Example
// limit capture operation to 3 images
var options = { limit: 3 };
navigator.device.capture.captureImage(captureSuccess, captureError, options);
### iOS Quirks
- The __limit__ parameter is not supported, and only one image is taken per invocation.
## CaptureVideoOptions
> Encapsulates video capture configuration options.
### Properties
- __limit__: The maximum number of video clips the device's user can capture in a single capture operation. The value must be greater than or equal to 1 (defaults to 1).
- __duration__: The maximum duration of a video clip, in seconds.
### Example
// limit capture operation to 3 video clips
var options = { limit: 3 };
navigator.device.capture.captureVideo(captureSuccess, captureError, options);
### iOS Quirks
- The __limit__ property is ignored. Only one video is recorded per invocation.
### Android Quirks
- Android supports an additional __quality__ property, to allow capturing video at different qualities. A value of `1` ( the default ) means high quality and value of `0` means low quality, suitable for MMS messages.
See [here](http://developer.android.com/reference/android/provider/MediaStore.html#EXTRA_VIDEO_QUALITY) for more details.
### Example ( Android w/ quality )
// limit capture operation to 1 video clip of low quality
var options = { limit: 1, quality: 0 };
navigator.device.capture.captureVideo(captureSuccess, captureError, options);
## CaptureCB
> Invoked upon a successful media capture operation.
function captureSuccess( MediaFile[] mediaFiles ) { ... };
### Description
This function executes after a successful capture operation completes.
At this point a media file has been captured, and either the user has
exited the media capture application, or the capture limit has been
reached.
Each `MediaFile` object describes a captured media file.
### Example
// capture callback
function captureSuccess(mediaFiles) {
var i, path, len;
for (i = 0, len = mediaFiles.length; i < len; i += 1) {
path = mediaFiles[i].fullPath;
// do something interesting with the file
}
};
## CaptureError
> Encapsulates the error code resulting from a failed media capture operation.
### Properties
- __code__: One of the pre-defined error codes listed below.
### Constants
- `CaptureError.CAPTURE_INTERNAL_ERR`: The camera or microphone failed to capture image or sound.
- `CaptureError.CAPTURE_APPLICATION_BUSY`: The camera or audio capture application is currently serving another capture request.
- `CaptureError.CAPTURE_INVALID_ARGUMENT`: Invalid use of the API (e.g., the value of `limit` is less than one).
- `CaptureError.CAPTURE_NO_MEDIA_FILES`: The user exits the camera or audio capture application before capturing anything.
- `CaptureError.CAPTURE_PERMISSION_DENIED`: The user denied a permission required to perform the given capture request.
- `CaptureError.CAPTURE_NOT_SUPPORTED`: The requested capture operation is not supported.
## CaptureErrorCB
> Invoked if an error occurs during a media capture operation.
function captureError( CaptureError error ) { ... };
### Description
This function executes if an error occurs when trying to launch a
media capture operation. Failure scenarios include when the capture
application is busy, a capture operation is already taking place, or
the user cancels the operation before any media files are captured.
This function executes with a `CaptureError` object containing an
appropriate error `code`.
### Example
// capture error callback
var captureError = function(error) {
navigator.notification.alert('Error code: ' + error.code, null, 'Capture Error');
};
## ConfigurationData
> Encapsulates a set of media capture parameters that a device supports.
### Description
Describes media capture modes supported by the device. The
configuration data includes the MIME type, and capture dimensions for
video or image capture.
The MIME types should adhere to [RFC2046](http://www.ietf.org/rfc/rfc2046.txt). Examples:
- `video/3gpp`
- `video/quicktime`
- `image/jpeg`
- `audio/amr`
- `audio/wav`
### Properties
- __type__: The ASCII-encoded lowercase string representing the media type. (DOMString)
- __height__: The height of the image or video in pixels. The value is zero for sound clips. (Number)
- __width__: The width of the image or video in pixels. The value is zero for sound clips. (Number)
### Example
// retrieve supported image modes
var imageModes = navigator.device.capture.supportedImageModes;
// Select mode that has the highest horizontal resolution
var width = 0;
var selectedmode;
for each (var mode in imageModes) {
if (mode.width > width) {
width = mode.width;
selectedmode = mode;
}
}
Not supported by any platform. All configuration data arrays are empty.
## MediaFile.getFormatData
> Retrieves format information about the media capture file.
mediaFile.getFormatData(
MediaFileDataSuccessCB successCallback,
[MediaFileDataErrorCB errorCallback]
);
### Description
This function asynchronously attempts to retrieve the format
information for the media file. If successful, it invokes the
`MediaFileDataSuccessCB` callback with a `MediaFileData` object. If
the attempt fails, this function invokes the `MediaFileDataErrorCB`
callback.
### Supported Platforms
- Android
- iOS
- Windows
### Android Quirks
The API to access media file format information is limited, so not all
`MediaFileData` properties are supported.
### iOS Quirks
The API to access media file format information is limited, so not all
`MediaFileData` properties are supported.
## MediaFile
> Encapsulates properties of a media capture file.
### Properties
- __name__: The name of the file, without path information. (DOMString)
- __fullPath__: The full path of the file, including the name. (DOMString)
- __type__: The file's mime type (DOMString)
- __lastModifiedDate__: The date and time when the file was last modified. (Date)
- __size__: The size of the file, in bytes. (Number)
### Methods
- __MediaFile.getFormatData__: Retrieves the format information of the media file.
## MediaFileData
> Encapsulates format information about a media file.
### Properties
- __codecs__: The actual format of the audio and video content. (DOMString)
- __bitrate__: The average bitrate of the content. The value is zero for images. (Number)
- __height__: The height of the image or video in pixels. The value is zero for audio clips. (Number)
- __width__: The width of the image or video in pixels. The value is zero for audio clips. (Number)
- __duration__: The length of the video or sound clip in seconds. The value is zero for images. (Number)
### Android Quirks
Supports the following `MediaFileData` properties:
- __codecs__: Not supported, and returns `null`.
- __bitrate__: Not supported, and returns zero.
- __height__: Supported: image and video files only.
- __width__: Supported: image and video files only.
- __duration__: Supported: audio and video files only.
### iOS Quirks
Supports the following `MediaFileData` properties:
- __codecs__: Not supported, and returns `null`.
- __bitrate__: Supported on iOS4 devices for audio only. Returns zero for images and videos.
- __height__: Supported: image and video files only.
- __width__: Supported: image and video files only.
- __duration__: Supported: audio and video files only.
## Android Lifecycle Quirks
When capturing audio, video, or images on the Android platform, there is a chance that the
application will get destroyed after the Cordova Webview is pushed to the background by
the native capture application. See the [Android Lifecycle Guide][android-lifecycle] for
a full description of the issue. In this case, the success and failure callbacks passed
to the capture method will not be fired and instead the results of the call will be
delivered via a document event that fires after the Cordova [resume event][resume-event].
In your app, you should subscribe to the two possible events like so:
```javascript
function onDeviceReady() {
// pendingcaptureresult is fired if the capture call is successful
document.addEventListener('pendingcaptureresult', function(mediaFiles) {
// Do something with result
});
// pendingcaptureerror is fired if the capture call is unsuccessful
document.addEventListener('pendingcaptureerror', function(error) {
// Handle error case
});
}
// Only subscribe to events after deviceready fires
document.addEventListener('deviceready', onDeviceReady);
```
It is up you to track what part of your code these results are coming from. Be sure to
save and restore your app's state as part of the [pause][pause-event] and
[resume][resume-event] events as appropriate. Please note that these events will only
fire on the Android platform and only when the Webview was destroyed during a capture
operation.
[android-lifecycle]: http://cordova.apache.org/docs/en/latest/guide/platforms/android/index.html#lifecycle-guide
[pause-event]: http://cordova.apache.org/docs/en/latest/cordova/events/events.html#pause
[resume-event]: http://cordova.apache.org/docs/en/latest/cordova/events/events.html#resume
<!--
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
-->
# Release Notes
### 3.0.3 (Jun 19, 2019)
- fix(android): Catch ActivityNotFoundException ([#104](https://github.com/apache/cordova-plugin-media-capture/issues/104)) ([`f69ba2a`](https://github.com/apache/cordova-plugin-media-capture/commit/f69ba2a))
- fix(android): CB-14260: (android) captureImage permission denial on android 8.1 ([#95](https://github.com/apache/cordova-plugin-media-capture/issues/95)) ([`3755f9f`](https://github.com/apache/cordova-plugin-media-capture/commit/3755f9f))
- docs: remove outdated translations ([`6422b2b`](https://github.com/apache/cordova-plugin-media-capture/commit/6422b2b))
- build: add `.npmignore` to remove unneeded files from npm package ([`586f917`](https://github.com/apache/cordova-plugin-media-capture/commit/586f917))
- build: add `.gitattributes` to force LF (instead of possible CRLF on Windows) ([`246ce57`](https://github.com/apache/cordova-plugin-media-capture/commit/246ce57))
- ci(travis): Update Travis CI configuration for new paramedic ([#134](https://github.com/apache/cordova-plugin-media-capture/issues/134)) ([`f59af25`](https://github.com/apache/cordova-plugin-media-capture/commit/f59af25))
- chore(github): Add or update GitHub pull request and issue template ([`e5a982e`](https://github.com/apache/cordova-plugin-media-capture/commit/e5a982e))
- docs: remove JIRA link ([`c3928c8`](https://github.com/apache/cordova-plugin-media-capture/commit/c3928c8))
- ci(travis): also accept terms for android sdk `android-27` ([`59966ac`](https://github.com/apache/cordova-plugin-media-capture/commit/59966ac))
- chore(types): Add CaptureError.CAPTURE_PERMISSION_DENIED to type declaration file [#98](https://github.com/apache/cordova-plugin-media-capture/issues/98) ([`eff0128`](https://github.com/apache/cordova-plugin-media-capture/commit/eff0128), [`5fb4917`](https://github.com/apache/cordova-plugin-media-capture/commit/5fb4917))
### 3.0.2 (Apr 12, 2018)
* [CB-13866](https://issues.apache.org/jira/browse/CB-13866): **iOS** fix Camera opens in portrait orientation on iphones
### 3.0.1 (Dec 27, 2017)
* [CB-13707](https://issues.apache.org/jira/browse/CB-13707) Fix to allow 3.0.0 version install (#88)
* Bump cordova-plugin-file dependency to 6.0.0
### 3.0.0 (Dec 15, 2017)
* [CB-13669](https://issues.apache.org/jira/browse/CB-13669) : Remove deprecated plugins
### 2.0.0 (Nov 06, 2017)
* [CB-13520](https://issues.apache.org/jira/browse/CB-13520) (all): Add 'protective' entry to `cordovaDependencies`
* [CB-13266](https://issues.apache.org/jira/browse/CB-13266) (ios): Remove **iOS** usage descriptions
* [CB-13473](https://issues.apache.org/jira/browse/CB-13473) (CI) Removed **Browser** builds from AppVeyor
* [CB-13294](https://issues.apache.org/jira/browse/CB-13294) Remove `cordova-plugin-compat`
* [CB-13299](https://issues.apache.org/jira/browse/CB-13299) (CI) Fix **Android** builds
* [CB-12895](https://issues.apache.org/jira/browse/CB-12895) added `eslint` and removed `jshint`
* [CB-13028](https://issues.apache.org/jira/browse/CB-13028) (CI) **Browser** builds on Travis and AppVeyor
* [CB-12882](https://issues.apache.org/jira/browse/CB-12882) (ios): adds support for permissions checks for `captureVideo` and `captureImage` methods
* [CB-12847](https://issues.apache.org/jira/browse/CB-12847) added `bugs` entry to `package.json`.
### 1.4.3 (Apr 27, 2017)
* [CB-12622](https://issues.apache.org/jira/browse/CB-12622) Added **Android 6.0** build badges to `README`
* [CB-12685](https://issues.apache.org/jira/browse/CB-12685) added `package.json` to tests folder
### 1.4.2 (Feb 28, 2017)
* [CB-12353](https://issues.apache.org/jira/browse/CB-12353) Corrected merges usage in `plugin.xml`
* [CB-12369](https://issues.apache.org/jira/browse/CB-12369) Add plugin typings from `DefinitelyTyped`
* [CB-12363](https://issues.apache.org/jira/browse/CB-12363) Added build badges for **iOS 9.3** and **iOS 10.0**
* [CB-12230](https://issues.apache.org/jira/browse/CB-12230) Removed **Windows 8.1** build badges
### 1.4.1 (Dec 07, 2016)
* [CB-12224](https://issues.apache.org/jira/browse/CB-12224) Updated version and RELEASENOTES.md for release 1.4.1
* [CB-10701](https://issues.apache.org/jira/browse/CB-10701) [CB-7117](https://issues.apache.org/jira/browse/CB-7117) android: Add manual test to check getting metadata
* [CB-10489](https://issues.apache.org/jira/browse/CB-10489) Fix for MediaFile.getFormatData / audio recording support
* [CB-10488](https://issues.apache.org/jira/browse/CB-10488) Fix for MediaFile.getFormatData
* [CB-11996](https://issues.apache.org/jira/browse/CB-11996) Added a new manual test to capture multiple images
* [CB-11995](https://issues.apache.org/jira/browse/CB-11995) (android) Do not pass a file URI to the camera
* [CB-11917](https://issues.apache.org/jira/browse/CB-11917) - Remove pull request template checklist item: "iCLA has been submitted…"
* [CB-11832](https://issues.apache.org/jira/browse/CB-11832) Incremented plugin version.
### 1.4.0 (Sep 08, 2016)
* Add mandatory **iOS 10** privacy description for microphone
* [CB-11795](https://issues.apache.org/jira/browse/CB-11795) Add 'protective' entry to cordovaDependencies
* [CB-11821](https://issues.apache.org/jira/browse/CB-11821) (ios) Add mandatory **iOS 10** privacy description
* Plugin uses `Android Log class` and not `Cordova LOG class`
* Add badges for paramedic builds on Jenkins
* [CB-11396](https://issues.apache.org/jira/browse/CB-11396) - Audio Media Capture Crashes if app stores file on external storage
* Add pull request template.
* [CB-11212](https://issues.apache.org/jira/browse/CB-11212) **iOS**: Explicitly set the bundle for images
* [CB-10554](https://issues.apache.org/jira/browse/CB-10554) Implementing plugin `save/restore` API for Android
* [CB-11165](https://issues.apache.org/jira/browse/CB-11165) removed peer dependency
* [CB-10996](https://issues.apache.org/jira/browse/CB-10996) Adding front matter to README.md
### 1.3.0 (Apr 15, 2016)
* Replace `PermissionHelper.java` with `cordova-plugin-compat`
* CB-10670, [CB-10994](https://issues.apache.org/jira/browse/CB-10994) **Android**, Marshmallow permissions
* [CB-10720](https://issues.apache.org/jira/browse/CB-10720) Fixing README for display on Cordova website
* [CB-10636](https://issues.apache.org/jira/browse/CB-10636) Add `JSHint` for plugins
* [CB-10690](https://issues.apache.org/jira/browse/CB-10690) **windows**, fix crash when user cancels/closes photo app
### 1.2.0 (Jan 15, 2016)
* [CB-10100](https://issues.apache.org/jira/browse/CB-10100) updated file dependency to not grab new majors
* [CB-8863](https://issues.apache.org/jira/browse/CB-8863) Fix block usage of self
### 1.1.0 (Nov 18, 2015)
* [CB-10035](https://issues.apache.org/jira/browse/CB-10035) Updated `RELEASENOTES` to be newest to oldest
* Fixing contribute link.
* [CB-9249](https://issues.apache.org/jira/browse/CB-9249) Fix **iOS** warnings in Media Capture plugin
* Document the quality property in **Android** quirks
* Add `CaptureVideoOption` for quality
### 1.0.1 (Jun 17, 2015)
* [CB-9128](https://issues.apache.org/jira/browse/CB-9128) cordova-plugin-media-capture documentation translation: cordova-plugin-media-capture
* fix npm md issue
### 1.0.0 (Apr 15, 2015)
* [CB-8746](https://issues.apache.org/jira/browse/CB-8746) bumped version of file dependency
* [CB-8746](https://issues.apache.org/jira/browse/CB-8746) gave plugin major version bump
* [CB-8747](https://issues.apache.org/jira/browse/CB-8747) updated dependency, added peer dependency
* [CB-8683](https://issues.apache.org/jira/browse/CB-8683) changed plugin-id to pacakge-name
* [CB-8653](https://issues.apache.org/jira/browse/CB-8653) properly updated translated docs to use new id
* [CB-8653](https://issues.apache.org/jira/browse/CB-8653) updated translated docs to use new id
* Use TRAVIS_BUILD_DIR, install paramedic by npm
* docs: added Windows to supported platforms
* [CB-8687](https://issues.apache.org/jira/browse/CB-8687) consolidate manifest targets
* [CB-7963](https://issues.apache.org/jira/browse/CB-7963) Adds support for browser platform
* [CB-8653](https://issues.apache.org/jira/browse/CB-8653) Updated Readme
* [CB-8659](https://issues.apache.org/jira/browse/CB-8659): ios: 4.0.x Compatibility: Remove use of initWebView method
* [CB-8571](https://issues.apache.org/jira/browse/CB-8571) Integrate TravisCI
* [CB-8438](https://issues.apache.org/jira/browse/CB-8438) cordova-plugin-media-capture documentation translation: cordova-plugin-media-capture
* [CB-8538](https://issues.apache.org/jira/browse/CB-8538) Added package.json file
### 0.3.6 (Feb 04, 2015)
* [CB-8351](https://issues.apache.org/jira/browse/CB-8351) ios: Use inline copies of deprecated CDV_IsIpad and CDV_IsIphone5
* [CB-8351](https://issues.apache.org/jira/browse/CB-8351) ios: Stop using (newly) deprecated CDVJSON.h
* [CB-8351](https://issues.apache.org/jira/browse/CB-8351) ios: Use argumentForIndex rather than NSArray extension
* [CB-7977](https://issues.apache.org/jira/browse/CB-7977) Mention deviceready in plugin docs
### 0.3.5 (Dec 02, 2014)
* [CB-7597](https://issues.apache.org/jira/browse/CB-7597) - `Localizable.strings` for Media Capture are in the default template, it should be in the plugin
* [CB-7700](https://issues.apache.org/jira/browse/CB-7700) cordova-plugin-media-capture documentation translation: cordova-plugin-media-capture
### 0.3.4 (Oct 03, 2014)
* [CB-7453](https://issues.apache.org/jira/browse/CB-7453) Adds fallback to m4a audio format when mp3 recording fails.
* [CB-7429](https://issues.apache.org/jira/browse/CB-7429) Fixes image capture manual tests on windows
* [CB-7429](https://issues.apache.org/jira/browse/CB-7429) Move windows8 and windows Proxies into one file
* [CB-7429](https://issues.apache.org/jira/browse/CB-7429) Adds media capture support for windows
### 0.3.3 (Sep 17, 2014)
* Renamed test dir, added nested `plugin.xml`
* added documentation for manual tests
* [CB-6959](https://issues.apache.org/jira/browse/CB-6959) Added manual tests
* [CB-6959](https://issues.apache.org/jira/browse/CB-6959) Port capture tests to plugin-test-framework
### 0.3.2 (Aug 06, 2014)
* ubuntu: fix compler warnings
* ubuntu: support qt 5.2
* [CB-6127](https://issues.apache.org/jira/browse/CB-6127) Updated translations for docs
* [CB-6978](https://issues.apache.org/jira/browse/CB-6978) captureImage() function fails in Android
* [CB-6890](https://issues.apache.org/jira/browse/CB-6890): Fix pluginManager access for 4.0.x branch
### 0.3.1 (Jun 05, 2014)
* Added translations to documentation. Github close #14
* Remove deprecated symbols for iOS < 6
* Fixes captureTasks UI URIs
* [CB-6808](https://issues.apache.org/jira/browse/CB-6808) Add license
* [CB-6706](https://issues.apache.org/jira/browse/CB-6706): Relax dependency on file plugin
* [CB-6491](https://issues.apache.org/jira/browse/CB-6491) add CONTRIBUTING.md
### 0.3.0 (Apr 17, 2014)
* [CB-6152](https://issues.apache.org/jira/browse/CB-6152): [ios, android] Make mediafile compatible with file plugin
* [CB-6385](https://issues.apache.org/jira/browse/CB-6385): Specify file plugin dependency version
* [CB-6212](https://issues.apache.org/jira/browse/CB-6212): [iOS] fix warnings compiled under arm64 64-bit
* [CB-6016](https://issues.apache.org/jira/browse/CB-6016) [BlackBerry10] Add audio capture capability
* [Blackberry10] Add rim xml namespaces declaration
* [CB-6422](https://issues.apache.org/jira/browse/CB-6422) [windows8] use cordova/exec/proxy
* [CB-6460](https://issues.apache.org/jira/browse/CB-6460): Update license headers
* Add NOTICE file
### 0.2.8 (Feb 26, 2014)
* [CB-5202](https://issues.apache.org/jira/browse/CB-5202) Fix video capture crash on Android 4.3+
### 0.2.7 (Feb 05, 2014)
* [ubuntu] request audio/camera/microphone permission
* fixed cordova cli add capture plugin not work wp
* [CB-5685](https://issues.apache.org/jira/browse/CB-5685) [BlackBerry10] Add access_shared permission
### 0.2.6 (Jan 02, 2014)
* [CB-5658](https://issues.apache.org/jira/browse/CB-5658) Add doc/index.md for Media Capture plugin
* [CB-5569](https://issues.apache.org/jira/browse/CB-5569) Windows8. MediaFile constructor does not exist
* [CB-5517](https://issues.apache.org/jira/browse/CB-5517) Fix the audio capture IO exception by putting it in a runnable
### 0.2.5 (Dec 4, 2013)
* add ubuntu platform
* Added amazon-fireos platform. Change to use amazon-fireos as a platform if user agent string contains 'cordova-amazon-fireos'
* [CB-5291](https://issues.apache.org/jira/browse/CB-5291) - ios - Media Capture Audio - status bar issues under iOS 7
* [CB-5275](https://issues.apache.org/jira/browse/CB-5275): CaptureImage and CaptureVideo have runnables and CaptureVideo works on 4.2. Still doesn't work for 4.3
### 0.2.4 (Oct 28, 2013)
* [CB-5199](https://issues.apache.org/jira/browse/CB-5199) - ios - Media Capture - UI issues under iOS 7
* [CB-5128](https://issues.apache.org/jira/browse/CB-5128): added repo + issue tag to `plugin.xml` for media capture plugin
* [CB-5010](https://issues.apache.org/jira/browse/CB-5010) Incremented plugin version on dev branch.
### 0.2.3 (Oct 9, 2013)
* [CB-4720](https://issues.apache.org/jira/browse/CB-4720): fixed incorrect feature tag in `plugin.xml` for wp
* [CB-4915](https://issues.apache.org/jira/browse/CB-4915) Incremented plugin version on dev branch.
### 0.2.2 (Sept 25, 2013)
* [CB-4889](https://issues.apache.org/jira/browse/CB-4889) bumping&resetting version
* [windows8] commandProxy was moved
* [windows8] commandProxy was moved
* [CB-4889](https://issues.apache.org/jira/browse/CB-4889)
* [CB-4889](https://issues.apache.org/jira/browse/CB-4889) renaming org.apache.cordova.core.media-capture to org.apache.cordova.media-capture and updating dependency
* Rename CHANGELOG.md -> RELEASENOTES.md
* [CB-4847](https://issues.apache.org/jira/browse/CB-4847) iOS 7 microphone access requires user permission - if denied, CDVCapture, CDVSound does not handle it properly
* [CB-4826](https://issues.apache.org/jira/browse/CB-4826) Fix warning using UITextAlignmentCenter
* [CB-4826](https://issues.apache.org/jira/browse/CB-4826) Fix XCode 5 capture plugin warnings
* [CB-4488](https://issues.apache.org/jira/browse/CB-4488) - added manual capture test
* [CB-4764](https://issues.apache.org/jira/browse/CB-4764) Remove reference to DirectoryManager from Capture.java
* [CB-4763](https://issues.apache.org/jira/browse/CB-4763) Use own version of FileHelper.
* [CB-4752](https://issues.apache.org/jira/browse/CB-4752) Incremented plugin version on dev branch.
{
"name": "cordova-plugin-media-capture",
"version": "3.0.3",
"description": "Cordova Media Capture Plugin",
"types": "./types/index.d.ts",
"cordova": {
"id": "cordova-plugin-media-capture",
"platforms": [
"android",
"ios",
"windows",
"browser"
]
},
"repository": {
"type": "git",
"url": "https://github.com/apache/cordova-plugin-media-capture"
},
"bugs": {
"url": "https://github.com/apache/cordova-plugin-media-capture/issues"
},
"keywords": [
"cordova",
"media",
"capture",
"ecosystem:cordova",
"cordova-android",
"cordova-ios",
"cordova-windows"
],
"scripts": {
"test": "npm run eslint",
"eslint": "eslint www && eslint src && eslint tests"
},
"author": "Apache Software Foundation",
"license": "Apache-2.0",
"engines": {
"cordovaDependencies": {
">=1.4.4": {
"cordova-ios": ">=4.0.0"
},
"2.0.0": {
"cordova-android": ">=6.3.0"
},
"4.0.0": {
"cordova": ">100"
}
}
},
"devDependencies": {
"eslint": "^4.0.0",
"eslint-config-semistandard": "^11.0.0",
"eslint-config-standard": "^10.2.1",
"eslint-plugin-import": "^2.3.0",
"eslint-plugin-node": "^5.0.0",
"eslint-plugin-promise": "^3.5.0",
"eslint-plugin-standard": "^3.0.1"
}
}
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<plugin xmlns="http://apache.org/cordova/ns/plugins/1.0"
xmlns:android="http://schemas.android.com/apk/res/android"
id="cordova-plugin-media-capture"
version="3.0.3">
<name>Capture</name>
<description>Cordova Media Capture Plugin</description>
<license>Apache 2.0</license>
<keywords>cordova,media,capture</keywords>
<repo>https://github.com/apache/cordova-plugin-media-capture</repo>
<issue>https://github.com/apache/cordova-plugin-media-capture/issues</issue>
<engines>
<engine name="cordova-android" version=">=6.3.0" />
</engines>
<dependency id="cordova-plugin-file" version="^6.0.0" />
<js-module src="www/CaptureAudioOptions.js" name="CaptureAudioOptions">
<clobbers target="CaptureAudioOptions" />
</js-module>
<js-module src="www/CaptureImageOptions.js" name="CaptureImageOptions">
<clobbers target="CaptureImageOptions" />
</js-module>
<js-module src="www/CaptureVideoOptions.js" name="CaptureVideoOptions">
<clobbers target="CaptureVideoOptions" />
</js-module>
<js-module src="www/CaptureError.js" name="CaptureError">
<clobbers target="CaptureError" />
</js-module>
<js-module src="www/MediaFileData.js" name="MediaFileData">
<clobbers target="MediaFileData" />
</js-module>
<js-module src="www/MediaFile.js" name="MediaFile">
<clobbers target="MediaFile" />
</js-module>
<js-module src="www/helpers.js" name="helpers">
<runs />
</js-module>
<js-module src="www/capture.js" name="capture">
<clobbers target="navigator.device.capture" />
</js-module>
<!-- android -->
<platform name="android">
<config-file target="res/xml/config.xml" parent="/*">
<feature name="Capture" >
<param name="android-package" value="org.apache.cordova.mediacapture.Capture"/>
</feature>
</config-file>
<config-file target="AndroidManifest.xml" parent="/*">
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.RECORD_VIDEO"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
</config-file>
<source-file src="src/android/Capture.java" target-dir="src/org/apache/cordova/mediacapture" />
<source-file src="src/android/FileHelper.java" target-dir="src/org/apache/cordova/mediacapture" />
<source-file src="src/android/PendingRequests.java" target-dir="src/org/apache/cordova/mediacapture" />
<js-module src="www/android/init.js" name="init">
<runs />
</js-module>
</platform>
<!-- ios -->
<platform name="ios">
<config-file target="config.xml" parent="/*">
<feature name="Capture">
<param name="ios-package" value="CDVCapture" />
</feature>
</config-file>
<header-file src="src/ios/CDVCapture.h" />
<source-file src="src/ios/CDVCapture.m" />
<resource-file src="src/ios/CDVCapture.bundle" />
<framework src="CoreGraphics.framework" />
<framework src="MobileCoreServices.framework" />
</platform>
<!-- windows -->
<platform name="windows">
<config-file target="package.appxmanifest" parent="/Package/Capabilities">
<DeviceCapability Name="microphone" />
<DeviceCapability Name="webcam" />
</config-file>
<js-module src="src/windows/MediaFile.js" name="MediaFile2">
<merges target="MediaFile" />
</js-module>
<js-module src="src/windows/CaptureProxy.js" name="CaptureProxy">
<runs />
</js-module>
</platform>
<!-- browser -->
<platform name="browser">
<!-- this overrides navigator.device.capture namespace with browser-specific implementation -->
<js-module src="src/browser/CaptureProxy.js" name="CaptureProxy">
<runs />
</js-module>
</platform>
</plugin>
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
package org.apache.cordova.mediacapture;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import android.content.ActivityNotFoundException;
import android.os.Build;
import android.os.Bundle;
import org.apache.cordova.file.FileUtils;
import org.apache.cordova.file.LocalFilesystemURL;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.LOG;
import org.apache.cordova.PermissionHelper;
import org.apache.cordova.PluginManager;
import org.apache.cordova.mediacapture.PendingRequests.Request;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.Manifest;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.database.Cursor;
import android.graphics.BitmapFactory;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Environment;
import android.provider.MediaStore;
public class Capture extends CordovaPlugin {
private static final String VIDEO_3GPP = "video/3gpp";
private static final String VIDEO_MP4 = "video/mp4";
private static final String AUDIO_3GPP = "audio/3gpp";
private static final String[] AUDIO_TYPES = new String[] {"audio/3gpp", "audio/aac", "audio/amr", "audio/wav"};
private static final String IMAGE_JPEG = "image/jpeg";
private static final int CAPTURE_AUDIO = 0; // Constant for capture audio
private static final int CAPTURE_IMAGE = 1; // Constant for capture image
private static final int CAPTURE_VIDEO = 2; // Constant for capture video
private static final String LOG_TAG = "Capture";
private static final int CAPTURE_INTERNAL_ERR = 0;
// private static final int CAPTURE_APPLICATION_BUSY = 1;
// private static final int CAPTURE_INVALID_ARGUMENT = 2;
private static final int CAPTURE_NO_MEDIA_FILES = 3;
private static final int CAPTURE_PERMISSION_DENIED = 4;
private static final int CAPTURE_NOT_SUPPORTED = 20;
private boolean cameraPermissionInManifest; // Whether or not the CAMERA permission is declared in AndroidManifest.xml
private final PendingRequests pendingRequests = new PendingRequests();
private int numPics; // Number of pictures before capture activity
private Uri imageUri;
// public void setContext(Context mCtx)
// {
// if (CordovaInterface.class.isInstance(mCtx))
// cordova = (CordovaInterface) mCtx;
// else
// LOG.d(LOG_TAG, "ERROR: You must use the CordovaInterface for this to work correctly. Please implement it in your activity");
// }
@Override
protected void pluginInitialize() {
super.pluginInitialize();
// CB-10670: The CAMERA permission does not need to be requested unless it is declared
// in AndroidManifest.xml. This plugin does not declare it, but others may and so we must
// check the package info to determine if the permission is present.
cameraPermissionInManifest = false;
try {
PackageManager packageManager = this.cordova.getActivity().getPackageManager();
String[] permissionsInPackage = packageManager.getPackageInfo(this.cordova.getActivity().getPackageName(), PackageManager.GET_PERMISSIONS).requestedPermissions;
if (permissionsInPackage != null) {
for (String permission : permissionsInPackage) {
if (permission.equals(Manifest.permission.CAMERA)) {
cameraPermissionInManifest = true;
break;
}
}
}
} catch (NameNotFoundException e) {
// We are requesting the info for our package, so this should
// never be caught
LOG.e(LOG_TAG, "Failed checking for CAMERA permission in manifest", e);
}
}
@Override
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
if (action.equals("getFormatData")) {
JSONObject obj = getFormatData(args.getString(0), args.getString(1));
callbackContext.success(obj);
return true;
}
JSONObject options = args.optJSONObject(0);
if (action.equals("captureAudio")) {
this.captureAudio(pendingRequests.createRequest(CAPTURE_AUDIO, options, callbackContext));
}
else if (action.equals("captureImage")) {
this.captureImage(pendingRequests.createRequest(CAPTURE_IMAGE, options, callbackContext));
}
else if (action.equals("captureVideo")) {
this.captureVideo(pendingRequests.createRequest(CAPTURE_VIDEO, options, callbackContext));
}
else {
return false;
}
return true;
}
/**
* Provides the media data file data depending on it's mime type
*
* @param filePath path to the file
* @param mimeType of the file
* @return a MediaFileData object
*/
private JSONObject getFormatData(String filePath, String mimeType) throws JSONException {
Uri fileUrl = filePath.startsWith("file:") ? Uri.parse(filePath) : Uri.fromFile(new File(filePath));
JSONObject obj = new JSONObject();
// setup defaults
obj.put("height", 0);
obj.put("width", 0);
obj.put("bitrate", 0);
obj.put("duration", 0);
obj.put("codecs", "");
// If the mimeType isn't set the rest will fail
// so let's see if we can determine it.
if (mimeType == null || mimeType.equals("") || "null".equals(mimeType)) {
mimeType = FileHelper.getMimeType(fileUrl, cordova);
}
LOG.d(LOG_TAG, "Mime type = " + mimeType);
if (mimeType.equals(IMAGE_JPEG) || filePath.endsWith(".jpg")) {
obj = getImageData(fileUrl, obj);
}
else if (Arrays.asList(AUDIO_TYPES).contains(mimeType)) {
obj = getAudioVideoData(filePath, obj, false);
}
else if (mimeType.equals(VIDEO_3GPP) || mimeType.equals(VIDEO_MP4)) {
obj = getAudioVideoData(filePath, obj, true);
}
return obj;
}
/**
* Get the Image specific attributes
*
* @param filePath path to the file
* @param obj represents the Media File Data
* @return a JSONObject that represents the Media File Data
* @throws JSONException
*/
private JSONObject getImageData(Uri fileUrl, JSONObject obj) throws JSONException {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(fileUrl.getPath(), options);
obj.put("height", options.outHeight);
obj.put("width", options.outWidth);
return obj;
}
/**
* Get the Image specific attributes
*
* @param filePath path to the file
* @param obj represents the Media File Data
* @param video if true get video attributes as well
* @return a JSONObject that represents the Media File Data
* @throws JSONException
*/
private JSONObject getAudioVideoData(String filePath, JSONObject obj, boolean video) throws JSONException {
MediaPlayer player = new MediaPlayer();
try {
player.setDataSource(filePath);
player.prepare();
obj.put("duration", player.getDuration() / 1000);
if (video) {
obj.put("height", player.getVideoHeight());
obj.put("width", player.getVideoWidth());
}
} catch (IOException e) {
LOG.d(LOG_TAG, "Error: loading video file");
}
return obj;
}
/**
* Sets up an intent to capture audio. Result handled by onActivityResult()
*/
private void captureAudio(Request req) {
if (!PermissionHelper.hasPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)) {
PermissionHelper.requestPermission(this, req.requestCode, Manifest.permission.READ_EXTERNAL_STORAGE);
} else {
try {
Intent intent = new Intent(android.provider.MediaStore.Audio.Media.RECORD_SOUND_ACTION);
this.cordova.startActivityForResult((CordovaPlugin) this, intent, req.requestCode);
} catch (ActivityNotFoundException ex) {
pendingRequests.resolveWithFailure(req, createErrorObject(CAPTURE_NOT_SUPPORTED, "No Activity found to handle Audio Capture."));
}
}
}
private String getTempDirectoryPath() {
File cache = null;
// Use internal storage
cache = cordova.getActivity().getCacheDir();
// Create the cache directory if it doesn't exist
cache.mkdirs();
return cache.getAbsolutePath();
}
/**
* Sets up an intent to capture images. Result handled by onActivityResult()
*/
private void captureImage(Request req) {
boolean needExternalStoragePermission =
!PermissionHelper.hasPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE);
boolean needCameraPermission = cameraPermissionInManifest &&
!PermissionHelper.hasPermission(this, Manifest.permission.CAMERA);
if (needExternalStoragePermission || needCameraPermission) {
if (needExternalStoragePermission && needCameraPermission) {
PermissionHelper.requestPermissions(this, req.requestCode, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA});
} else if (needExternalStoragePermission) {
PermissionHelper.requestPermission(this, req.requestCode, Manifest.permission.WRITE_EXTERNAL_STORAGE);
} else {
PermissionHelper.requestPermission(this, req.requestCode, Manifest.permission.CAMERA);
}
} else {
// Save the number of images currently on disk for later
this.numPics = queryImgDB(whichContentStore()).getCount();
Intent intent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE);
ContentResolver contentResolver = this.cordova.getActivity().getContentResolver();
ContentValues cv = new ContentValues();
cv.put(MediaStore.Images.Media.MIME_TYPE, IMAGE_JPEG);
imageUri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, cv);
LOG.d(LOG_TAG, "Taking a picture and saving to: " + imageUri.toString());
intent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, imageUri);
this.cordova.startActivityForResult((CordovaPlugin) this, intent, req.requestCode);
}
}
private static void createWritableFile(File file) throws IOException {
file.createNewFile();
file.setWritable(true, false);
}
/**
* Sets up an intent to capture video. Result handled by onActivityResult()
*/
private void captureVideo(Request req) {
if(cameraPermissionInManifest && !PermissionHelper.hasPermission(this, Manifest.permission.CAMERA)) {
PermissionHelper.requestPermission(this, req.requestCode, Manifest.permission.CAMERA);
} else {
Intent intent = new Intent(android.provider.MediaStore.ACTION_VIDEO_CAPTURE);
if(Build.VERSION.SDK_INT > 7){
intent.putExtra("android.intent.extra.durationLimit", req.duration);
intent.putExtra("android.intent.extra.videoQuality", req.quality);
}
this.cordova.startActivityForResult((CordovaPlugin) this, intent, req.requestCode);
}
}
/**
* Called when the video view exits.
*
* @param requestCode The request code originally supplied to startActivityForResult(),
* allowing you to identify who this result came from.
* @param resultCode The integer result code returned by the child activity through its setResult().
* @param intent An Intent, which can return result data to the caller (various data can be attached to Intent "extras").
* @throws JSONException
*/
public void onActivityResult(int requestCode, int resultCode, final Intent intent) {
final Request req = pendingRequests.get(requestCode);
// Result received okay
if (resultCode == Activity.RESULT_OK) {
Runnable processActivityResult = new Runnable() {
@Override
public void run() {
switch(req.action) {
case CAPTURE_AUDIO:
onAudioActivityResult(req, intent);
break;
case CAPTURE_IMAGE:
onImageActivityResult(req);
break;
case CAPTURE_VIDEO:
onVideoActivityResult(req, intent);
break;
}
}
};
this.cordova.getThreadPool().execute(processActivityResult);
}
// If canceled
else if (resultCode == Activity.RESULT_CANCELED) {
// If we have partial results send them back to the user
if (req.results.length() > 0) {
pendingRequests.resolveWithSuccess(req);
}
// user canceled the action
else {
pendingRequests.resolveWithFailure(req, createErrorObject(CAPTURE_NO_MEDIA_FILES, "Canceled."));
}
}
// If something else
else {
// If we have partial results send them back to the user
if (req.results.length() > 0) {
pendingRequests.resolveWithSuccess(req);
}
// something bad happened
else {
pendingRequests.resolveWithFailure(req, createErrorObject(CAPTURE_NO_MEDIA_FILES, "Did not complete!"));
}
}
}
public void onAudioActivityResult(Request req, Intent intent) {
// Get the uri of the audio clip
Uri data = intent.getData();
// create a file object from the uri
req.results.put(createMediaFile(data));
if (req.results.length() >= req.limit) {
// Send Uri back to JavaScript for listening to audio
pendingRequests.resolveWithSuccess(req);
} else {
// still need to capture more audio clips
captureAudio(req);
}
}
public void onImageActivityResult(Request req) {
// Add image to results
req.results.put(createMediaFile(imageUri));
checkForDuplicateImage();
if (req.results.length() >= req.limit) {
// Send Uri back to JavaScript for viewing image
pendingRequests.resolveWithSuccess(req);
} else {
// still need to capture more images
captureImage(req);
}
}
public void onVideoActivityResult(Request req, Intent intent) {
Uri data = null;
if (intent != null){
// Get the uri of the video clip
data = intent.getData();
}
if( data == null){
File movie = new File(getTempDirectoryPath(), "Capture.avi");
data = Uri.fromFile(movie);
}
// create a file object from the uri
if(data == null) {
pendingRequests.resolveWithFailure(req, createErrorObject(CAPTURE_NO_MEDIA_FILES, "Error: data is null"));
}
else {
req.results.put(createMediaFile(data));
if (req.results.length() >= req.limit) {
// Send Uri back to JavaScript for viewing video
pendingRequests.resolveWithSuccess(req);
} else {
// still need to capture more video clips
captureVideo(req);
}
}
}
/**
* Creates a JSONObject that represents a File from the Uri
*
* @param data the Uri of the audio/image/video
* @return a JSONObject that represents a File
* @throws IOException
*/
private JSONObject createMediaFile(Uri data) {
File fp = webView.getResourceApi().mapUriToFile(data);
JSONObject obj = new JSONObject();
Class webViewClass = webView.getClass();
PluginManager pm = null;
try {
Method gpm = webViewClass.getMethod("getPluginManager");
pm = (PluginManager) gpm.invoke(webView);
} catch (NoSuchMethodException e) {
} catch (IllegalAccessException e) {
} catch (InvocationTargetException e) {
}
if (pm == null) {
try {
Field pmf = webViewClass.getField("pluginManager");
pm = (PluginManager)pmf.get(webView);
} catch (NoSuchFieldException e) {
} catch (IllegalAccessException e) {
}
}
FileUtils filePlugin = (FileUtils) pm.getPlugin("File");
LocalFilesystemURL url = filePlugin.filesystemURLforLocalPath(fp.getAbsolutePath());
try {
// File properties
obj.put("name", fp.getName());
obj.put("fullPath", Uri.fromFile(fp));
if (url != null) {
obj.put("localURL", url.toString());
}
// Because of an issue with MimeTypeMap.getMimeTypeFromExtension() all .3gpp files
// are reported as video/3gpp. I'm doing this hacky check of the URI to see if it
// is stored in the audio or video content store.
if (fp.getAbsoluteFile().toString().endsWith(".3gp") || fp.getAbsoluteFile().toString().endsWith(".3gpp")) {
if (data.toString().contains("/audio/")) {
obj.put("type", AUDIO_3GPP);
} else {
obj.put("type", VIDEO_3GPP);
}
} else {
obj.put("type", FileHelper.getMimeType(Uri.fromFile(fp), cordova));
}
obj.put("lastModifiedDate", fp.lastModified());
obj.put("size", fp.length());
} catch (JSONException e) {
// this will never happen
e.printStackTrace();
}
return obj;
}
private JSONObject createErrorObject(int code, String message) {
JSONObject obj = new JSONObject();
try {
obj.put("code", code);
obj.put("message", message);
} catch (JSONException e) {
// This will never happen
}
return obj;
}
/**
* Creates a cursor that can be used to determine how many images we have.
*
* @return a cursor
*/
private Cursor queryImgDB(Uri contentStore) {
return this.cordova.getActivity().getContentResolver().query(
contentStore,
new String[] { MediaStore.Images.Media._ID },
null,
null,
null);
}
/**
* Used to find out if we are in a situation where the Camera Intent adds to images
* to the content store.
*/
private void checkForDuplicateImage() {
Uri contentStore = whichContentStore();
Cursor cursor = queryImgDB(contentStore);
int currentNumOfImages = cursor.getCount();
// delete the duplicate file if the difference is 2
if ((currentNumOfImages - numPics) == 2) {
cursor.moveToLast();
int id = Integer.valueOf(cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media._ID))) - 1;
Uri uri = Uri.parse(contentStore + "/" + id);
this.cordova.getActivity().getContentResolver().delete(uri, null, null);
}
}
/**
* Determine if we are storing the images in internal or external storage
* @return Uri
*/
private Uri whichContentStore() {
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
return android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
} else {
return android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI;
}
}
private void executeRequest(Request req) {
switch (req.action) {
case CAPTURE_AUDIO:
this.captureAudio(req);
break;
case CAPTURE_IMAGE:
this.captureImage(req);
break;
case CAPTURE_VIDEO:
this.captureVideo(req);
break;
}
}
public void onRequestPermissionResult(int requestCode, String[] permissions,
int[] grantResults) throws JSONException {
Request req = pendingRequests.get(requestCode);
if (req != null) {
boolean success = true;
for(int r:grantResults) {
if (r == PackageManager.PERMISSION_DENIED) {
success = false;
break;
}
}
if (success) {
executeRequest(req);
} else {
pendingRequests.resolveWithFailure(req, createErrorObject(CAPTURE_PERMISSION_DENIED, "Permission denied."));
}
}
}
public Bundle onSaveInstanceState() {
return pendingRequests.toBundle();
}
public void onRestoreStateForActivityResult(Bundle state, CallbackContext callbackContext) {
pendingRequests.setLastSavedState(state, callbackContext);
}
}
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
package org.apache.cordova.mediacapture;
import android.net.Uri;
import android.webkit.MimeTypeMap;
import org.apache.cordova.CordovaInterface;
import java.util.Locale;
// TODO: Replace with CordovaResourceApi.getMimeType() post 3.1.
public class FileHelper {
public static String getMimeTypeForExtension(String path) {
String extension = path;
int lastDot = extension.lastIndexOf('.');
if (lastDot != -1) {
extension = extension.substring(lastDot + 1);
}
// Convert the URI string to lower case to ensure compatibility with MimeTypeMap (see CB-2185).
extension = extension.toLowerCase(Locale.getDefault());
if (extension.equals("3ga")) {
return "audio/3gpp";
}
return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
}
/**
* Returns the mime type of the data specified by the given URI string.
*
* @param uriString the URI string of the data
* @return the mime type of the specified data
*/
public static String getMimeType(Uri uri, CordovaInterface cordova) {
String mimeType = null;
if ("content".equals(uri.getScheme())) {
mimeType = cordova.getActivity().getContentResolver().getType(uri);
} else {
mimeType = getMimeTypeForExtension(uri.getPath());
}
return mimeType;
}
}
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
package org.apache.cordova.mediacapture;
import android.os.Bundle;
import android.util.SparseArray;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.LOG;
import org.apache.cordova.PluginResult;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
/**
* Holds the pending javascript requests for the plugin
*/
public class PendingRequests {
private static final String LOG_TAG = "PendingCaptureRequests";
private static final String CURRENT_ID_KEY = "currentReqId";
private static final String REQUEST_KEY_PREFIX = "request_";
private int currentReqId = 0;
private SparseArray<Request> requests = new SparseArray<Request>();
private Bundle lastSavedState;
private CallbackContext resumeContext;
/**
* Creates a request and adds it to the array of pending requests. Each created request gets a
* unique result code for use with startActivityForResult() and requestPermission()
* @param action The action this request corresponds to (capture image, capture audio, etc.)
* @param options The options for this request passed from the javascript
* @param callbackContext The CallbackContext to return the result to
* @return The newly created Request object with a unique result code
* @throws JSONException
*/
public synchronized Request createRequest(int action, JSONObject options, CallbackContext callbackContext) throws JSONException {
Request req = new Request(action, options, callbackContext);
requests.put(req.requestCode, req);
return req;
}
/**
* Gets the request corresponding to this request code
* @param requestCode The request code for the desired request
* @return The request corresponding to the given request code or null if such a
* request is not found
*/
public synchronized Request get(int requestCode) {
// Check to see if this request was saved
if (lastSavedState != null && lastSavedState.containsKey(REQUEST_KEY_PREFIX + requestCode)) {
Request r = new Request(lastSavedState.getBundle(REQUEST_KEY_PREFIX + requestCode), this.resumeContext, requestCode);
requests.put(requestCode, r);
// Only one of the saved requests will get restored, because that's all cordova-android
// supports. Having more than one is an extremely unlikely scenario anyway
this.lastSavedState = null;
this.resumeContext = null;
return r;
}
return requests.get(requestCode);
}
/**
* Removes the request from the array of pending requests and sends an error plugin result
* to the CallbackContext that contains the given error object
* @param req The request to be resolved
* @param error The error to be returned to the CallbackContext
*/
public synchronized void resolveWithFailure(Request req, JSONObject error) {
req.callbackContext.error(error);
requests.remove(req.requestCode);
}
/**
* Removes the request from the array of pending requests and sends a successful plugin result
* to the CallbackContext that contains the result of the request
* @param req The request to be resolved
*/
public synchronized void resolveWithSuccess(Request req) {
req.callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, req.results));
requests.remove(req.requestCode);
}
/**
* Each request gets a unique ID that represents its request code when calls are made to
* Activities and for permission requests
* @return A unique request code
*/
private synchronized int incrementCurrentReqId() {
return currentReqId ++;
}
/**
* Restore state saved by calling toBundle along with a callbackContext to be used in
* delivering the results of a pending callback
*
* @param lastSavedState The bundle received from toBundle()
* @param resumeContext The callbackContext to return results to
*/
public synchronized void setLastSavedState(Bundle lastSavedState, CallbackContext resumeContext) {
this.lastSavedState = lastSavedState;
this.resumeContext = resumeContext;
this.currentReqId = lastSavedState.getInt(CURRENT_ID_KEY);
}
/**
* Save the current pending requests to a bundle for saving when the Activity gets destroyed.
*
* @return A Bundle that can be used to restore state using setLastSavedState()
*/
public synchronized Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putInt(CURRENT_ID_KEY, currentReqId);
for (int i = 0; i < requests.size(); i++) {
Request r = requests.valueAt(i);
int requestCode = requests.keyAt(i);
bundle.putBundle(REQUEST_KEY_PREFIX + requestCode, r.toBundle());
}
if (requests.size() > 1) {
// This scenario is hopefully very unlikely because there isn't much that can be
// done about it. Should only occur if an external Activity is launched while
// there is a pending permission request and the device is on low memory
LOG.w(LOG_TAG, "More than one media capture request pending on Activity destruction. Some requests will be dropped!");
}
return bundle;
}
/**
* Holds the options and CallbackContext for a capture request made to the plugin.
*/
public class Request {
// Keys for use in saving requests to a bundle
private static final String ACTION_KEY = "action";
private static final String LIMIT_KEY = "limit";
private static final String DURATION_KEY = "duration";
private static final String QUALITY_KEY = "quality";
private static final String RESULTS_KEY = "results";
// Unique int used to identify this request in any Android Permission or Activity callbacks
public int requestCode;
// The action that this request is performing
public int action;
// The number of pics/vids/audio clips to take (CAPTURE_IMAGE, CAPTURE_VIDEO, CAPTURE_AUDIO)
public long limit = 1;
// Optional max duration of recording in seconds (CAPTURE_VIDEO only)
public int duration = 0;
// Quality level for video capture 0 low, 1 high (CAPTURE_VIDEO only)
public int quality = 1;
// The array of results to be returned to the javascript callback on success
public JSONArray results = new JSONArray();
// The callback context for this plugin request
private CallbackContext callbackContext;
private Request(int action, JSONObject options, CallbackContext callbackContext) throws JSONException {
this.callbackContext = callbackContext;
this.action = action;
if (options != null) {
this.limit = options.optLong("limit", 1);
this.duration = options.optInt("duration", 0);
this.quality = options.optInt("quality", 1);
}
this.requestCode = incrementCurrentReqId();
}
private Request(Bundle bundle, CallbackContext callbackContext, int requestCode) {
this.callbackContext = callbackContext;
this.requestCode = requestCode;
this.action = bundle.getInt(ACTION_KEY);
this.limit = bundle.getLong(LIMIT_KEY);
this.duration = bundle.getInt(DURATION_KEY);
this.quality = bundle.getInt(QUALITY_KEY);
try {
this.results = new JSONArray(bundle.getString(RESULTS_KEY));
} catch(JSONException e) {
// This should never be caught
LOG.e(LOG_TAG, "Error parsing results for request from saved bundle", e);
}
}
private Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putInt(ACTION_KEY, this.action);
bundle.putLong(LIMIT_KEY, this.limit);
bundle.putInt(DURATION_KEY, this.duration);
bundle.putInt(QUALITY_KEY, this.quality);
bundle.putString(RESULTS_KEY, this.results.toString());
return bundle;
}
}
}
/*
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
/* global require, module */
var MediaFile = require('cordova-plugin-media-capture.MediaFile');
var MediaFileData = require('cordova-plugin-media-capture.MediaFileData');
var CaptureError = require('cordova-plugin-media-capture.CaptureError');
/**
* Helper function that converts data URI to Blob
* @param {String} dataURI Data URI to convert
* @return {Blob} Blob, covnerted from DataURI String
*/
function dataURItoBlob (dataURI) {
// convert base64 to raw binary data held in a string
// doesn't handle URLEncoded DataURIs
var byteString = atob(dataURI.split(',')[1]); // eslint-disable-line no-undef
// separate out the mime component
var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
// write the bytes of the string to an ArrayBuffer
var ab = new ArrayBuffer(byteString.length);
var ia = new Uint8Array(ab);
for (var i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
// write the ArrayBuffer to a blob, and you're done
return new Blob([ab], { type: mimeString }); // eslint-disable-line no-undef
}
/**
* Creates basic camera UI with preview 'video' element and 'Cancel' button
* Capture starts, when you clicking on preview.
*/
function CameraUI () {
// Root element for preview
var container = document.createElement('div');
container.style.cssText = 'left: 0px; top: 0px; width: 100%; height: 100%; position: fixed; z-index:9999;' +
'padding: 40px; background-color: rgba(0,0,0,0.75);' +
'text-align:center; visibility: hidden';
// Set up root element contetnts
container.innerHTML =
'<div id="captureHint" style="height:100%; position:relative; display:inline-flex; align-content:flex-start;">' +
'<h2 style="position: absolute; width: 100%; background-color: rgba(255,255,255,0.25); margin: 0">' +
'Click on preview to capture image. Click outside of preview to cancel.</h1>' +
'<video id="capturePreview" style="height: 100%"></video>' +
'</div>';
// Add container element to DOM but do not display it since visibility == hidden
document.body.appendChild(container);
// Create fullscreen preview
var preview = document.getElementById('capturePreview');
preview.autoplay = true;
// We'll show preview only when video element content
// is fully loaded to avoid glitches
preview.onplay = function () {
container.style.visibility = 'visible';
};
this.container = container;
this.preview = preview;
}
/**
* Displays capture preview
* @param {Number} count Number of images to take
* @param {Function} successCB Success callback, that accepts data URL of captured image
* @param {Function} errorCB Error callback
*/
CameraUI.prototype.startPreview = function (count, successCB, errorCB) {
var that = this;
this.preview.onclick = function (e) {
// proceed with capture here
// We don't need to propagate click event to parent elements.
// Otherwise click on vieo element will trigger click event handler
// for preview root element and cause preview cancellation
e.stopPropagation();
// Create canvas element, put video frame on it
// and save its contant as Data URL
var canvas = document.createElement('canvas');
canvas.width = this.videoWidth;
canvas.height = this.videoHeight;
canvas.getContext('2d').drawImage(that.preview, 0, 0);
successCB(canvas.toDataURL('image/jpeg'));
};
this.container.onclick = function () {
// Cancel capture here
errorCB(new CaptureError(CaptureError.CAPTURE_NO_MEDIA_FILES));
};
navigator.getUserMedia({video: true}, function (previewStream) {
// Save video stream to be able to stop it later
that._previewStream = previewStream;
that.preview.src = URL.createObjectURL(previewStream); // eslint-disable-line no-undef
// We don't need to set visibility = true for preview element
// since this will be done automatically in onplay event handler
}, function (/* err */) {
errorCB(new CaptureError(CaptureError.CAPTURE_INTERNAL_ERR));
});
};
/**
* Destroys camera preview, removes all elements created
*/
CameraUI.prototype.destroyPreview = function () {
this.preview.pause();
this.preview.src = null;
this._previewStream.stop();
this._previewStream = null;
if (this.container) {
document.body.removeChild(this.container);
}
};
module.exports = {
captureAudio: function (successCallback, errorCallback) {
if (errorCallback) {
errorCallback(new CaptureError(CaptureError.CAPTURE_NOT_SUPPORTED));
}
},
captureVideo: function (successCallback, errorCallback) {
if (errorCallback) {
errorCallback(new CaptureError(CaptureError.CAPTURE_NOT_SUPPORTED));
}
},
captureImage: function (successCallback, errorCallback, args) {
var fail = function (code) {
if (errorCallback) {
errorCallback(new CaptureError(code || CaptureError.CAPTURE_INTERNAL_ERR));
}
};
var options = args[0];
var limit = options.limit || 1;
if (typeof limit !== 'number' || limit < 1) {
fail(CaptureError.CAPTURE_INVALID_ARGUMENT);
return;
}
// Counter for already taken images
var imagesTaken = 0;
navigator.getUserMedia = navigator.getUserMedia ||
navigator.webkitGetUserMedia ||
navigator.mozGetUserMedia ||
navigator.msGetUserMedia;
if (!navigator.getUserMedia) {
fail(CaptureError.CAPTURE_NOT_SUPPORTED);
return;
}
var ui = new CameraUI();
ui.startPreview(limit, function (data) {
// Check if we're done with capture. If so, then destroy UI
if (++imagesTaken >= limit) {
ui.destroyPreview();
}
// Array of resultant MediaFiles
var mediaFiles = [];
// save data to file here
window.requestFileSystem(window.TEMPORARY, data.length * limit, function (fileSystem) {
// If we need to capture multiple files, then append counter to filename
var fileName = limit <= 1 ? 'image.jpg' : 'image' + imagesTaken + '.jpg';
fileSystem.root.getFile(fileName, {create: true}, function (file) {
file.createWriter(function (writer) {
writer.onwriteend = function () {
file.getMetadata(function (meta) {
mediaFiles.push(new MediaFile(file.name, file.toURL(), 'image/jpeg', meta.modificationTime, meta.size));
// Check if we're done with capture. If so, then call a successCallback
if (imagesTaken >= limit) {
successCallback(mediaFiles);
}
}, fail);
};
writer.onerror = fail;
// Since success callback for start preview returns
// a base64 encoded string, we need to convert it to blob first
writer.write(dataURItoBlob(data));
});
}, fail);
}, fail);
}, function (err) {
ui.destroyPreview();
fail(err.code);
});
},
getFormatData: function (successCallback, errorCallback, args) {
var img = document.createElement('img');
img.src = args[0];
img.onload = function () {
if (successCallback) {
successCallback(new MediaFileData(null, 0, img.height, img.width, 0));
}
};
}
};
require('cordova/exec/proxy').add('Capture', module.exports);
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
// controller title for Videos
"Videos title" = "Videos";
// accessibility label for recording button
"toggle audio recording" = "starten/beenden der Tonaufnahme";
// notification spoken by VoiceOver when timed recording finishes
"timed recording complete" = "programmierte Aufnahme beendet";
// accessibility hint for display of recorded elapsed time
"recorded time in minutes and seconds" = "aufgenommene Zeit in Minuten und Sekunden";
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
// controller title for Videos
"Videos title" = "Videos";
// accessibility label for recording button
"toggle audio recording" = "toggle audio recording";
// notification spoken by VoiceOver when timed recording finishes
"timed recording complete" = "timed recording complete";
// accessibility hint for display of recorded elapsed time
"recorded time in minutes and seconds" = "recorded time in minutes and seconds";
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
// controller title for Videos
"Videos title" = "Videos";
// accessibility label for recording button
"toggle audio recording" = "grabación de audio cambiar";
// notification spoken by VoiceOver when timed recording finishes
"timed recording complete" = "tiempo de grabación completo";
// accessibility hint for display of recorded elapsed time
"recorded time in minutes and seconds" = "tiempo registrado en minutos y segundos";
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
// controller title for Videos
"Videos title" = "Videor";
// accessibility label for recording button
"toggle audio recording" = "börja/avsluta inspelning";
// notification spoken by VoiceOver when timed recording finishes
"timed recording complete" = "inspelning har avslutad";
// accessibility hint for display of recorded elapsed time
"recorded time in minutes and seconds" = "inspelad tid in minuter och sekund";
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
#import <Foundation/Foundation.h>
#import <MobileCoreServices/MobileCoreServices.h>
#import <AVFoundation/AVFoundation.h>
#import <Cordova/CDVPlugin.h>
#import "CDVFile.h"
enum CDVCaptureError {
CAPTURE_INTERNAL_ERR = 0,
CAPTURE_APPLICATION_BUSY = 1,
CAPTURE_INVALID_ARGUMENT = 2,
CAPTURE_NO_MEDIA_FILES = 3,
CAPTURE_PERMISSION_DENIED = 4,
CAPTURE_NOT_SUPPORTED = 20
};
typedef NSUInteger CDVCaptureError;
@interface CDVImagePicker : UIImagePickerController
{
NSString* callbackid;
NSInteger quality;
NSString* mimeType;
}
@property (assign) NSInteger quality;
@property (copy) NSString* callbackId;
@property (copy) NSString* mimeType;
@end
@interface CDVCapture : CDVPlugin <UIImagePickerControllerDelegate, UINavigationControllerDelegate>
{
CDVImagePicker* pickerController;
BOOL inUse;
}
@property BOOL inUse;
- (void)captureAudio:(CDVInvokedUrlCommand*)command;
- (void)captureImage:(CDVInvokedUrlCommand*)command;
- (CDVPluginResult*)processImage:(UIImage*)image type:(NSString*)mimeType forCallbackId:(NSString*)callbackId;
- (void)captureVideo:(CDVInvokedUrlCommand*)command;
- (CDVPluginResult*)processVideo:(NSString*)moviePath forCallbackId:(NSString*)callbackId;
- (void)getMediaModes:(CDVInvokedUrlCommand*)command;
- (void)getFormatData:(CDVInvokedUrlCommand*)command;
- (NSDictionary*)getMediaDictionaryFromPath:(NSString*)fullPath ofType:(NSString*)type;
- (void)imagePickerController:(UIImagePickerController*)picker didFinishPickingMediaWithInfo:(NSDictionary*)info;
- (void)imagePickerController:(UIImagePickerController*)picker didFinishPickingImage:(UIImage*)image editingInfo:(NSDictionary*)editingInfo;
- (void)imagePickerControllerDidCancel:(UIImagePickerController*)picker;
@end
@interface CDVAudioNavigationController : UINavigationController
@end
/* AudioRecorderViewController is used to create a simple view for audio recording.
* It is created from [Capture captureAudio]. It creates a very simple interface for
* recording by presenting just a record/stop button and a Done button to close the view.
* The recording time is displayed and recording for a specified duration is supported. When duration
* is specified there is no UI to the user - recording just stops when the specified
* duration is reached. The UI has been minimized to avoid localization.
*/
@interface CDVAudioRecorderViewController : UIViewController <AVAudioRecorderDelegate>
{
CDVCaptureError errorCode;
NSString* callbackId;
NSNumber* duration;
CDVCapture* captureCommand;
UIBarButtonItem* doneButton;
UIView* recordingView;
UIButton* recordButton;
UIImage* recordImage;
UIImage* stopRecordImage;
UILabel* timerLabel;
AVAudioRecorder* avRecorder;
AVAudioSession* avSession;
CDVPluginResult* pluginResult;
NSTimer* timer;
BOOL isTimed;
}
@property (nonatomic) CDVCaptureError errorCode;
@property (nonatomic, copy) NSString* callbackId;
@property (nonatomic, copy) NSNumber* duration;
@property (nonatomic, strong) CDVCapture* captureCommand;
@property (nonatomic, strong) UIBarButtonItem* doneButton;
@property (nonatomic, strong) UIView* recordingView;
@property (nonatomic, strong) UIButton* recordButton;
@property (nonatomic, strong) UIImage* recordImage;
@property (nonatomic, strong) UIImage* stopRecordImage;
@property (nonatomic, strong) UILabel* timerLabel;
@property (nonatomic, strong) AVAudioRecorder* avRecorder;
@property (nonatomic, strong) AVAudioSession* avSession;
@property (nonatomic, strong) CDVPluginResult* pluginResult;
@property (nonatomic, strong) NSTimer* timer;
@property (nonatomic) BOOL isTimed;
- (id)initWithCommand:(CDVPlugin*)theCommand duration:(NSNumber*)theDuration callbackId:(NSString*)theCallbackId;
- (void)processButton:(id)sender;
- (void)stopRecordingCleanup;
- (void)dismissAudioView:(id)sender;
- (NSString*)formatTime:(int)interval;
- (void)updateTime;
@end
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
#import "CDVCapture.h"
#import "CDVFile.h"
#import <Cordova/CDVAvailability.h>
#define kW3CMediaFormatHeight @"height"
#define kW3CMediaFormatWidth @"width"
#define kW3CMediaFormatCodecs @"codecs"
#define kW3CMediaFormatBitrate @"bitrate"
#define kW3CMediaFormatDuration @"duration"
#define kW3CMediaModeType @"type"
@implementation NSBundle (PluginExtensions)
+ (NSBundle*) pluginBundle:(CDVPlugin*)plugin {
NSBundle* bundle = [NSBundle bundleWithPath: [[NSBundle mainBundle] pathForResource:NSStringFromClass([plugin class]) ofType: @"bundle"]];
return bundle;
}
@end
#define PluginLocalizedString(plugin, key, comment) [[NSBundle pluginBundle:(plugin)] localizedStringForKey:(key) value:nil table:nil]
@implementation CDVImagePicker
@synthesize quality;
@synthesize callbackId;
@synthesize mimeType;
- (uint64_t)accessibilityTraits
{
NSString* systemVersion = [[UIDevice currentDevice] systemVersion];
if (([systemVersion compare:@"4.0" options:NSNumericSearch] != NSOrderedAscending)) { // this means system version is not less than 4.0
return UIAccessibilityTraitStartsMediaSession;
}
return UIAccessibilityTraitNone;
}
- (BOOL)prefersStatusBarHidden {
return YES;
}
- (UIViewController*)childViewControllerForStatusBarHidden {
return nil;
}
- (void)viewWillAppear:(BOOL)animated {
SEL sel = NSSelectorFromString(@"setNeedsStatusBarAppearanceUpdate");
if ([self respondsToSelector:sel]) {
[self performSelector:sel withObject:nil afterDelay:0];
}
[super viewWillAppear:animated];
}
@end
@implementation CDVCapture
@synthesize inUse;
- (void)pluginInitialize
{
self.inUse = NO;
}
- (void)captureAudio:(CDVInvokedUrlCommand*)command
{
NSString* callbackId = command.callbackId;
NSDictionary* options = [command argumentAtIndex:0];
if ([options isKindOfClass:[NSNull class]]) {
options = [NSDictionary dictionary];
}
NSNumber* duration = [options objectForKey:@"duration"];
// the default value of duration is 0 so use nil (no duration) if default value
if (duration) {
duration = [duration doubleValue] == 0 ? nil : duration;
}
CDVPluginResult* result = nil;
if (NSClassFromString(@"AVAudioRecorder") == nil) {
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageToErrorObject:CAPTURE_NOT_SUPPORTED];
} else if (self.inUse == YES) {
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageToErrorObject:CAPTURE_APPLICATION_BUSY];
} else {
// all the work occurs here
CDVAudioRecorderViewController* audioViewController = [[CDVAudioRecorderViewController alloc] initWithCommand:self duration:duration callbackId:callbackId];
// Now create a nav controller and display the view...
CDVAudioNavigationController* navController = [[CDVAudioNavigationController alloc] initWithRootViewController:audioViewController];
self.inUse = YES;
[self.viewController presentViewController:navController animated:YES completion:nil];
}
if (result) {
[self.commandDelegate sendPluginResult:result callbackId:callbackId];
}
}
- (void)captureImage:(CDVInvokedUrlCommand*)command
{
NSString* callbackId = command.callbackId;
NSDictionary* options = [command argumentAtIndex:0];
if ([options isKindOfClass:[NSNull class]]) {
options = [NSDictionary dictionary];
}
// options could contain limit and mode neither of which are supported at this time
// taking more than one picture (limit) is only supported if provide own controls via cameraOverlayView property
// can support mode in OS
if (![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) {
NSLog(@"Capture.imageCapture: camera not available.");
CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageToErrorObject:CAPTURE_NOT_SUPPORTED];
[self.commandDelegate sendPluginResult:result callbackId:callbackId];
} else {
if (pickerController == nil) {
pickerController = [[CDVImagePicker alloc] init];
}
[self showAlertIfAccessProhibited];
pickerController.delegate = self;
pickerController.sourceType = UIImagePickerControllerSourceTypeCamera;
pickerController.allowsEditing = NO;
if ([pickerController respondsToSelector:@selector(mediaTypes)]) {
// iOS 3.0
pickerController.mediaTypes = [NSArray arrayWithObjects:(NSString*)kUTTypeImage, nil];
}
/*if ([pickerController respondsToSelector:@selector(cameraCaptureMode)]){
// iOS 4.0
pickerController.cameraCaptureMode = UIImagePickerControllerCameraCaptureModePhoto;
pickerController.cameraDevice = UIImagePickerControllerCameraDeviceRear;
pickerController.cameraFlashMode = UIImagePickerControllerCameraFlashModeAuto;
}*/
// CDVImagePicker specific property
pickerController.callbackId = callbackId;
pickerController.modalPresentationStyle = UIModalPresentationCurrentContext;
[self.viewController presentViewController:pickerController animated:YES completion:nil];
}
}
/* Process a still image from the camera.
* IN:
* UIImage* image - the UIImage data returned from the camera
* NSString* callbackId
*/
- (CDVPluginResult*)processImage:(UIImage*)image type:(NSString*)mimeType forCallbackId:(NSString*)callbackId
{
CDVPluginResult* result = nil;
// save the image to photo album
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil);
NSData* data = nil;
if (mimeType && [mimeType isEqualToString:@"image/png"]) {
data = UIImagePNGRepresentation(image);
} else {
data = UIImageJPEGRepresentation(image, 0.5);
}
// write to temp directory and return URI
NSString* docsPath = [NSTemporaryDirectory()stringByStandardizingPath]; // use file system temporary directory
NSError* err = nil;
NSFileManager* fileMgr = [[NSFileManager alloc] init];
// generate unique file name
NSString* filePath;
int i = 1;
do {
filePath = [NSString stringWithFormat:@"%@/photo_%03d.jpg", docsPath, i++];
} while ([fileMgr fileExistsAtPath:filePath]);
if (![data writeToFile:filePath options:NSAtomicWrite error:&err]) {
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageToErrorObject:CAPTURE_INTERNAL_ERR];
if (err) {
NSLog(@"Error saving image: %@", [err localizedDescription]);
}
} else {
// create MediaFile object
NSDictionary* fileDict = [self getMediaDictionaryFromPath:filePath ofType:mimeType];
NSArray* fileArray = [NSArray arrayWithObject:fileDict];
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:fileArray];
}
return result;
}
- (void)captureVideo:(CDVInvokedUrlCommand*)command
{
NSString* callbackId = command.callbackId;
NSDictionary* options = [command argumentAtIndex:0];
if ([options isKindOfClass:[NSNull class]]) {
options = [NSDictionary dictionary];
}
// options could contain limit, duration and mode
// taking more than one video (limit) is only supported if provide own controls via cameraOverlayView property
NSNumber* duration = [options objectForKey:@"duration"];
NSString* mediaType = nil;
if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) {
// there is a camera, it is available, make sure it can do movies
pickerController = [[CDVImagePicker alloc] init];
NSArray* types = nil;
if ([UIImagePickerController respondsToSelector:@selector(availableMediaTypesForSourceType:)]) {
types = [UIImagePickerController availableMediaTypesForSourceType:UIImagePickerControllerSourceTypeCamera];
// NSLog(@"MediaTypes: %@", [types description]);
if ([types containsObject:(NSString*)kUTTypeMovie]) {
mediaType = (NSString*)kUTTypeMovie;
} else if ([types containsObject:(NSString*)kUTTypeVideo]) {
mediaType = (NSString*)kUTTypeVideo;
}
}
}
if (!mediaType) {
// don't have video camera return error
NSLog(@"Capture.captureVideo: video mode not available.");
CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageToErrorObject:CAPTURE_NOT_SUPPORTED];
[self.commandDelegate sendPluginResult:result callbackId:callbackId];
pickerController = nil;
} else {
[self showAlertIfAccessProhibited];
pickerController.delegate = self;
pickerController.sourceType = UIImagePickerControllerSourceTypeCamera;
pickerController.allowsEditing = NO;
// iOS 3.0
pickerController.mediaTypes = [NSArray arrayWithObjects:mediaType, nil];
if ([mediaType isEqualToString:(NSString*)kUTTypeMovie]){
if (duration) {
pickerController.videoMaximumDuration = [duration doubleValue];
}
//NSLog(@"pickerController.videoMaximumDuration = %f", pickerController.videoMaximumDuration);
}
// iOS 4.0
if ([pickerController respondsToSelector:@selector(cameraCaptureMode)]) {
pickerController.cameraCaptureMode = UIImagePickerControllerCameraCaptureModeVideo;
// pickerController.videoQuality = UIImagePickerControllerQualityTypeHigh;
// pickerController.cameraDevice = UIImagePickerControllerCameraDeviceRear;
// pickerController.cameraFlashMode = UIImagePickerControllerCameraFlashModeAuto;
}
// CDVImagePicker specific property
pickerController.callbackId = callbackId;
pickerController.modalPresentationStyle = UIModalPresentationCurrentContext;
[self.viewController presentViewController:pickerController animated:YES completion:nil];
}
}
- (CDVPluginResult*)processVideo:(NSString*)moviePath forCallbackId:(NSString*)callbackId
{
// save the movie to photo album (only avail as of iOS 3.1)
/* don't need, it should automatically get saved
NSLog(@"can save %@: %d ?", moviePath, UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(moviePath));
if (&UIVideoAtPathIsCompatibleWithSavedPhotosAlbum != NULL && UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(moviePath) == YES) {
NSLog(@"try to save movie");
UISaveVideoAtPathToSavedPhotosAlbum(moviePath, nil, nil, nil);
NSLog(@"finished saving movie");
}*/
// create MediaFile object
NSDictionary* fileDict = [self getMediaDictionaryFromPath:moviePath ofType:nil];
NSArray* fileArray = [NSArray arrayWithObject:fileDict];
return [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:fileArray];
}
- (void)showAlertIfAccessProhibited
{
if (![self hasCameraAccess]) {
[self showPermissionsAlert];
}
}
- (BOOL)hasCameraAccess
{
AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
return status != AVAuthorizationStatusDenied && status != AVAuthorizationStatusRestricted;
}
- (void)showPermissionsAlert
{
__weak CDVCapture* weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[[[UIAlertView alloc] initWithTitle:[[NSBundle mainBundle]
objectForInfoDictionaryKey:@"CFBundleDisplayName"]
message:NSLocalizedString(@"Access to the camera has been prohibited; please enable it in the Settings app to continue.", nil)
delegate:weakSelf
cancelButtonTitle:NSLocalizedString(@"OK", nil)
otherButtonTitles:NSLocalizedString(@"Settings", nil), nil] show];
});
}
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
if (buttonIndex == 1) {
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString]];
}
CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageToErrorObject:CAPTURE_PERMISSION_DENIED];
[[pickerController presentingViewController] dismissViewControllerAnimated:YES completion:nil];
[self.commandDelegate sendPluginResult:result callbackId:pickerController.callbackId];
pickerController = nil;
self.inUse = NO;
}
- (void)getMediaModes:(CDVInvokedUrlCommand*)command
{
// NSString* callbackId = [command argumentAtIndex:0];
// NSMutableDictionary* imageModes = nil;
NSArray* imageArray = nil;
NSArray* movieArray = nil;
NSArray* audioArray = nil;
if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) {
// there is a camera, find the modes
// can get image/jpeg or image/png from camera
/* can't find a way to get the default height and width and other info
* for images/movies taken with UIImagePickerController
*/
NSDictionary* jpg = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInt:0], kW3CMediaFormatHeight,
[NSNumber numberWithInt:0], kW3CMediaFormatWidth,
@"image/jpeg", kW3CMediaModeType,
nil];
NSDictionary* png = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInt:0], kW3CMediaFormatHeight,
[NSNumber numberWithInt:0], kW3CMediaFormatWidth,
@"image/png", kW3CMediaModeType,
nil];
imageArray = [NSArray arrayWithObjects:jpg, png, nil];
if ([UIImagePickerController respondsToSelector:@selector(availableMediaTypesForSourceType:)]) {
NSArray* types = [UIImagePickerController availableMediaTypesForSourceType:UIImagePickerControllerSourceTypeCamera];
if ([types containsObject:(NSString*)kUTTypeMovie]) {
NSDictionary* mov = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInt:0], kW3CMediaFormatHeight,
[NSNumber numberWithInt:0], kW3CMediaFormatWidth,
@"video/quicktime", kW3CMediaModeType,
nil];
movieArray = [NSArray arrayWithObject:mov];
}
}
}
NSDictionary* modes = [NSDictionary dictionaryWithObjectsAndKeys:
imageArray ? (NSObject*) imageArray:[NSNull null], @"image",
movieArray ? (NSObject*) movieArray:[NSNull null], @"video",
audioArray ? (NSObject*) audioArray:[NSNull null], @"audio",
nil];
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:modes options:0 error:nil];
NSString* jsonStr = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
NSString* jsString = [NSString stringWithFormat:@"navigator.device.capture.setSupportedModes(%@);", jsonStr];
[self.commandDelegate evalJs:jsString];
}
- (void)getFormatData:(CDVInvokedUrlCommand*)command
{
NSString* callbackId = command.callbackId;
// existence of fullPath checked on JS side
NSString* fullPath = [command argumentAtIndex:0];
// mimeType could be null
NSString* mimeType = nil;
if ([command.arguments count] > 1) {
mimeType = [command argumentAtIndex:1];
}
BOOL bError = NO;
CDVCaptureError errorCode = CAPTURE_INTERNAL_ERR;
CDVPluginResult* result = nil;
if (!mimeType || [mimeType isKindOfClass:[NSNull class]]) {
// try to determine mime type if not provided
id command = [self.commandDelegate getCommandInstance:@"File"];
bError = !([command isKindOfClass:[CDVFile class]]);
if (!bError) {
CDVFile* cdvFile = (CDVFile*)command;
mimeType = [cdvFile getMimeTypeFromPath:fullPath];
if (!mimeType) {
// can't do much without mimeType, return error
bError = YES;
errorCode = CAPTURE_INVALID_ARGUMENT;
}
}
}
if (!bError) {
// create and initialize return dictionary
NSMutableDictionary* formatData = [NSMutableDictionary dictionaryWithCapacity:5];
[formatData setObject:[NSNull null] forKey:kW3CMediaFormatCodecs];
[formatData setObject:[NSNumber numberWithInt:0] forKey:kW3CMediaFormatBitrate];
[formatData setObject:[NSNumber numberWithInt:0] forKey:kW3CMediaFormatHeight];
[formatData setObject:[NSNumber numberWithInt:0] forKey:kW3CMediaFormatWidth];
[formatData setObject:[NSNumber numberWithInt:0] forKey:kW3CMediaFormatDuration];
if ([mimeType rangeOfString:@"image/"].location != NSNotFound) {
UIImage* image = [UIImage imageWithContentsOfFile:fullPath];
if (image) {
CGSize imgSize = [image size];
[formatData setObject:[NSNumber numberWithInteger:imgSize.width] forKey:kW3CMediaFormatWidth];
[formatData setObject:[NSNumber numberWithInteger:imgSize.height] forKey:kW3CMediaFormatHeight];
}
} else if (([mimeType rangeOfString:@"video/"].location != NSNotFound) && (NSClassFromString(@"AVURLAsset") != nil)) {
NSURL* movieURL = [NSURL fileURLWithPath:fullPath];
AVURLAsset* movieAsset = [[AVURLAsset alloc] initWithURL:movieURL options:nil];
CMTime duration = [movieAsset duration];
[formatData setObject:[NSNumber numberWithFloat:CMTimeGetSeconds(duration)] forKey:kW3CMediaFormatDuration];
NSArray* allVideoTracks = [movieAsset tracksWithMediaType:AVMediaTypeVideo];
if ([allVideoTracks count] > 0) {
AVAssetTrack* track = [[movieAsset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0];
CGSize size = [track naturalSize];
[formatData setObject:[NSNumber numberWithFloat:size.height] forKey:kW3CMediaFormatHeight];
[formatData setObject:[NSNumber numberWithFloat:size.width] forKey:kW3CMediaFormatWidth];
// not sure how to get codecs or bitrate???
// AVMetadataItem
// AudioFile
} else {
NSLog(@"No video tracks found for %@", fullPath);
}
} else if ([mimeType rangeOfString:@"audio/"].location != NSNotFound) {
if (NSClassFromString(@"AVAudioPlayer") != nil) {
NSURL* fileURL = [NSURL fileURLWithPath:fullPath];
NSError* err = nil;
AVAudioPlayer* avPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:fileURL error:&err];
if (!err) {
// get the data
[formatData setObject:[NSNumber numberWithDouble:[avPlayer duration]] forKey:kW3CMediaFormatDuration];
if ([avPlayer respondsToSelector:@selector(settings)]) {
NSDictionary* info = [avPlayer settings];
NSNumber* bitRate = [info objectForKey:AVEncoderBitRateKey];
if (bitRate) {
[formatData setObject:bitRate forKey:kW3CMediaFormatBitrate];
}
}
} // else leave data init'ed to 0
}
}
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:formatData];
// NSLog(@"getFormatData: %@", [formatData description]);
}
if (bError) {
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageToErrorObject:(int)errorCode];
}
if (result) {
[self.commandDelegate sendPluginResult:result callbackId:callbackId];
}
}
- (NSDictionary*)getMediaDictionaryFromPath:(NSString*)fullPath ofType:(NSString*)type
{
NSFileManager* fileMgr = [[NSFileManager alloc] init];
NSMutableDictionary* fileDict = [NSMutableDictionary dictionaryWithCapacity:5];
CDVFile *fs = [self.commandDelegate getCommandInstance:@"File"];
// Get canonical version of localPath
NSURL *fileURL = [NSURL URLWithString:[NSString stringWithFormat:@"file://%@", fullPath]];
NSURL *resolvedFileURL = [fileURL URLByResolvingSymlinksInPath];
NSString *path = [resolvedFileURL path];
CDVFilesystemURL *url = [fs fileSystemURLforLocalPath:path];
[fileDict setObject:[fullPath lastPathComponent] forKey:@"name"];
[fileDict setObject:fullPath forKey:@"fullPath"];
if (url) {
[fileDict setObject:[url absoluteURL] forKey:@"localURL"];
}
// determine type
if (!type) {
id command = [self.commandDelegate getCommandInstance:@"File"];
if ([command isKindOfClass:[CDVFile class]]) {
CDVFile* cdvFile = (CDVFile*)command;
NSString* mimeType = [cdvFile getMimeTypeFromPath:fullPath];
[fileDict setObject:(mimeType != nil ? (NSObject*)mimeType : [NSNull null]) forKey:@"type"];
}
}
NSDictionary* fileAttrs = [fileMgr attributesOfItemAtPath:fullPath error:nil];
[fileDict setObject:[NSNumber numberWithUnsignedLongLong:[fileAttrs fileSize]] forKey:@"size"];
NSDate* modDate = [fileAttrs fileModificationDate];
NSNumber* msDate = [NSNumber numberWithDouble:[modDate timeIntervalSince1970] * 1000];
[fileDict setObject:msDate forKey:@"lastModifiedDate"];
return fileDict;
}
- (void)imagePickerController:(UIImagePickerController*)picker didFinishPickingImage:(UIImage*)image editingInfo:(NSDictionary*)editingInfo
{
// older api calls new one
[self imagePickerController:picker didFinishPickingMediaWithInfo:editingInfo];
}
/* Called when image/movie is finished recording.
* Calls success or error code as appropriate
* 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
* name
* fullPath
* type
* lastModifiedDate
* size
*/
- (void)imagePickerController:(UIImagePickerController*)picker didFinishPickingMediaWithInfo:(NSDictionary*)info
{
CDVImagePicker* cameraPicker = (CDVImagePicker*)picker;
NSString* callbackId = cameraPicker.callbackId;
[[picker presentingViewController] dismissViewControllerAnimated:YES completion:nil];
CDVPluginResult* result = nil;
UIImage* image = nil;
NSString* mediaType = [info objectForKey:UIImagePickerControllerMediaType];
if (!mediaType || [mediaType isEqualToString:(NSString*)kUTTypeImage]) {
// mediaType is nil then only option is UIImagePickerControllerOriginalImage
if ([UIImagePickerController respondsToSelector:@selector(allowsEditing)] &&
(cameraPicker.allowsEditing && [info objectForKey:UIImagePickerControllerEditedImage])) {
image = [info objectForKey:UIImagePickerControllerEditedImage];
} else {
image = [info objectForKey:UIImagePickerControllerOriginalImage];
}
}
if (image != nil) {
// mediaType was image
result = [self processImage:image type:cameraPicker.mimeType forCallbackId:callbackId];
} else if ([mediaType isEqualToString:(NSString*)kUTTypeMovie]) {
// process video
NSString* moviePath = [(NSURL *)[info objectForKey:UIImagePickerControllerMediaURL] path];
if (moviePath) {
result = [self processVideo:moviePath forCallbackId:callbackId];
}
}
if (!result) {
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageToErrorObject:CAPTURE_INTERNAL_ERR];
}
[self.commandDelegate sendPluginResult:result callbackId:callbackId];
pickerController = nil;
}
- (void)imagePickerControllerDidCancel:(UIImagePickerController*)picker
{
CDVImagePicker* cameraPicker = (CDVImagePicker*)picker;
NSString* callbackId = cameraPicker.callbackId;
[[picker presentingViewController] dismissViewControllerAnimated:YES completion:nil];
CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageToErrorObject:CAPTURE_NO_MEDIA_FILES];
[self.commandDelegate sendPluginResult:result callbackId:callbackId];
pickerController = nil;
}
@end
@implementation CDVAudioNavigationController
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 90000
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
{
// delegate to CVDAudioRecorderViewController
return [self.topViewController supportedInterfaceOrientations];
}
#else
- (NSUInteger)supportedInterfaceOrientations
{
// delegate to CVDAudioRecorderViewController
return [self.topViewController supportedInterfaceOrientations];
}
#endif
@end
@interface CDVAudioRecorderViewController () {
UIStatusBarStyle _previousStatusBarStyle;
}
@end
@implementation CDVAudioRecorderViewController
@synthesize errorCode, callbackId, duration, captureCommand, doneButton, recordingView, recordButton, recordImage, stopRecordImage, timerLabel, avRecorder, avSession, pluginResult, timer, isTimed;
- (NSString*)resolveImageResource:(NSString*)resource
{
NSString* systemVersion = [[UIDevice currentDevice] systemVersion];
BOOL isLessThaniOS4 = ([systemVersion compare:@"4.0" options:NSNumericSearch] == NSOrderedAscending);
// the iPad image (nor retina) differentiation code was not in 3.x, and we have to explicitly set the path
// if user wants iPhone only app to run on iPad they must remove *~ipad.* images from CDVCapture.bundle
if (isLessThaniOS4) {
NSString* iPadResource = [NSString stringWithFormat:@"%@~ipad.png", resource];
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad && [UIImage imageNamed:iPadResource]) {
return iPadResource;
} else {
return [NSString stringWithFormat:@"%@.png", resource];
}
}
return resource;
}
- (id)initWithCommand:(CDVCapture*)theCommand duration:(NSNumber*)theDuration callbackId:(NSString*)theCallbackId
{
if ((self = [super init])) {
self.captureCommand = theCommand;
self.duration = theDuration;
self.callbackId = theCallbackId;
self.errorCode = CAPTURE_NO_MEDIA_FILES;
self.isTimed = self.duration != nil;
_previousStatusBarStyle = [UIApplication sharedApplication].statusBarStyle;
return self;
}
return nil;
}
- (void)loadView
{
if ([self respondsToSelector:@selector(edgesForExtendedLayout)]) {
self.edgesForExtendedLayout = UIRectEdgeNone;
}
// create view and display
CGRect viewRect = [[UIScreen mainScreen] applicationFrame];
UIView* tmp = [[UIView alloc] initWithFrame:viewRect];
// make backgrounds
NSString* microphoneResource = @"CDVCapture.bundle/microphone";
BOOL isIphone5 = ([[UIScreen mainScreen] bounds].size.width == 568 && [[UIScreen mainScreen] bounds].size.height == 320) || ([[UIScreen mainScreen] bounds].size.height == 568 && [[UIScreen mainScreen] bounds].size.width == 320);
if (isIphone5) {
microphoneResource = @"CDVCapture.bundle/microphone-568h";
}
NSBundle* cdvBundle = [NSBundle bundleForClass:[CDVCapture class]];
UIImage* microphone = [UIImage imageNamed:[self resolveImageResource:microphoneResource] inBundle:cdvBundle compatibleWithTraitCollection:nil];
UIView* microphoneView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, viewRect.size.width, microphone.size.height)];
[microphoneView setBackgroundColor:[UIColor colorWithPatternImage:microphone]];
[microphoneView setUserInteractionEnabled:NO];
[microphoneView setIsAccessibilityElement:NO];
[tmp addSubview:microphoneView];
// add bottom bar view
UIImage* grayBkg = [UIImage imageNamed:[self resolveImageResource:@"CDVCapture.bundle/controls_bg"] inBundle:cdvBundle compatibleWithTraitCollection:nil];
UIView* controls = [[UIView alloc] initWithFrame:CGRectMake(0, microphone.size.height, viewRect.size.width, grayBkg.size.height)];
[controls setBackgroundColor:[UIColor colorWithPatternImage:grayBkg]];
[controls setUserInteractionEnabled:NO];
[controls setIsAccessibilityElement:NO];
[tmp addSubview:controls];
// make red recording background view
UIImage* recordingBkg = [UIImage imageNamed:[self resolveImageResource:@"CDVCapture.bundle/recording_bg"] inBundle:cdvBundle compatibleWithTraitCollection:nil];
UIColor* background = [UIColor colorWithPatternImage:recordingBkg];
self.recordingView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, viewRect.size.width, recordingBkg.size.height)];
[self.recordingView setBackgroundColor:background];
[self.recordingView setHidden:YES];
[self.recordingView setUserInteractionEnabled:NO];
[self.recordingView setIsAccessibilityElement:NO];
[tmp addSubview:self.recordingView];
// add label
self.timerLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, viewRect.size.width, recordingBkg.size.height)];
// timerLabel.autoresizingMask = reSizeMask;
[self.timerLabel setBackgroundColor:[UIColor clearColor]];
[self.timerLabel setTextColor:[UIColor whiteColor]];
#ifdef __IPHONE_6_0
[self.timerLabel setTextAlignment:NSTextAlignmentCenter];
#else
// for iOS SDK < 6.0
[self.timerLabel setTextAlignment:UITextAlignmentCenter];
#endif
[self.timerLabel setText:@"0:00"];
[self.timerLabel setAccessibilityHint:PluginLocalizedString(captureCommand, @"recorded time in minutes and seconds", nil)];
self.timerLabel.accessibilityTraits |= UIAccessibilityTraitUpdatesFrequently;
self.timerLabel.accessibilityTraits &= ~UIAccessibilityTraitStaticText;
[tmp addSubview:self.timerLabel];
// Add record button
self.recordImage = [UIImage imageNamed:[self resolveImageResource:@"CDVCapture.bundle/record_button"] inBundle:cdvBundle compatibleWithTraitCollection:nil];
self.stopRecordImage = [UIImage imageNamed:[self resolveImageResource:@"CDVCapture.bundle/stop_button"] inBundle:cdvBundle compatibleWithTraitCollection:nil];
self.recordButton.accessibilityTraits |= [self accessibilityTraits];
self.recordButton = [[UIButton alloc] initWithFrame:CGRectMake((viewRect.size.width - recordImage.size.width) / 2, (microphone.size.height + (grayBkg.size.height - recordImage.size.height) / 2), recordImage.size.width, recordImage.size.height)];
[self.recordButton setAccessibilityLabel:PluginLocalizedString(captureCommand, @"toggle audio recording", nil)];
[self.recordButton setImage:recordImage forState:UIControlStateNormal];
[self.recordButton addTarget:self action:@selector(processButton:) forControlEvents:UIControlEventTouchUpInside];
[tmp addSubview:recordButton];
// make and add done button to navigation bar
self.doneButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(dismissAudioView:)];
[self.doneButton setStyle:UIBarButtonItemStyleDone];
self.navigationItem.rightBarButtonItem = self.doneButton;
[self setView:tmp];
}
- (void)viewDidLoad
{
[super viewDidLoad];
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil);
NSError* error = nil;
if (self.avSession == nil) {
// create audio session
self.avSession = [AVAudioSession sharedInstance];
if (error) {
// return error if can't create recording audio session
NSLog(@"error creating audio session: %@", [[error userInfo] description]);
self.errorCode = CAPTURE_INTERNAL_ERR;
[self dismissAudioView:nil];
}
}
// create file to record to in temporary dir
NSString* docsPath = [NSTemporaryDirectory()stringByStandardizingPath]; // use file system temporary directory
NSError* err = nil;
NSFileManager* fileMgr = [[NSFileManager alloc] init];
// generate unique file name
NSString* filePath;
int i = 1;
do {
filePath = [NSString stringWithFormat:@"%@/audio_%03d.wav", docsPath, i++];
} while ([fileMgr fileExistsAtPath:filePath]);
NSURL* fileURL = [NSURL fileURLWithPath:filePath isDirectory:NO];
// create AVAudioPlayer
NSDictionary *recordSetting = [[NSMutableDictionary alloc] init];
self.avRecorder = [[AVAudioRecorder alloc] initWithURL:fileURL settings:recordSetting error:&err];
if (err) {
NSLog(@"Failed to initialize AVAudioRecorder: %@\n", [err localizedDescription]);
self.avRecorder = nil;
// return error
self.errorCode = CAPTURE_INTERNAL_ERR;
[self dismissAudioView:nil];
} else {
self.avRecorder.delegate = self;
[self.avRecorder prepareToRecord];
self.recordButton.enabled = YES;
self.doneButton.enabled = YES;
}
}
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 90000
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
{
UIInterfaceOrientationMask orientation = UIInterfaceOrientationMaskPortrait;
UIInterfaceOrientationMask supported = [captureCommand.viewController supportedInterfaceOrientations];
orientation = orientation | (supported & UIInterfaceOrientationMaskPortraitUpsideDown);
return orientation;
}
#else
- (NSUInteger)supportedInterfaceOrientations
{
NSUInteger orientation = UIInterfaceOrientationMaskPortrait; // must support portrait
NSUInteger supported = [captureCommand.viewController supportedInterfaceOrientations];
orientation = orientation | (supported & UIInterfaceOrientationMaskPortraitUpsideDown);
return orientation;
}
#endif
- (void)viewDidUnload
{
[self setView:nil];
[self.captureCommand setInUse:NO];
}
- (void)processButton:(id)sender
{
if (self.avRecorder.recording) {
// stop recording
[self.avRecorder stop];
self.isTimed = NO; // recording was stopped via button so reset isTimed
// view cleanup will occur in audioRecordingDidFinishRecording
} else {
// begin recording
[self.recordButton setImage:stopRecordImage forState:UIControlStateNormal];
self.recordButton.accessibilityTraits &= ~[self accessibilityTraits];
[self.recordingView setHidden:NO];
__block NSError* error = nil;
__weak CDVAudioRecorderViewController* weakSelf = self;
void (^startRecording)(void) = ^{
[weakSelf.avSession setCategory:AVAudioSessionCategoryRecord error:&error];
[weakSelf.avSession setActive:YES error:&error];
if (error) {
// can't continue without active audio session
weakSelf.errorCode = CAPTURE_INTERNAL_ERR;
[weakSelf dismissAudioView:nil];
} else {
if (weakSelf.duration) {
weakSelf.isTimed = true;
[weakSelf.avRecorder recordForDuration:[duration doubleValue]];
} else {
[weakSelf.avRecorder record];
}
[weakSelf.timerLabel setText:@"0.00"];
weakSelf.timer = [NSTimer scheduledTimerWithTimeInterval:0.5f target:weakSelf selector:@selector(updateTime) userInfo:nil repeats:YES];
weakSelf.doneButton.enabled = NO;
}
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil);
};
SEL rrpSel = NSSelectorFromString(@"requestRecordPermission:");
if ([self.avSession respondsToSelector:rrpSel])
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self.avSession performSelector:rrpSel withObject:^(BOOL granted){
if (granted) {
startRecording();
} else {
NSLog(@"Error creating audio session, microphone permission denied.");
weakSelf.errorCode = CAPTURE_INTERNAL_ERR;
[weakSelf dismissAudioView:nil];
}
}];
#pragma clang diagnostic pop
} else {
startRecording();
}
}
}
/*
* helper method to clean up when stop recording
*/
- (void)stopRecordingCleanup
{
if (self.avRecorder.recording) {
[self.avRecorder stop];
}
[self.recordButton setImage:recordImage forState:UIControlStateNormal];
self.recordButton.accessibilityTraits |= [self accessibilityTraits];
[self.recordingView setHidden:YES];
self.doneButton.enabled = YES;
if (self.avSession) {
// deactivate session so sounds can come through
[self.avSession setCategory:AVAudioSessionCategoryPlayAndRecord error:nil];
[self.avSession setActive:NO error:nil];
}
if (self.duration && self.isTimed) {
// VoiceOver announcement so user knows timed recording has finished
//BOOL isUIAccessibilityAnnouncementNotification = (&UIAccessibilityAnnouncementNotification != NULL);
if (UIAccessibilityAnnouncementNotification) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 500ull * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, PluginLocalizedString(captureCommand, @"timed recording complete", nil));
});
}
} else {
// issue a layout notification change so that VO will reannounce the button label when recording completes
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil);
}
}
- (void)dismissAudioView:(id)sender
{
// called when done button pressed or when error condition to do cleanup and remove view
[[self.captureCommand.viewController.presentedViewController presentingViewController] dismissViewControllerAnimated:YES completion:nil];
if (!self.pluginResult) {
// return error
self.pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageToErrorObject:(int)self.errorCode];
}
self.avRecorder = nil;
[self.avSession setCategory:AVAudioSessionCategoryPlayAndRecord error:nil];
[self.avSession setActive:NO error:nil];
[self.captureCommand setInUse:NO];
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil);
// return result
[self.captureCommand.commandDelegate sendPluginResult:pluginResult callbackId:callbackId];
if (IsAtLeastiOSVersion(@"7.0")) {
[[UIApplication sharedApplication] setStatusBarStyle:_previousStatusBarStyle];
}
}
- (void)updateTime
{
// update the label with the elapsed time
[self.timerLabel setText:[self formatTime:self.avRecorder.currentTime]];
}
- (NSString*)formatTime:(int)interval
{
// is this format universal?
int secs = interval % 60;
int min = interval / 60;
if (interval < 60) {
return [NSString stringWithFormat:@"0:%02d", interval];
} else {
return [NSString stringWithFormat:@"%d:%02d", min, secs];
}
}
- (void)audioRecorderDidFinishRecording:(AVAudioRecorder*)recorder successfully:(BOOL)flag
{
// may be called when timed audio finishes - need to stop time and reset buttons
[self.timer invalidate];
[self stopRecordingCleanup];
// generate success result
if (flag) {
NSString* filePath = [avRecorder.url path];
// NSLog(@"filePath: %@", filePath);
NSDictionary* fileDict = [captureCommand getMediaDictionaryFromPath:filePath ofType:@"audio/wav"];
NSArray* fileArray = [NSArray arrayWithObject:fileDict];
self.pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:fileArray];
} else {
self.pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageToErrorObject:CAPTURE_INTERNAL_ERR];
}
}
- (void)audioRecorderEncodeErrorDidOccur:(AVAudioRecorder*)recorder error:(NSError*)error
{
[self.timer invalidate];
[self stopRecordingCleanup];
NSLog(@"error recording audio");
self.pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageToErrorObject:CAPTURE_INTERNAL_ERR];
[self dismissAudioView:nil];
}
- (UIStatusBarStyle)preferredStatusBarStyle
{
return UIStatusBarStyleDefault;
}
- (void)viewWillAppear:(BOOL)animated
{
if (IsAtLeastiOSVersion(@"7.0")) {
[[UIApplication sharedApplication] setStatusBarStyle:[self preferredStatusBarStyle]];
}
[super viewWillAppear:animated];
}
@end
/*
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
/* global Windows:true */
var MediaFile = require('cordova-plugin-media-capture.MediaFile');
var CaptureError = require('cordova-plugin-media-capture.CaptureError');
var CaptureAudioOptions = require('cordova-plugin-media-capture.CaptureAudioOptions');
var CaptureVideoOptions = require('cordova-plugin-media-capture.CaptureVideoOptions');
var MediaFileData = require('cordova-plugin-media-capture.MediaFileData');
/*
* Class that combines all logic for capturing picture and video on WP8.1
*/
function MediaCaptureProxy () {
var previewContainer;
var capturePreview = null;
var captureCancelButton = null; // eslint-disable-line no-unused-vars
var captureSettings = null;
var captureStarted = false;
var capturedPictureFile;
var capturedVideoFile;
var capture = null;
var CaptureNS = Windows.Media.Capture;
/**
* Helper function that toggles visibility of DOM elements with provided ids
* @param {String} variable number of elements' ids which visibility needs to be toggled
*/
function toggleElements () {
// convert arguments to array
var args = Array.prototype.slice.call(arguments);
args.forEach(function (buttonId) {
var buttonEl = document.getElementById(buttonId);
if (buttonEl) {
var curDisplayStyle = buttonEl.style.display;
buttonEl.style.display = curDisplayStyle === 'none' ? 'block' : 'none';
}
});
}
/**
* Creates basic camera UI with preview 'video' element and 'Cancel' button
* Capture starts, when you clicking on preview.
*/
function createCameraUI () {
var buttonStyle = 'margin: 7px; border: 2.5px solid white; width: 45%; height: 35px; color: white; background-color: black;';
previewContainer = document.createElement('div');
previewContainer.style.cssText = 'background-position: 50% 50%; background-repeat: no-repeat; background-size: contain; background-color: black; left: 0px; top: 0px; width: 100%; height: 100%; position: fixed; z-index: 9999';
previewContainer.innerHTML =
'<video id="capturePreview" style="width: 100%; height: 100%"></video>' +
'<div id="previewButtons" style="width: 100%; bottom: 0px; display: flex; position: absolute; justify-content: space-around; background-color: black;">' +
'<button id="takePicture" style="' + buttonStyle + '">Capture</button>' +
'<button id="cancelCapture" style="' + buttonStyle + '">Cancel</button>' +
'<button id="selectPicture" style="display: none; ' + buttonStyle + '">Accept</button>' +
'<button id="retakePicture" style="display: none; ' + buttonStyle + '">Retake</button>' +
'</div>';
document.body.appendChild(previewContainer);
// Create fullscreen preview
capturePreview = document.getElementById('capturePreview');
// Create cancel button
captureCancelButton = document.getElementById('cancelCapture');
capture = new CaptureNS.MediaCapture();
captureSettings = new CaptureNS.MediaCaptureInitializationSettings();
captureSettings.streamingCaptureMode = CaptureNS.StreamingCaptureMode.audioAndVideo;
}
/**
* Starts camera preview and binds provided callbacks to controls
* @param {function} takeCallback Callback for Take button
* @param {function} errorCallback Callback for Cancel button + default error callback
* @param {function} selectCallback Callback for Select button
* @param {function} retakeCallback Callback for Retake button
*/
function startCameraPreview (takeCallback, errorCallback, selectCallback, retakeCallback) {
// try to select appropriate device for capture
// rear camera is preferred option
var expectedPanel = Windows.Devices.Enumeration.Panel.back;
Windows.Devices.Enumeration.DeviceInformation.findAllAsync(Windows.Devices.Enumeration.DeviceClass.videoCapture).done(function (devices) {
if (devices.length > 0) {
devices.forEach(function (currDev) {
if (currDev.enclosureLocation && currDev.enclosureLocation.panel && currDev.enclosureLocation.panel === expectedPanel) {
captureSettings.videoDeviceId = currDev.id;
}
});
capture.initializeAsync(captureSettings).done(function () {
// This is necessary since WP8.1 MediaCapture outputs video stream rotated 90 degrees CCW
// TODO: This can be not consistent across devices, need additional testing on various devices
// msdn.microsoft.com/en-us/library/windows/apps/hh452807.aspx
capture.setPreviewRotation(Windows.Media.Capture.VideoRotation.clockwise90Degrees);
capturePreview.msZoom = true;
capturePreview.src = URL.createObjectURL(capture); // eslint-disable-line no-undef
capturePreview.play();
previewContainer.style.display = 'block';
// Bind events to controls
capturePreview.onclick = takeCallback;
document.getElementById('takePicture').onclick = takeCallback;
document.getElementById('cancelCapture').onclick = function () {
errorCallback(CaptureError.CAPTURE_NO_MEDIA_FILES);
};
document.getElementById('selectPicture').onclick = selectCallback;
document.getElementById('retakePicture').onclick = retakeCallback;
}, function (err) {
destroyCameraPreview();
errorCallback(CaptureError.CAPTURE_INTERNAL_ERR, err);
});
} else {
// no appropriate devices found
destroyCameraPreview();
errorCallback(CaptureError.CAPTURE_INTERNAL_ERR);
}
});
}
/**
* Destroys camera preview, removes all elements created
*/
function destroyCameraPreview () {
capturePreview.pause();
capturePreview.src = null;
if (previewContainer) {
document.body.removeChild(previewContainer);
}
if (capture) {
capture.stopRecordAsync();
capture = null;
}
}
return {
/**
* Initiate video capture using MediaCapture class
* @param {function} successCallback Called, when user clicked on preview, with captured file object
* @param {function} errorCallback Called on any error
*/
captureVideo: function (successCallback, errorCallback) {
try {
createCameraUI();
startCameraPreview(function () {
// This callback called twice: whem video capture started and when it ended
// so we need to check capture status
if (!captureStarted) {
// remove cancel button and rename 'Take' button to 'Stop'
toggleElements('cancelCapture');
document.getElementById('takePicture').text = 'Stop';
var encodingProperties = Windows.Media.MediaProperties.MediaEncodingProfile.createMp4(Windows.Media.MediaProperties.VideoEncodingQuality.auto);
var generateUniqueCollisionOption = Windows.Storage.CreationCollisionOption.generateUniqueName;
var localFolder = Windows.Storage.ApplicationData.current.localFolder;
localFolder.createFileAsync('cameraCaptureVideo.mp4', generateUniqueCollisionOption).done(function (capturedFile) {
capture.startRecordToStorageFileAsync(encodingProperties, capturedFile).done(function () {
capturedVideoFile = capturedFile;
captureStarted = true;
}, function (err) {
destroyCameraPreview();
errorCallback(CaptureError.CAPTURE_INTERNAL_ERR, err);
});
}, function (err) {
destroyCameraPreview();
errorCallback(CaptureError.CAPTURE_INTERNAL_ERR, err);
});
} else {
capture.stopRecordAsync().done(function () {
destroyCameraPreview();
successCallback(capturedVideoFile);
});
}
}, errorCallback);
} catch (ex) {
destroyCameraPreview();
errorCallback(CaptureError.CAPTURE_INTERNAL_ERR, ex);
}
},
/**
* Initiate image capture using MediaCapture class
* @param {function} successCallback Called, when user clicked on preview, with captured file object
* @param {function} errorCallback Called on any error
*/
capturePhoto: function (successCallback, errorCallback) {
try {
createCameraUI();
startCameraPreview(
// Callback for Take button - captures intermediate image file.
function () {
var encodingProperties = Windows.Media.MediaProperties.ImageEncodingProperties.createJpeg();
var overwriteCollisionOption = Windows.Storage.CreationCollisionOption.replaceExisting;
var tempFolder = Windows.Storage.ApplicationData.current.temporaryFolder;
tempFolder.createFileAsync('cameraCaptureImage.jpg', overwriteCollisionOption).done(function (capturedFile) {
capture.capturePhotoToStorageFileAsync(encodingProperties, capturedFile).done(function () {
// store intermediate result in object's global variable
capturedPictureFile = capturedFile;
// show pre-captured image and toggle visibility of all buttons
previewContainer.style.backgroundImage = 'url("' + 'ms-appdata:///temp/' + capturedFile.name + '")';
toggleElements('capturePreview', 'takePicture', 'cancelCapture', 'selectPicture', 'retakePicture');
}, function (err) {
destroyCameraPreview();
errorCallback(CaptureError.CAPTURE_INTERNAL_ERR, err);
});
}, function (err) {
destroyCameraPreview();
errorCallback(CaptureError.CAPTURE_INTERNAL_ERR, err);
});
},
// error + cancel callback
function (err) {
destroyCameraPreview();
errorCallback(err);
},
// Callback for Select button - copies intermediate file into persistent application's storage
function () {
var generateUniqueCollisionOption = Windows.Storage.CreationCollisionOption.generateUniqueName;
var localFolder = Windows.Storage.ApplicationData.current.localFolder;
capturedPictureFile.copyAsync(localFolder, capturedPictureFile.name, generateUniqueCollisionOption).done(function (copiedFile) {
destroyCameraPreview();
successCallback(copiedFile);
}, function (err) {
destroyCameraPreview();
errorCallback(err);
});
},
// Callback for retake button - just toggles visibility of necessary elements
function () {
toggleElements('capturePreview', 'takePicture', 'cancelCapture', 'selectPicture', 'retakePicture');
}
);
} catch (ex) {
destroyCameraPreview();
errorCallback(CaptureError.CAPTURE_INTERNAL_ERR, ex);
}
}
};
}
module.exports = {
captureAudio: function (successCallback, errorCallback, args) {
var options = args[0];
var audioOptions = new CaptureAudioOptions();
if (typeof (options.duration) === 'undefined') {
audioOptions.duration = 3600; // Arbitrary amount, need to change later
} else if (options.duration > 0) {
audioOptions.duration = options.duration;
} else {
errorCallback(new CaptureError(CaptureError.CAPTURE_INVALID_ARGUMENT));
return;
}
// Some shortcuts for long namespaces
var CaptureNS = Windows.Media.Capture;
var MediaPropsNS = Windows.Media.MediaProperties;
var localAppData = Windows.Storage.ApplicationData.current.localFolder;
var generateUniqueName = Windows.Storage.NameCollisionOption.generateUniqueName;
var mediaCapture = new CaptureNS.MediaCapture();
var mediaCaptureSettings = new CaptureNS.MediaCaptureInitializationSettings();
var mp3EncodingProfile = new MediaPropsNS.MediaEncodingProfile.createMp3(MediaPropsNS.AudioEncodingQuality.auto); // eslint-disable-line new-cap
var m4aEncodingProfile = new MediaPropsNS.MediaEncodingProfile.createM4a(MediaPropsNS.AudioEncodingQuality.auto); // eslint-disable-line new-cap
mediaCaptureSettings.streamingCaptureMode = CaptureNS.StreamingCaptureMode.audio;
var capturedFile;
var stopRecordTimeout;
var stopRecord = function () {
mediaCapture.stopRecordAsync().then(function () {
capturedFile.getBasicPropertiesAsync().then(function (basicProperties) {
var result = new MediaFile(capturedFile.name, 'ms-appdata:///local/' + capturedFile.name, capturedFile.contentType, basicProperties.dateModified, basicProperties.size);
result.fullPath = capturedFile.path;
successCallback([result]);
}, function () {
errorCallback(new CaptureError(CaptureError.CAPTURE_NO_MEDIA_FILES));
});
}, function () { errorCallback(new CaptureError(CaptureError.CAPTURE_NO_MEDIA_FILES)); });
};
mediaCapture.initializeAsync(mediaCaptureSettings).done(function () {
localAppData.createFileAsync('captureAudio.mp3', generateUniqueName).then(function (storageFile) {
capturedFile = storageFile;
mediaCapture.startRecordToStorageFileAsync(mp3EncodingProfile, capturedFile).then(function () {
stopRecordTimeout = setTimeout(stopRecord, audioOptions.duration * 1000);
}, function (err) {
// -1072868846 is the error code for "No suitable transform was found to encode or decode the content."
// so we try to use another (m4a) format
if (err.number === -1072868846) {
// first we clear existing timeout to prevent success callback to be called with invalid arguments
// second we start same actions to try to record m4a audio
clearTimeout(stopRecordTimeout);
localAppData.createFileAsync('captureAudio.m4a', generateUniqueName).then(function (storageFile) {
capturedFile = storageFile;
mediaCapture.startRecordToStorageFileAsync(m4aEncodingProfile, capturedFile).then(function () {
stopRecordTimeout = setTimeout(stopRecord, audioOptions.duration * 1000);
}, function () {
// if we here, we're totally failed to record either mp3 or m4a
errorCallback(new CaptureError(CaptureError.CAPTURE_INTERNAL_ERR));
});
});
} else {
errorCallback(new CaptureError(CaptureError.CAPTURE_INTERNAL_ERR));
}
});
}, function () { errorCallback(new CaptureError(CaptureError.CAPTURE_NO_MEDIA_FILES)); });
});
},
captureImage: function (successCallback, errorCallback, args) {
var CaptureNS = Windows.Media.Capture;
function fail (code, data) {
var err = new CaptureError(code);
err.message = data;
errorCallback(err);
}
// Check if necessary API available
if (!CaptureNS.CameraCaptureUI) {
// We are running on WP8.1 which lacks CameraCaptureUI class
// so we need to use MediaCapture class instead and implement custom UI for camera
var proxy = new MediaCaptureProxy();
proxy.capturePhoto(function (photoFile) {
photoFile.getBasicPropertiesAsync().done(function (basicProperties) {
var result = new MediaFile(photoFile.name, 'ms-appdata:///local/' + photoFile.name, photoFile.contentType, basicProperties.dateModified, basicProperties.size);
result.fullPath = photoFile.path;
successCallback([result]);
}, function (err) {
fail(CaptureError.CAPTURE_INTERNAL_ERR, err);
});
}, function (err) {
fail(err);
});
} else {
var cameraCaptureUI = new Windows.Media.Capture.CameraCaptureUI();
cameraCaptureUI.photoSettings.allowCropping = true;
cameraCaptureUI.photoSettings.maxResolution = Windows.Media.Capture.CameraCaptureUIMaxPhotoResolution.highestAvailable;
cameraCaptureUI.photoSettings.format = Windows.Media.Capture.CameraCaptureUIPhotoFormat.jpeg;
cameraCaptureUI.captureFileAsync(Windows.Media.Capture.CameraCaptureUIMode.photo).done(function (file) {
if (file) {
file.moveAsync(Windows.Storage.ApplicationData.current.localFolder, 'cameraCaptureImage.jpg', Windows.Storage.NameCollisionOption.generateUniqueName).then(function () {
file.getBasicPropertiesAsync().then(function (basicProperties) {
var result = new MediaFile(file.name, 'ms-appdata:///local/' + file.name, file.contentType, basicProperties.dateModified, basicProperties.size);
result.fullPath = file.path;
successCallback([result]);
}, function () {
errorCallback(new CaptureError(CaptureError.CAPTURE_NO_MEDIA_FILES));
});
}, function () {
errorCallback(new CaptureError(CaptureError.CAPTURE_NO_MEDIA_FILES));
});
} else {
errorCallback(new CaptureError(CaptureError.CAPTURE_NO_MEDIA_FILES));
}
}, function () {
errorCallback(new CaptureError(CaptureError.CAPTURE_NO_MEDIA_FILES));
});
}
},
captureVideo: function (successCallback, errorCallback, args) {
var options = args[0];
var CaptureNS = Windows.Media.Capture;
function fail (code, data) {
var err = new CaptureError(code);
err.message = data;
errorCallback(err);
}
// Check if necessary API available
if (!CaptureNS.CameraCaptureUI) {
// We are running on WP8.1 which lacks CameraCaptureUI class
// so we need to use MediaCapture class instead and implement custom UI for camera
var proxy = new MediaCaptureProxy();
proxy.captureVideo(function (videoFile) {
videoFile.getBasicPropertiesAsync().done(function (basicProperties) {
var result = new MediaFile(videoFile.name, 'ms-appdata:///local/' + videoFile.name, videoFile.contentType, basicProperties.dateModified, basicProperties.size);
result.fullPath = videoFile.path;
successCallback([result]);
}, function (err) {
fail(CaptureError.CAPTURE_INTERNAL_ERR, err);
});
}, fail);
} else {
var videoOptions = new CaptureVideoOptions();
if (options.duration && options.duration > 0) {
videoOptions.duration = options.duration;
}
if (options.limit > 1) {
videoOptions.limit = options.limit;
}
var cameraCaptureUI = new Windows.Media.Capture.CameraCaptureUI();
cameraCaptureUI.videoSettings.allowTrimming = true;
cameraCaptureUI.videoSettings.format = Windows.Media.Capture.CameraCaptureUIVideoFormat.mp4;
cameraCaptureUI.videoSettings.maxDurationInSeconds = videoOptions.duration;
cameraCaptureUI.captureFileAsync(Windows.Media.Capture.CameraCaptureUIMode.video).then(function (file) {
if (file) {
file.moveAsync(Windows.Storage.ApplicationData.current.localFolder, 'cameraCaptureVideo.mp4', Windows.Storage.NameCollisionOption.generateUniqueName).then(function () {
file.getBasicPropertiesAsync().then(function (basicProperties) {
var result = new MediaFile(file.name, 'ms-appdata:///local/' + file.name, file.contentType, basicProperties.dateModified, basicProperties.size);
result.fullPath = file.path;
successCallback([result]);
}, function () {
errorCallback(new CaptureError(CaptureError.CAPTURE_NO_MEDIA_FILES));
});
}, function () {
errorCallback(new CaptureError(CaptureError.CAPTURE_NO_MEDIA_FILES));
});
} else {
errorCallback(new CaptureError(CaptureError.CAPTURE_NO_MEDIA_FILES));
}
}, function () { errorCallback(new CaptureError(CaptureError.CAPTURE_NO_MEDIA_FILES)); });
}
},
getFormatData: function (successCallback, errorCallback, args) {
Windows.Storage.StorageFile.getFileFromPathAsync(args[0]).then(
function (storageFile) {
var mediaTypeFlag = String(storageFile.contentType).split('/')[0].toLowerCase();
if (mediaTypeFlag === 'audio') {
storageFile.properties.getMusicPropertiesAsync().then(function (audioProperties) {
successCallback(new MediaFileData(null, audioProperties.bitrate, 0, 0, audioProperties.duration / 1000));
}, function () {
errorCallback(new CaptureError(CaptureError.CAPTURE_INVALID_ARGUMENT));
});
} else if (mediaTypeFlag === 'video') {
storageFile.properties.getVideoPropertiesAsync().then(function (videoProperties) {
successCallback(new MediaFileData(null, videoProperties.bitrate, videoProperties.height, videoProperties.width, videoProperties.duration / 1000));
}, function () {
errorCallback(new CaptureError(CaptureError.CAPTURE_INVALID_ARGUMENT));
});
} else if (mediaTypeFlag === 'image') {
storageFile.properties.getImagePropertiesAsync().then(function (imageProperties) {
successCallback(new MediaFileData(null, 0, imageProperties.height, imageProperties.width, 0));
}, function () {
errorCallback(new CaptureError(CaptureError.CAPTURE_INVALID_ARGUMENT));
});
} else { errorCallback(new CaptureError(CaptureError.CAPTURE_INVALID_ARGUMENT)); }
}, function () {
errorCallback(new CaptureError(CaptureError.CAPTURE_INVALID_ARGUMENT));
}
);
}
};
require('cordova/exec/proxy').add('Capture', module.exports);
/*
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
/* global Windows:true */
var MediaFileData = require('cordova-plugin-media-capture.MediaFileData');
var CaptureError = require('cordova-plugin-media-capture.CaptureError');
module.exports = {
getFormatData: function (successCallback, errorCallback, args) {
Windows.Storage.StorageFile.getFileFromPathAsync(this.fullPath).then(
function (storageFile) {
var mediaTypeFlag = String(storageFile.contentType).split('/')[0].toLowerCase();
if (mediaTypeFlag === 'audio') {
storageFile.properties.getMusicPropertiesAsync().then(
function (audioProperties) {
successCallback(new MediaFileData(null, audioProperties.bitrate, 0, 0, audioProperties.duration / 1000));
}, function () {
errorCallback(new CaptureError(CaptureError.CAPTURE_INVALID_ARGUMENT));
}
);
} else if (mediaTypeFlag === 'video') {
storageFile.properties.getVideoPropertiesAsync().then(
function (videoProperties) {
successCallback(new MediaFileData(null, videoProperties.bitrate, videoProperties.height, videoProperties.width, videoProperties.duration / 1000));
}, function () {
errorCallback(new CaptureError(CaptureError.CAPTURE_INVALID_ARGUMENT));
}
);
} else if (mediaTypeFlag === 'image') {
storageFile.properties.getImagePropertiesAsync().then(
function (imageProperties) {
successCallback(new MediaFileData(null, 0, imageProperties.height, imageProperties.width, 0));
}, function () {
errorCallback(new CaptureError(CaptureError.CAPTURE_INVALID_ARGUMENT));
}
);
} else {
errorCallback(new CaptureError(CaptureError.CAPTURE_INVALID_ARGUMENT));
}
}, function () {
errorCallback(new CaptureError(CaptureError.CAPTURE_INVALID_ARGUMENT));
}
);
}
};
{
"name": "cordova-plugin-media-capture-tests",
"version": "3.0.3",
"description": "",
"cordova": {
"id": "cordova-plugin-media-capture-tests",
"platforms": []
},
"keywords": [
"ecosystem:cordova"
],
"author": "",
"license": "Apache 2.0"
}
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<plugin xmlns="http://apache.org/cordova/ns/plugins/1.0"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:rim="http://www.blackberry.com/ns/widgets"
id="cordova-plugin-media-capture-tests"
version="3.0.3">
<name>Cordova Media Capture Plugin Tests</name>
<license>Apache 2.0</license>
<dependency id="cordova-plugin-media" />
<js-module src="tests.js" name="tests">
</js-module>
</plugin>
/*
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
/* eslint-env jasmine */
/* global CaptureAudioOptions, CaptureImageOptions, CaptureVideoOptions, CaptureError */
/* global Media, MediaFile, MediaFileData, resolveLocalFileSystemURL */
exports.defineAutoTests = function () {
describe('Capture (navigator.device.capture)', function () {
it('capture.spec.1 should exist', function () {
expect(navigator.device).toBeDefined();
expect(navigator.device.capture).toBeDefined();
});
it('capture.spec.2 should have the correct properties ', function () {
expect(navigator.device.capture.supportedAudioModes).toBeDefined();
expect(navigator.device.capture.supportedImageModes).toBeDefined();
expect(navigator.device.capture.supportedVideoModes).toBeDefined();
});
it('capture.spec.3 should contain a captureAudio function', function () {
expect(navigator.device.capture.captureAudio).toBeDefined();
expect(typeof navigator.device.capture.captureAudio === 'function').toBe(true);
});
it('capture.spec.4 should contain a captureImage function', function () {
expect(navigator.device.capture.captureImage).toBeDefined();
expect(typeof navigator.device.capture.captureImage === 'function').toBe(true);
});
it('capture.spec.5 should contain a captureVideo function', function () {
expect(navigator.device.capture.captureVideo).toBeDefined();
expect(typeof navigator.device.capture.captureVideo === 'function').toBe(true);
});
describe('CaptureAudioOptions', function () {
it('capture.spec.6 CaptureAudioOptions constructor should exist', function () {
var options = new CaptureAudioOptions();
expect(options).toBeDefined();
expect(options.limit).toBeDefined();
expect(options.duration).toBeDefined();
});
});
describe('CaptureImageOptions', function () {
it('capture.spec.7 CaptureImageOptions constructor should exist', function () {
var options = new CaptureImageOptions();
expect(options).toBeDefined();
expect(options.limit).toBeDefined();
});
});
describe('CaptureVideoOptions', function () {
it('capture.spec.8 CaptureVideoOptions constructor should exist', function () {
var options = new CaptureVideoOptions();
expect(options).toBeDefined();
expect(options.limit).toBeDefined();
expect(options.duration).toBeDefined();
});
});
describe('CaptureError interface', function () {
it('capture.spec.9 CaptureError constants should be defined', function () {
expect(CaptureError.CAPTURE_INTERNAL_ERR).toBe(0);
expect(CaptureError.CAPTURE_APPLICATION_BUSY).toBe(1);
expect(CaptureError.CAPTURE_INVALID_ARGUMENT).toBe(2);
expect(CaptureError.CAPTURE_NO_MEDIA_FILES).toBe(3);
});
it('capture.spec.10 CaptureError properties should exist', function () {
var error = new CaptureError();
expect(error).toBeDefined();
expect(error.code).toBeDefined();
});
});
describe('MediaFileData', function () {
it('capture.spec.11 MediaFileData constructor should exist', function () {
var fileData = new MediaFileData();
expect(fileData).toBeDefined();
expect(fileData.bitrate).toBeDefined();
expect(fileData.codecs).toBeDefined();
expect(fileData.duration).toBeDefined();
expect(fileData.height).toBeDefined();
expect(fileData.width).toBeDefined();
});
});
describe('MediaFile', function () {
it('capture.spec.12 MediaFile constructor should exist', function () {
var fileData = new MediaFile();
expect(fileData).toBeDefined();
expect(fileData.name).toBeDefined();
expect(fileData.type).toBeDefined();
expect(fileData.lastModifiedDate).toBeDefined();
expect(fileData.size).toBeDefined();
});
});
});
};
/******************************************************************************/
/******************************************************************************/
/******************************************************************************/
exports.defineManualTests = function (contentEl, createActionButton) {
var pageStartTime = +new Date();
function log (value) {
document.getElementById('camera_status').textContent += (new Date() - pageStartTime) / 1000 + ': ' + value + '\n';
console.log(value);
}
function captureAudioWin (mediaFiles) {
var path = mediaFiles[0].fullPath;
log('Audio captured: ' + path);
var m = new Media(path);
m.play();
getFileMetadata(mediaFiles[0]);
}
function captureAudioFail (e) {
log('Error getting audio: ' + e.code);
}
function getAudio () {
clearStatus();
var options = { limit: 1, duration: 10 };
navigator.device.capture.captureAudio(captureAudioWin, captureAudioFail, options);
}
function captureImageWin (mediaFiles) {
var path = mediaFiles[0].fullPath;
// Necessary since windows doesn't allow file URLs for <img> elements
if (cordova.platformId === 'windows' || cordova.platformId === 'windows8' || cordova.platformId === 'browser') { // eslint-disable-line no-undef
path = mediaFiles[0].localURL;
}
log('Image captured: ' + path);
document.getElementById('camera_image').src = path;
}
function captureImagesWin (mediaFiles) {
var path = mediaFiles[0].fullPath;
// Necessary since windows doesn't allow file URLs for <img> elements
if (cordova.platformId === 'windows' || cordova.platformId === 'windows8' || cordova.platformId === 'browser') { // eslint-disable-line no-undef
path = mediaFiles[0].localURL;
}
var path2 = mediaFiles[1].fullPath;
// Necessary since windows doesn't allow file URLs for <img> elements
if (cordova.platformId === 'windows' || cordova.platformId === 'windows8' || cordova.platformId === 'browser') { // eslint-disable-line no-undef
path = mediaFiles[1].localURL;
}
var path3 = mediaFiles[2].fullPath;
// Necessary since windows doesn't allow file URLs for <img> elements
if (cordova.platformId === 'windows' || cordova.platformId === 'windows8' || cordova.platformId === 'browser') { // eslint-disable-line no-undef
path = mediaFiles[2].localURL;
}
log('Image captured: ' + path);
log('Image captured: ' + path2);
log('Image captured: ' + path3);
document.getElementById('camera_image').src = path;
document.getElementById('camera_image2').src = path2;
document.getElementById('camera_image3').src = path3;
}
function captureImageFail (e) {
log('Error getting image: ' + e.code);
}
function getImage () {
clearStatus();
var options = { limit: 1 };
navigator.device.capture.captureImage(captureImageWin, captureImageFail, options);
}
function getImages () {
clearStatus();
var options = { limit: 3 };
navigator.device.capture.captureImage(captureImagesWin, captureImageFail, options);
}
function captureVideoWin (mediaFiles) {
var path = mediaFiles[0].fullPath;
log('Video captured: ' + path);
// need to inject the video element into the html
// doesn't seem to work if you have a pre-existing video element and
// add in a source tag
var vid = document.createElement('video');
vid.id = 'theVideo';
vid.width = '320';
vid.height = '240';
vid.controls = 'true';
var source_vid = document.createElement('source');
source_vid.id = 'theSource';
source_vid.src = path;
vid.appendChild(source_vid);
document.getElementById('video_container').appendChild(vid);
getFileMetadata(mediaFiles[0]);
}
function getFileMetadata (mediaFile) {
mediaFile.getFormatData(getMetadataWin, getMetadataFail);
}
function getMetadataWin (metadata) {
var strMetadata =
'duration = ' + metadata.duration + '\n' +
'width = ' + metadata.width + '\n' +
'height = ' + metadata.height;
log(strMetadata);
}
function getMetadataFail (e) {
log('Error getting metadata: ' + e.code);
}
function captureVideoFail (e) {
log('Error getting video: ' + e.code);
}
function getVideo () {
clearStatus();
var options = { limit: 1, duration: 10 };
navigator.device.capture.captureVideo(captureVideoWin, captureVideoFail, options);
}
function permissionWasNotAllowed () {
log('Media has been captured. Have you forgotten to disallow camera for this app?');
}
function catchPermissionError (error) {
if (CaptureError.CAPTURE_PERMISSION_DENIED === error.code) {
log('Sucess: permission error has been detected!');
} else {
log('Error: another error with code: ' + error.code);
}
}
function getVideoPermissionError () {
var options = { limit: 1, duration: 10 };
navigator.device.capture.captureVideo(permissionWasNotAllowed, catchPermissionError, options);
}
function getImagePermissionError () {
var options = { limit: 1 };
navigator.device.capture.captureImage(permissionWasNotAllowed, catchPermissionError, options);
}
function resolveMediaFileURL (mediaFile, callback) {
resolveLocalFileSystemURL(mediaFile.localURL, function (entry) {
log('Resolved by URL: ' + mediaFile.localURL);
if (callback) callback();
}, function (err) {
log('Failed to resolve by URL: ' + mediaFile.localURL);
log('Error: ' + JSON.stringify(err));
if (callback) callback();
});
}
function resolveMediaFile (mediaFile, callback) {
resolveLocalFileSystemURL(mediaFile.fullPath, function (entry) {
log('Resolved by path: ' + mediaFile.fullPath);
if (callback) callback();
}, function (err) {
log('Failed to resolve by path: ' + mediaFile.fullPath);
log('Error: ' + JSON.stringify(err));
if (callback) callback();
});
}
function resolveVideo () {
clearStatus();
var options = { limit: 1, duration: 5 };
navigator.device.capture.captureVideo(function (mediaFiles) {
captureVideoWin(mediaFiles);
resolveMediaFile(mediaFiles[0], function () {
resolveMediaFileURL(mediaFiles[0]);
});
}, captureVideoFail, options);
}
function clearStatus () {
document.getElementById('camera_status').innerHTML = '';
document.getElementById('camera_image').src = 'about:blank';
document.getElementById('camera_image2').src = 'about:blank';
document.getElementById('camera_image3').src = 'about:blank';
}
/******************************************************************************/
contentEl.innerHTML = '<div id="info" style="white-space: pre-wrap">' +
'<b>Status:</b> <div id="camera_status"></div>' +
'img1: <img width="100" id="camera_image">' +
'img2: <img width="100" id="camera_image2">' +
'img3: <img width="100" id="camera_image3">' +
'video: <div id="video_container"></div>' +
'</div><div id="audio"></div>' +
'Expected result: Audio recorder will come up. Press record button to record for 10 seconds. Press Done. Status box will update with audio file and automatically play recording.' +
'<p/> <div id="image"></div>' +
'Expected result: Status box will update with image just taken.' +
'<p/> <div id="images"></div>' +
'Expected result: Status box will update with images just taken.' +
'<p/> <div id="video"></div>' +
'Expected result: Record 10 second video. Status box will update with video file that you can play.' +
'<p/> <div id="video_and_resolve"></div>' +
'Expected result: Record 5 second video. Status box will show that URL was resolved and video will get added at the bottom of the status box for playback.' +
'<p/> <div id="prohibited_camera_video"></div>' +
'Expected result (iOS only): camera picker and alert with message that camera access is prohibited are shown. The alert has 2 buttons: OK and Settings. By click on "OK" camera is hidden, by pressing Settings it shows privacy settings for the app' +
'<p/> <div id="prohibited_camera_image"></div>' +
'Expected result (iOS only): camera picker and alert with message that camera access is prohibited are shown. The alert has 2 buttons: OK and Settings. By click on "OK" camera is hidden, by pressing Settings it shows privacy settings for the app';
createActionButton('Capture 10 sec of audio and play', function () {
getAudio();
}, 'audio');
createActionButton('Capture 1 image', function () {
getImage();
}, 'image');
createActionButton('Capture 3 images', function () {
getImages();
}, 'images');
createActionButton('Capture 10 sec of video', function () {
getVideo();
}, 'video');
createActionButton('Capture 5 sec of video and resolve', function () {
resolveVideo();
}, 'video_and_resolve');
createActionButton('Disable access to Camera and click to capture video', function () {
getVideoPermissionError();
}, 'prohibited_camera_video');
createActionButton('Disable access to Camera and click to capture image', function () {
getImagePermissionError();
}, 'prohibited_camera_image');
};
// Type definitions for Apache Cordova MediaCapture plugin
// Project: https://github.com/apache/cordova-plugin-media-capture
// Definitions by: Microsoft Open Technologies Inc <http://msopentech.com>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
//
// Copyright (c) Microsoft Open Technologies Inc
// Licensed under the MIT license
interface Navigator {
device: Device;
}
interface Device {
capture: Capture;
}
/** This plugin provides access to the device's audio, image, and video capture capabilities. */
interface Capture {
/**
* Start the audio recorder application and return information about captured audio clip files.
* @param onSuccess Executes when the capture operation finishes with an array
* of MediaFile objects describing each captured audio clip file.
* @param onError Executes, if the user terminates the operation before an audio clip is captured,
* with a CaptureError object, featuring the CaptureError.CAPTURE_NO_MEDIA_FILES error code.
* @param options Encapsulates audio capture configuration options.
*/
captureAudio(
onSuccess: (mediaFiles: MediaFile[]) => void,
onError: (error: CaptureError) => void,
options?: AudioOptions): void ;
/**
* Start the camera application and return information about captured image files.
* @param onSuccess Executes when the capture operation finishes with an array
* of MediaFile objects describing each captured image clip file.
* @param onError Executes, if the user terminates the operation before an audio clip is captured,
* with a CaptureError object, featuring the CaptureError.CAPTURE_NO_MEDIA_FILES error code.
* @param options Encapsulates audio capture configuration options.
*/
captureImage(
onSuccess: (mediaFiles: MediaFile[]) => void,
onError: (error: CaptureError) => void,
options?: ImageOptions): void ;
/**
* Start the video recorder application and return information about captured video clip files.
* @param onSuccess Executes when the capture operation finishes with an array
* of MediaFile objects describing each captured video clip file.
* @param onError Executes, if the user terminates the operation before an audio clip is captured,
* with a CaptureError object, featuring the CaptureError.CAPTURE_NO_MEDIA_FILES error code.
* @param options Encapsulates audio capture configuration options.
*/
captureVideo(
onSuccess: (mediaFiles: MediaFile[]) => void,
onError: (error: CaptureError) => void,
options?: VideoOptions): void ;
/** The audio recording formats supported by the device. */
supportedAudioModes: ConfigurationData[];
/** The recording image sizes and formats supported by the device. */
supportedImageModes: ConfigurationData[];
/** The recording video resolutions and formats supported by the device. */
supportedVideoModes: ConfigurationData[];
}
/** Encapsulates properties of a media capture file. */
interface MediaFile {
/** The name of the file, without path information. */
name: string;
/** The full path of the file, including the name. */
fullPath: string;
/** The file's mime type */
type: string;
/** The date and time when the file was last modified. */
lastModifiedDate: Date;
/** The size of the file, in bytes. */
size: number;
/**
* Retrieves format information about the media capture file.
* @param successCallback Invoked with a MediaFileData object when successful.
* @param errorCallback Invoked if the attempt fails, this function.
*/
getFormatData(
successCallback: (data: MediaFileData) => void,
errorCallback?: () => void): void;
}
/** Encapsulates format information about a media file. */
interface MediaFileData {
/** The actual format of the audio and video content. */
codecs: string;
/** The average bitrate of the content. The value is zero for images. */
bitrate: number;
/** The height of the image or video in pixels. The value is zero for audio clips. */
height: number;
/** The width of the image or video in pixels. The value is zero for audio clips. */
width: number;
/** The length of the video or sound clip in seconds. The value is zero for images. */
duration: number;
}
/** Encapsulates the error code resulting from a failed media capture operation. */
interface CaptureError {
/**
* One of the pre-defined error codes listed below.
* CaptureError.CAPTURE_INTERNAL_ERR
* The camera or microphone failed to capture image or sound.
* CaptureError.CAPTURE_APPLICATION_BUSY
* The camera or audio capture application is currently serving another capture request.
* CaptureError.CAPTURE_INVALID_ARGUMENT
* Invalid use of the API (e.g., the value of limit is less than one).
* CaptureError.CAPTURE_NO_MEDIA_FILES
* The user exits the camera or audio capture application before capturing anything.
* CaptureError.CAPTURE_NOT_SUPPORTED
* The requested capture operation is not supported.
*/
code: number;
message: string;
}
declare var CaptureError: {
/** Constructor for CaptureError */
new (code: number, message: string): CaptureError;
CAPTURE_INTERNAL_ERR: number;
CAPTURE_APPLICATION_BUSY: number;
CAPTURE_INVALID_ARGUMENT: number;
CAPTURE_NO_MEDIA_FILES: number;
CAPTURE_NOT_SUPPORTED: number;
CAPTURE_PERMISSION_DENIED: number;
}
/** Encapsulates audio capture configuration options. */
interface AudioOptions {
/**
* The maximum number of audio clips the device's user can capture in a single
* capture operation. The value must be greater than or equal to 1.
*/
limit?: number;
/** The maximum duration of a audio clip, in seconds. */
duration?: number;
}
/** Encapsulates image capture configuration options. */
interface ImageOptions {
/**
* The maximum number of images the user can capture in a single capture operation.
* The value must be greater than or equal to 1 (defaults to 1).
*/
limit?: number;
}
/** Encapsulates video capture configuration options. */
interface VideoOptions {
/**
* The maximum number of video clips the device's user can capture in a single
* capture operation. The value must be greater than or equal to 1.
*/
limit?: number;
/** The maximum duration of a video clip, in seconds. */
duration?: number;
}
/** Encapsulates a set of media capture parameters that a device supports. */
interface ConfigurationData {
/** The ASCII-encoded lowercase string representing the media type. */
type: string;
/** The height of the image or video in pixels. The value is zero for sound clips. */
height: number;
/** The width of the image or video in pixels. The value is zero for sound clips. */
width: number;
}
\ No newline at end of file
...@@ -193,5 +193,13 @@ ...@@ -193,5 +193,13 @@
}, },
"is_top_level": true, "is_top_level": true,
"variables": {} "variables": {}
},
"cordova-plugin-media-capture": {
"source": {
"type": "registry",
"id": "cordova-plugin-media-capture@3.0.3"
},
"is_top_level": true,
"variables": {}
} }
} }
\ No newline at end of file
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
<div class="number"> <div class="number">
<span v-for="(item,index) in codeList" :key="index">{{ item }}</span> <span v-for="(item,index) in codeList" :key="index">{{ item }}</span>
</div> </div>
<h-button :disabled="lastTime!=0" class="button" @click="recordVdeo">记住了,开始录制<span v-if="lastTime!=0">({{ lastTime }}s)</span></h-button> <h-button :disabled="lastTime!=0" class="button" @click.native="recordVdeo">记住了,开始录制<span v-if="lastTime!=0">({{ lastTime }}s)</span></h-button>
</div> </div>
<div class="close" @click="downNum=false">X</div> <div class="close" @click="downNum=false">X</div>
</div> </div>
...@@ -76,7 +76,17 @@ export default { ...@@ -76,7 +76,17 @@ export default {
}, },
methods: { methods: {
recordVdeo () { // 开始录制 recordVdeo () { // 开始录制
let vm = this
let onSuccess = function (mediaFiles) {
// 遍历获取录制的文件(iOS 只支持一次录制一个视频)
for (let i = 0; i < mediaFiles.length; i++) {
alert('录制成功!\n\n' + '文件名:' + mediaFiles[i].name + '\n' + '大小:' + mediaFiles[i].size + '\n\n' + 'localURL地址:' + mediaFiles[i].localURL + '\n\n' + 'fullPath地址:' + mediaFiles[i].fullPath)
}
}
let onError = function (error) {
hlsPopup.showLongcenter('录制失败,请重新录制')
}
hlsUtil.captureVideo(onSuccess, onError)
}, },
succesCall () { // 录制成功后进行检测 succesCall () { // 录制成功后进行检测
let vm = this let vm = this
...@@ -173,7 +183,7 @@ export default { ...@@ -173,7 +183,7 @@ export default {
width:90%; width:90%;
height: 50px; height: 50px;
background-color: #0073eb; background-color: #0073eb;
color: #fff; color: #fff !important;
border-radius: 4px; border-radius: 4px;
} }
.down-content { .down-content {
......
...@@ -307,6 +307,17 @@ export default { ...@@ -307,6 +307,17 @@ export default {
} }
}, },
/**
* 录制视频
*/
captureVideo: function(onSuccess, onError){
if (typeof onSuccess === 'function' && typeof onError === 'function') {
navigator.device.capture.captureVideo(onSuccess, onError, {duration: 15});
} else{
window.hlsPopup.showLongBottom('参数有误!')
}
},
/** /**
* 拨打电话仅仅限制于手机 * 拨打电话仅仅限制于手机
* @param number * @param number
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment