Android support for recording video (#262)

* Initial commit with Android video support

* stopCapture now works

* Bug fixes and parameter enhancements.  README updated.

* Modified stopCapture parameter count to match iOS

* fixed promise bug on stopCapture

* Update RCTCameraModule.java

In Android preview and recording sizes are different, which can cause an error.  This fix detects the difference and chooses a recording resolution that matches.

* Update RCTCameraModule.java

* Update RCTCamera.java

Creating video functions in style/convention of existing

* Update RCTCameraModule.java

Use new functions for adjusting video capture size and quality

* Update RCTCameraModule.java

Fixes issue where file not video playable (readable) on older devices

* Update AndroidManifest.xml

Since we're reading and writing video and pictures, need permissions for it.

* Fixed upside down camera (on some platforms), and misc bugs and crashes

* Added camera-roll and capture to memory support, new options, and support for duration, filesize, and metadata

* To make merge nicer, temporarily reverting "Added camera-roll and capture to memory support, new options, and support for duration, filesize, and metadata"

This reverts commit 9ea1ad409c7e6121cf0197172e752b7523d4b092.

* Fixed merge & brought back all improvements from 9ea1ad4

* Fixed logic for video -> camera roll

* Updates

* Uncommenting setProfile

* Fix support for React Native 0.25

* Renamed Camera to index

* * Fix after merge android recording

* * Fixed android camera roll file saving
* Added recording to example

* * Android promise rejections with exceptions
* Fixed preview, video and photo sizes
* Android recording result in new, javascript object, format

* * Removed example.index.android.js as there is Example project

* * Readme for example

* don't force a specific codec

* always use cache dir

* * Using MediaScannerConnection instead of ACTION_MEDIA_SCANNER_SCAN_FILE intent

* * As described in https://github.com/lwansbrough/react-native-camera/pull/262#issuecomment-239622268:
- fixed video the wrong direction and recoder start fail at "low,medium" on the nexus 5 x
This commit is contained in:
Marc Johnson 2016-08-27 20:49:46 -05:00 committed by Nicolas Charpentier
parent 1e092b5573
commit e326d51a53
16 changed files with 513 additions and 140 deletions

3
.gitignore vendored
View File

@ -4,3 +4,6 @@ node_modules
ios/RCTCamera.xcodeproj/xcuserdata
ios/RCTCamera.xcodeproj/project.xcworkspace
.DS_Store
.idea
*.iml
android/build

View File

@ -34,9 +34,11 @@ const styles = StyleSheet.create({
bottomOverlay: {
bottom: 0,
backgroundColor: 'rgba(0,0,0,0.4)',
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
},
captureButton: {
flex: 1,
padding: 15,
backgroundColor: 'white',
borderRadius: 40,
@ -47,6 +49,9 @@ const styles = StyleSheet.create({
flashButton: {
padding: 5,
},
buttonsSpace: {
width: 10,
},
});
export default class Example extends React.Component {
@ -63,9 +68,12 @@ export default class Example extends React.Component {
orientation: Camera.constants.Orientation.auto,
flashMode: Camera.constants.FlashMode.auto,
},
isRecording: false
};
this.takePicture = this.takePicture.bind(this);
this.startRecording = this.startRecording.bind(this);
this.stopRecording = this.stopRecording.bind(this);
this.switchType = this.switchType.bind(this);
this.switchFlash = this.switchFlash.bind(this);
}
@ -78,6 +86,26 @@ export default class Example extends React.Component {
}
}
startRecording() {
if (this.camera) {
this.camera.capture({mode: Camera.constants.CaptureMode.video})
.then((data) => console.log(data))
.catch(err => console.error(err));
this.setState({
isRecording: true
});
}
}
stopRecording() {
if (this.camera) {
this.camera.stopCapture();
this.setState({
isRecording: false
});
}
}
switchType() {
let newType;
const { back, front } = Camera.constants.Type;
@ -181,14 +209,42 @@ export default class Example extends React.Component {
</TouchableOpacity>
</View>
<View style={[styles.overlay, styles.bottomOverlay]}>
<TouchableOpacity
style={styles.captureButton}
onPress={this.takePicture}
>
<Image
source={require('./assets/ic_photo_camera_36pt.png')}
/>
</TouchableOpacity>
{
!this.state.isRecording
&&
<TouchableOpacity
style={styles.captureButton}
onPress={this.takePicture}
>
<Image
source={require('./assets/ic_photo_camera_36pt.png')}
/>
</TouchableOpacity>
||
null
}
<View style={styles.buttonsSpace} />
{
!this.state.isRecording
&&
<TouchableOpacity
style={styles.captureButton}
onPress={this.startRecording}
>
<Image
source={require('./assets/ic_videocam_36pt.png')}
/>
</TouchableOpacity>
||
<TouchableOpacity
style={styles.captureButton}
onPress={this.stopRecording}
>
<Image
source={require('./assets/ic_stop_36pt.png')}
/>
</TouchableOpacity>
}
</View>
</View>
);

14
Example/README.md Normal file
View File

@ -0,0 +1,14 @@
#### `Run example`
From project root run through cli:
- `cd Example/`
- `npm install`
For Android:
- `adb reverse tcp:8081 tcp:8081` or in Dev Settings input COMPUTER_IP:8081 for debug server
- `react-native run-android`
For iOS build:
- Open Example.xcodeproj with XCode
- Change IP for jsCodeLocation in AppDelegate.m file
- Run from XCode

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 B

View File

@ -132,7 +132,7 @@ Values: `true` (default), `false` (Boolean)
*Applies to video capture mode only.* Specifies whether or not audio should be captured with the video.
#### `iOS` `captureMode`
#### `captureMode`
Values: `Camera.constants.CaptureMode.still` (default), `Camera.constants.CaptureMode.video`
@ -140,11 +140,11 @@ The type of capture that will be performed by the camera - either a still image
#### `captureTarget`
Values: `Camera.constants.CaptureTarget.cameraRoll` (default), `Camera.constants.CaptureTarget.disk`, `Camera.constants.CaptureTarget.temp`, ~~`Camera.constants.CaptureTarget.memory`~~ (deprecated),
Values: `Camera.constants.CaptureTarget.cameraRoll` (ios only default), `Camera.constants.CaptureTarget.disk` (android default), `Camera.constants.CaptureTarget.temp`, ~~`Camera.constants.CaptureTarget.memory`~~ (deprecated),
This property allows you to specify the target output of the captured image data. By default the image binary is sent back as a base 64 encoded string. The disk output has been shown to improve capture response time, so that is the recommended value.
#### `iOS` `captureQuality`
#### `captureQuality`
Values: `Camera.constants.CaptureQuality.high` or `"high"` (default), `Camera.constants.CaptureQuality.medium` or `"medium"`, `Camera.constants.CaptureQuality.low` or `"low"`, `Camera.constants.CaptureQuality.photo` or `"photo"`.
@ -169,7 +169,7 @@ The `orientation` property allows you to specify the current orientation of the
Values: `true` (default) or `false`
This property allows you to specify whether a sound is played on capture. It is currently android only, pending [a reasonable mute implementation](http://stackoverflow.com/questions/4401232/avfoundation-how-to-turn-off-the-shutter-sound-when-capturestillimageasynchrono) in iOS.
This property allows you to specify whether a shutter sound is played on capture. It is currently android only, pending [a reasonable mute implementation](http://stackoverflow.com/questions/4401232/avfoundation-how-to-turn-off-the-shutter-sound-when-capturestillimageasynchrono) in iOS.
#### `iOS` `onBarCodeRead`
@ -283,7 +283,7 @@ Ends the current capture session for video captures. Only applies when the curre
## Component static methods
#### `Camera.checkDeviceAuthorizationStatus(): Promise`
#### `iOS` `Camera.checkDeviceAuthorizationStatus(): Promise`
Exposes the native API for checking if the device has authorized access to the camera. Can be used to call before loading the Camera component to ensure proper UX. The promise will be fulfilled with `true` or `false` depending on whether the device is authorized.
@ -292,7 +292,7 @@ This component supports subviews, so if you wish to use the camera view as a bac
## Example
To see more of the `react-native-camera` in action, you can check out the `Example` folder.
To see more of the `react-native-camera` in action, you can check out the source in [Example](https://github.com/lwansbrough/react-native-camera/tree/master/Example) folder.
------------

View File

@ -1,5 +1,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.lwansbrough.RCTCamera">
<uses-permission android:name="android.permission.CAMERA" />
<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" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
</manifest>

View File

@ -5,6 +5,7 @@
package com.lwansbrough.RCTCamera;
import android.hardware.Camera;
import android.media.CamcorderProfile;
import java.util.HashMap;
import java.util.List;
@ -18,6 +19,7 @@ public class RCTCamera {
private final Map<Number, Camera> _cameras;
private int _orientation = -1;
private int _actualDeviceOrientation = 0;
private int _adjustedDeviceOrientation = 0;
public static RCTCamera getInstance() {
return ourInstance;
@ -61,77 +63,60 @@ public class RCTCamera {
return cameraInfo.previewHeight;
}
public Camera.Size getBestPreviewSize(int type, int width, int height)
{
Camera camera = _cameras.get(type);
Camera.Size result = null;
if(camera == null) {
return null;
}
Camera.Parameters params = camera.getParameters();
for (Camera.Size size : params.getSupportedPreviewSizes()) {
if (size.width <= width && size.height <= height) {
if (result == null) {
result = size;
} else {
int resultArea = result.width * result.height;
int newArea = size.width * size.height;
public Camera.Size getBestSize(List<Camera.Size> supportedSizes, int maxWidth, int maxHeight) {
Camera.Size bestSize = null;
for (Camera.Size size : supportedSizes) {
if (size.width > maxWidth || size.height > maxHeight) {
continue;
}
if (newArea > resultArea) {
result = size;
}
}
if (bestSize == null) {
bestSize = size;
continue;
}
int resultArea = bestSize.width * bestSize.height;
int newArea = size.width * size.height;
if (newArea > resultArea) {
bestSize = size;
}
}
return result;
return bestSize;
}
public Camera.Size getBestPictureSize(int type, int width, int height)
{
Camera camera = _cameras.get(type);
Camera.Size result = null;
if(camera == null) {
return null;
}
Camera.Parameters params = camera.getParameters();
for (Camera.Size size : params.getSupportedPictureSizes()) {
if (size.width <= width && size.height <= height) {
if (result == null) {
result = size;
} else {
int resultArea = result.width * result.height;
int newArea = size.width * size.height;
private Camera.Size getSmallestSize(List<Camera.Size> supportedSizes) {
Camera.Size smallestSize = null;
for (Camera.Size size : supportedSizes) {
if (smallestSize == null) {
smallestSize = size;
continue;
}
if (newArea > resultArea) {
result = size;
}
}
int resultArea = smallestSize.width * smallestSize.height;
int newArea = size.width * size.height;
if (newArea < resultArea) {
smallestSize = size;
}
}
return result;
return smallestSize;
}
public Camera.Size getSmallestPictureSize(int type)
{
Camera camera = _cameras.get(type);
Camera.Size result = null;
if(camera == null) {
return null;
}
private List<Camera.Size> getSupportedVideoSizes(Camera camera) {
Camera.Parameters params = camera.getParameters();
for (Camera.Size size : params.getSupportedPictureSizes()) {
if (result == null) {
result = size;
} else {
int resultArea = result.width * result.height;
int newArea = size.width * size.height;
if (newArea < resultArea) {
result = size;
}
}
// defer to preview instead of params.getSupportedVideoSizes() http://bit.ly/1rxOsq0
// but prefer SupportedVideoSizes!
List<Camera.Size> sizes = params.getSupportedVideoSizes();
if (sizes != null) {
return sizes;
}
return result;
// Video sizes may be null, which indicates that all the supported
// preview sizes are supported for video recording.
return params.getSupportedPreviewSizes();
}
public int getOrientation() {
@ -147,6 +132,18 @@ public class RCTCamera {
adjustPreviewLayout(RCTCameraModule.RCT_CAMERA_TYPE_BACK);
}
public int getActualDeviceOrientation() {
return _actualDeviceOrientation;
}
public void setAdjustedDeviceOrientation(int orientation) {
_adjustedDeviceOrientation = orientation;
}
public int getAdjustedDeviceOrientation() {
return _adjustedDeviceOrientation;
}
public void setActualDeviceOrientation(int actualDeviceOrientation) {
_actualDeviceOrientation = actualDeviceOrientation;
adjustPreviewLayout(RCTCameraModule.RCT_CAMERA_TYPE_FRONT);
@ -155,23 +152,22 @@ public class RCTCamera {
public void setCaptureQuality(int cameraType, String captureQuality) {
Camera camera = _cameras.get(cameraType);
if (null == camera) {
if (camera == null) {
return;
}
Camera.Parameters parameters = camera.getParameters();
Camera.Size pictureSize = null;
switch (captureQuality) {
case "low":
pictureSize = getSmallestPictureSize(cameraType); // select the lowest res
case RCTCameraModule.RCT_CAMERA_CAPTURE_QUALITY_LOW:
pictureSize = getSmallestSize(parameters.getSupportedPictureSizes());
break;
case "medium":
case RCTCameraModule.RCT_CAMERA_CAPTURE_QUALITY_MEDIUM:
List<Camera.Size> sizes = parameters.getSupportedPictureSizes();
pictureSize = sizes.get(sizes.size() / 2);
break;
case "high":
pictureSize = getBestPictureSize(cameraType, Integer.MAX_VALUE, Integer.MAX_VALUE); // select the highest res
break;
case RCTCameraModule.RCT_CAMERA_CAPTURE_QUALITY_HIGH:
pictureSize = getBestSize(parameters.getSupportedPictureSizes(), Integer.MAX_VALUE, Integer.MAX_VALUE);
}
if (pictureSize != null) {
@ -180,6 +176,41 @@ public class RCTCamera {
}
}
public CamcorderProfile setCaptureVideoQuality(int cameraType, String captureQuality) {
Camera camera = _cameras.get(cameraType);
if (camera == null) {
return null;
}
Camera.Size videoSize = null;
CamcorderProfile cm = null;
switch (captureQuality) {
case RCTCameraModule.RCT_CAMERA_CAPTURE_QUALITY_LOW:
videoSize = getSmallestSize(getSupportedVideoSizes(camera));
cm = CamcorderProfile.get(_cameraTypeToIndex.get(cameraType), CamcorderProfile.QUALITY_480P);
break;
case RCTCameraModule.RCT_CAMERA_CAPTURE_QUALITY_MEDIUM:
List<Camera.Size> sizes = getSupportedVideoSizes(camera);
videoSize = sizes.get(sizes.size() / 2);
cm = CamcorderProfile.get(_cameraTypeToIndex.get(cameraType), CamcorderProfile.QUALITY_720P);
break;
case RCTCameraModule.RCT_CAMERA_CAPTURE_QUALITY_HIGH:
videoSize = getBestSize(getSupportedVideoSizes(camera), Integer.MAX_VALUE, Integer.MAX_VALUE);
cm = CamcorderProfile.get(_cameraTypeToIndex.get(cameraType), CamcorderProfile.QUALITY_HIGH);
}
if (cm == null){
return null;
}
if (videoSize != null) {
cm.videoFrameHeight = videoSize.height;
cm.videoFrameWidth = videoSize.width;
}
return cm;
}
public void setTorchMode(int cameraType, int torchMode) {
Camera camera = _cameras.get(cameraType);
if (null == camera) {
@ -230,8 +261,7 @@ public class RCTCamera {
}
}
public void adjustCameraRotationToDeviceOrientation(int type, int deviceOrientation)
{
public void adjustCameraRotationToDeviceOrientation(int type, int deviceOrientation) {
Camera camera = _cameras.get(type);
if (null == camera) {
return;
@ -256,7 +286,7 @@ public class RCTCamera {
}
}
private void adjustPreviewLayout(int type) {
public void adjustPreviewLayout(int type) {
Camera camera = _cameras.get(type);
if (null == camera) {
return;
@ -276,6 +306,7 @@ public class RCTCamera {
cameraInfo.rotation = rotation;
// TODO: take in account the _orientation prop
setAdjustedDeviceOrientation(displayRotation);
camera.setDisplayOrientation(displayRotation);
Camera.Parameters parameters = camera.getParameters();
@ -283,7 +314,7 @@ public class RCTCamera {
// set preview size
// defaults to highest resolution available
Camera.Size optimalPreviewSize = getBestPreviewSize(type, Integer.MAX_VALUE, Integer.MAX_VALUE);
Camera.Size optimalPreviewSize = getBestSize(parameters.getSupportedPreviewSizes(), Integer.MAX_VALUE, Integer.MAX_VALUE);
int width = optimalPreviewSize.width;
int height = optimalPreviewSize.height;

View File

@ -1,13 +1,16 @@
/**
* Created by Fabrice Armisen (farmisen@gmail.com) on 1/4/16.
* Android video recording support by Marc Johnson (me@marc.mn) 4/2016
*/
package com.lwansbrough.RCTCamera;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.content.ContentValues;
import android.hardware.Camera;
import android.media.CamcorderProfile;
import android.media.MediaActionSound;
import android.media.MediaRecorder;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.Environment;
import android.provider.MediaStore;
@ -25,7 +28,7 @@ import java.util.HashMap;
import java.util.Map;
import java.util.List;
public class RCTCameraModule extends ReactContextBaseJavaModule {
public class RCTCameraModule extends ReactContextBaseJavaModule implements MediaRecorder.OnInfoListener {
private static final String TAG = "RCTCameraModule";
public static final int RCT_CAMERA_ASPECT_FILL = 0;
@ -50,18 +53,37 @@ public class RCTCameraModule extends ReactContextBaseJavaModule {
public static final int RCT_CAMERA_TORCH_MODE_OFF = 0;
public static final int RCT_CAMERA_TORCH_MODE_ON = 1;
public static final int RCT_CAMERA_TORCH_MODE_AUTO = 2;
public static final String RCT_CAMERA_CAPTURE_QUALITY_HIGH = "high";
public static final String RCT_CAMERA_CAPTURE_QUALITY_MEDIUM = "medium";
public static final String RCT_CAMERA_CAPTURE_QUALITY_LOW = "low";
public static final int MEDIA_TYPE_IMAGE = 1;
public static final int MEDIA_TYPE_VIDEO = 2;
private final ReactApplicationContext _reactContext;
private RCTSensorOrientationChecker _sensorOrientationChecker;
private MediaRecorder mMediaRecorder = new MediaRecorder();
private long MRStartTime;
private File mVideoFile;
private Camera mCamera = null;
private Promise mRecordingPromise = null;
private ReadableMap mRecordingOptions;
public RCTCameraModule(ReactApplicationContext reactContext) {
super(reactContext);
_reactContext = reactContext;
_sensorOrientationChecker = new RCTSensorOrientationChecker(_reactContext);
}
public void onInfo(MediaRecorder mr, int what, int extra) {
if ( what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED ||
what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) {
if (mRecordingPromise != null) {
releaseMediaRecorder(); // release the MediaRecorder object and resolve promise
}
}
}
@Override
public String getName() {
return "RCTCameraModule";
@ -113,10 +135,10 @@ public class RCTCameraModule extends ReactContextBaseJavaModule {
private Map<String, Object> getCaptureQualityConstants() {
return Collections.unmodifiableMap(new HashMap<String, Object>() {
{
put("low", "low");
put("medium", "medium");
put("high", "high");
put("photo","high");
put("low", RCT_CAMERA_CAPTURE_QUALITY_LOW);
put("medium", RCT_CAMERA_CAPTURE_QUALITY_MEDIUM);
put("high", RCT_CAMERA_CAPTURE_QUALITY_HIGH);
put("photo", RCT_CAMERA_CAPTURE_QUALITY_HIGH);
}
});
}
@ -175,6 +197,201 @@ public class RCTCameraModule extends ReactContextBaseJavaModule {
});
}
private Throwable prepareMediaRecorder(ReadableMap options) {
CamcorderProfile cm = RCTCamera.getInstance().setCaptureVideoQuality(options.getInt("type"), options.getString("quality"));
// Attach callback to handle maxDuration (@see onInfo method in this file)
mMediaRecorder.setOnInfoListener(this);
mMediaRecorder.setCamera(mCamera);
mCamera.unlock(); // make available for mediarecorder
// Set AV sources
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
mMediaRecorder.setOrientationHint(RCTCamera.getInstance().getAdjustedDeviceOrientation());
if (cm == null) {
return new RuntimeException("CamcorderProfile not found in prepareMediaRecorder.");
}
cm.fileFormat = MediaRecorder.OutputFormat.MPEG_4;
mMediaRecorder.setProfile(cm);
mVideoFile = null;
switch (options.getInt("target")) {
case RCT_CAMERA_CAPTURE_TARGET_MEMORY:
mVideoFile = getTempMediaFile(MEDIA_TYPE_VIDEO); // temporarily
break;
case RCT_CAMERA_CAPTURE_TARGET_CAMERA_ROLL:
mVideoFile = getOutputCameraRollFile(MEDIA_TYPE_VIDEO);
break;
case RCT_CAMERA_CAPTURE_TARGET_TEMP:
mVideoFile = getTempMediaFile(MEDIA_TYPE_VIDEO);
break;
default:
case RCT_CAMERA_CAPTURE_TARGET_DISK:
mVideoFile = getOutputMediaFile(MEDIA_TYPE_VIDEO);
break;
}
if (mVideoFile == null) {
return new RuntimeException("Error while preparing output file in prepareMediaRecorder.");
}
mMediaRecorder.setOutputFile(mVideoFile.getPath());
if (options.hasKey("totalSeconds")) {
int totalSeconds = options.getInt("totalSeconds");
mMediaRecorder.setMaxDuration(totalSeconds * 1000);
}
if (options.hasKey("maxFileSize")) {
int maxFileSize = options.getInt("maxFileSize");
mMediaRecorder.setMaxFileSize(maxFileSize);
}
try {
mMediaRecorder.prepare();
} catch (Exception ex) {
Log.e(TAG, "Media recorder prepare error.", ex);
releaseMediaRecorder();
return ex;
}
return null;
}
@ReactMethod
private void record(final ReadableMap options, final Promise promise) {
if (mRecordingPromise != null) {
return;
}
mCamera = RCTCamera.getInstance().acquireCameraInstance(options.getInt("type"));
if (mCamera == null) {
promise.reject(new RuntimeException("No camera found."));
return;
}
Throwable prepareError = prepareMediaRecorder(options);
if (prepareError != null) {
promise.reject(prepareError);
return;
}
try {
mMediaRecorder.start();
MRStartTime = System.currentTimeMillis();
mRecordingOptions = options;
mRecordingPromise = promise; // only got here if mediaRecorder started
} catch (Exception ex) {
Log.e(TAG, "Media recorder start error.", ex);
promise.reject(ex);
}
}
private void releaseMediaRecorder() {
// Must record at least a second or MediaRecorder throws exceptions on some platforms
long duration = System.currentTimeMillis() - MRStartTime;
if (duration < 1500) {
try {
Thread.sleep(1500 - duration);
} catch(InterruptedException ex) {
Log.e(TAG, "releaseMediaRecorder thread sleep error.", ex);
}
}
try {
mMediaRecorder.stop(); // stop the recording
} catch (RuntimeException ex) {
Log.e(TAG, "Media recorder stop error.", ex);
}
mMediaRecorder.reset(); // clear recorder configuration
if (mCamera != null) {
mCamera.lock(); // relock camera for later use since we unlocked it
}
if (mRecordingPromise == null) {
return;
}
File f = new File(mVideoFile.getPath());
if (!f.exists()) {
mRecordingPromise.reject(new RuntimeException("There is nothing recorded."));
mRecordingPromise = null;
return;
}
f.setReadable(true, false); // so mediaplayer can play it
f.setWritable(true, false); // so can clean it up
WritableMap response = new WritableNativeMap();
switch (mRecordingOptions.getInt("target")) {
case RCT_CAMERA_CAPTURE_TARGET_MEMORY:
byte[] encoded = convertFileToByteArray(mVideoFile);
response.putString("data", new String(encoded, Base64.DEFAULT));
mRecordingPromise.resolve(response);
f.delete();
break;
case RCT_CAMERA_CAPTURE_TARGET_CAMERA_ROLL:
ContentValues values = new ContentValues();
values.put(MediaStore.Video.Media.DATA, mVideoFile.getPath());
values.put(MediaStore.Video.Media.TITLE, mRecordingOptions.hasKey("title") ? mRecordingOptions.getString("title") : "video");
if (mRecordingOptions.hasKey("description")) {
values.put(MediaStore.Video.Media.DESCRIPTION, mRecordingOptions.hasKey("description"));
}
if (mRecordingOptions.hasKey("latitude")) {
values.put(MediaStore.Video.Media.LATITUDE, mRecordingOptions.getString("latitude"));
}
if (mRecordingOptions.hasKey("longitude")) {
values.put(MediaStore.Video.Media.LONGITUDE, mRecordingOptions.getString("longitude"));
}
values.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4");
_reactContext.getContentResolver().insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values);
addToMediaStore(mVideoFile.getAbsolutePath());
response.putString("path", Uri.fromFile(mVideoFile).toString());
mRecordingPromise.resolve(response);
break;
case RCT_CAMERA_CAPTURE_TARGET_TEMP:
case RCT_CAMERA_CAPTURE_TARGET_DISK:
response.putString("path", Uri.fromFile(mVideoFile).toString());
mRecordingPromise.resolve(response);
}
mRecordingPromise = null;
}
public static byte[] convertFileToByteArray(File f)
{
byte[] byteArray = null;
try
{
InputStream inputStream = new FileInputStream(f);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] b = new byte[1024*8];
int bytesRead;
while ((bytesRead = inputStream.read(b)) != -1) {
bos.write(b, 0, bytesRead);
}
byteArray = bos.toByteArray();
}
catch (IOException e)
{
e.printStackTrace();
}
return byteArray;
}
@ReactMethod
public void capture(final ReadableMap options, final Promise promise) {
int orientation = options.hasKey("orientation") ? options.getInt("orientation") : RCTCamera.getInstance().getOrientation();
@ -194,13 +411,20 @@ public class RCTCameraModule extends ReactContextBaseJavaModule {
}
}
public void captureWithOrientation(final ReadableMap options, final Promise promise, int deviceOrientation) {
private void captureWithOrientation(final ReadableMap options, final Promise promise, int deviceOrientation) {
Camera camera = RCTCamera.getInstance().acquireCameraInstance(options.getInt("type"));
if (null == camera) {
promise.reject("No camera found.");
return;
}
if (options.getInt("mode") == RCT_CAMERA_CAPTURE_MODE_VIDEO) {
record(options, promise);
return;
}
RCTCamera.getInstance().setCaptureQuality(options.getInt("type"), options.getString("quality"));
if (options.hasKey("playSoundOnCapture") && options.getBoolean("playSoundOnCapture")) {
MediaActionSound sound = new MediaActionSound();
sound.play(MediaActionSound.SHUTTER_CLICK);
@ -223,63 +447,71 @@ public class RCTCameraModule extends ReactContextBaseJavaModule {
response.putString("data", encoded);
promise.resolve(response);
break;
case RCT_CAMERA_CAPTURE_TARGET_CAMERA_ROLL:
BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, bitmapOptions);
String url = MediaStore.Images.Media.insertImage(
_reactContext.getContentResolver(),
bitmap, options.getString("title"),
options.getString("description"));
response.putString("path", url);
case RCT_CAMERA_CAPTURE_TARGET_CAMERA_ROLL: {
File cameraRollFile = getOutputCameraRollFile(MEDIA_TYPE_IMAGE);
if (cameraRollFile == null) {
promise.reject("Error creating media file.");
return;
}
Throwable error = writeDataToFile(data, cameraRollFile);
if (error != null) {
promise.reject(error);
return;
}
addToMediaStore(cameraRollFile.getAbsolutePath());
response.putString("path", Uri.fromFile(cameraRollFile).toString());
promise.resolve(response);
break;
case RCT_CAMERA_CAPTURE_TARGET_DISK:
}
case RCT_CAMERA_CAPTURE_TARGET_DISK: {
File pictureFile = getOutputMediaFile(MEDIA_TYPE_IMAGE);
if (pictureFile == null) {
promise.reject("Error creating media file.");
return;
}
try {
FileOutputStream fos = new FileOutputStream(pictureFile);
fos.write(data);
fos.close();
} catch (FileNotFoundException e) {
promise.reject("File not found: " + e.getMessage());
} catch (IOException e) {
promise.reject("Error accessing file: " + e.getMessage());
Throwable error = writeDataToFile(data, pictureFile);
if (error != null) {
promise.reject(error);
return;
}
addToMediaStore(pictureFile.getAbsolutePath());
response.putString("path", Uri.fromFile(pictureFile).toString());
promise.resolve(response);
break;
case RCT_CAMERA_CAPTURE_TARGET_TEMP:
}
case RCT_CAMERA_CAPTURE_TARGET_TEMP: {
File tempFile = getTempMediaFile(MEDIA_TYPE_IMAGE);
if (tempFile == null) {
promise.reject("Error creating media file.");
return;
}
try {
FileOutputStream fos = new FileOutputStream(tempFile);
fos.write(data);
fos.close();
} catch (FileNotFoundException e) {
promise.reject("File not found: " + e.getMessage());
} catch (IOException e) {
promise.reject("Error accessing file: " + e.getMessage());
Throwable error = writeDataToFile(data, tempFile);
if (error != null) {
promise.reject(error);
}
response.putString("path", Uri.fromFile(tempFile).toString());
promise.resolve(response);
break;
}
}
}
});
}
@ReactMethod
public void stopCapture(final ReadableMap options, final Promise promise) {
// TODO: implement video capture
public void stopCapture(final Promise promise) {
if (mRecordingPromise != null) {
releaseMediaRecorder(); // release the MediaRecorder object
promise.resolve("Finished recording.");
} else {
promise.resolve("Not recording.");
}
}
@ReactMethod
@ -293,34 +525,57 @@ public class RCTCameraModule extends ReactContextBaseJavaModule {
promise.resolve(null != flashModes && !flashModes.isEmpty());
}
private File getOutputMediaFile(int type) {
File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES), "RCTCameraModule");
private Throwable writeDataToFile(byte[] data, File file) {
try {
FileOutputStream fos = new FileOutputStream(file);
fos.write(data);
fos.close();
} catch (FileNotFoundException e) {
return e;
} catch (IOException e) {
return e;
}
return null;
}
private File getOutputMediaFile(int type) {
return getOutputFile(
type,
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
);
}
private File getOutputCameraRollFile(int type) {
return getOutputFile(
type,
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)
);
}
private File getOutputFile(int type, File storageDir) {
// Create the storage directory if it does not exist
if (!mediaStorageDir.exists()) {
if (!mediaStorageDir.mkdirs()) {
Log.e(TAG, "failed to create directory:" + mediaStorageDir.getAbsolutePath());
if (!storageDir.exists()) {
if (!storageDir.mkdirs()) {
Log.e(TAG, "failed to create directory:" + storageDir.getAbsolutePath());
return null;
}
}
// Create a media file name
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
File mediaFile;
String photoName = String.format("%s", new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()));
if (type == MEDIA_TYPE_IMAGE) {
mediaFile = new File(mediaStorageDir.getPath() + File.separator +
"IMG_" + timeStamp + ".jpg");
photoName = String.format("IMG_%s.jpg", photoName);
} else if (type == MEDIA_TYPE_VIDEO) {
mediaFile = new File(mediaStorageDir.getPath() + File.separator +
"VID_" + timeStamp + ".mp4");
photoName = String.format("VID_%s.mp4", photoName);
} else {
Log.e(TAG, "Unsupported media type:" + type);
return null;
}
return mediaFile;
}
return new File(String.format("%s%s%s", storageDir.getPath(), File.separator, photoName));
}
private File getTempMediaFile(int type) {
try {
@ -342,4 +597,8 @@ public class RCTCameraModule extends ReactContextBaseJavaModule {
return null;
}
}
private void addToMediaStore(String path) {
MediaScannerConnection.scanFile(_reactContext, new String[] { path }, null, null);
}
}

View File

@ -5,7 +5,6 @@
package com.lwansbrough.RCTCamera;
import android.content.Context;
import android.graphics.*;
import android.hardware.SensorManager;
import android.view.OrientationEventListener;
import android.view.ViewGroup;
@ -65,6 +64,7 @@ public class RCTCameraView extends ViewGroup {
public void setCameraType(final int type) {
if (null != this._viewFinder) {
this._viewFinder.setCameraType(type);
RCTCamera.getInstance().adjustPreviewLayout(type);
} else {
_viewFinder = new RCTCameraViewFinder(_context, type);
if (-1 != this._flashMode) {

View File

@ -45,7 +45,7 @@ class RCTCameraViewFinder extends TextureView implements TextureView.SurfaceText
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
}
public double getRatio() {
public double getRatio() {
int width = RCTCamera.getInstance().getPreviewWidth(this._cameraType);
int height = RCTCamera.getInstance().getPreviewHeight(this._cameraType);
return ((float) width) / ((float) height);
@ -102,7 +102,11 @@ class RCTCameraViewFinder extends TextureView implements TextureView.SurfaceText
}
// set picture size
// defaults to max available size
Camera.Size optimalPictureSize = RCTCamera.getInstance().getBestPictureSize(_cameraType, Integer.MAX_VALUE, Integer.MAX_VALUE);
Camera.Size optimalPictureSize = RCTCamera.getInstance().getBestSize(
parameters.getSupportedPictureSizes(),
Integer.MAX_VALUE,
Integer.MAX_VALUE
);
parameters.setPictureSize(optimalPictureSize.width, optimalPictureSize.height);
_camera.setParameters(parameters);

View File

@ -198,9 +198,10 @@ export default class Camera extends Component {
stopCapture() {
if (this.state.isRecording) {
CameraManager.stopCapture();
this.setState({ isRecording: false });
return CameraManager.stopCapture();
}
return Promise.resolve("Not Recording.");
}
getFOV() {