mirror of
https://github.com/status-im/react-native-camera.git
synced 2026-09-01 14:11:06 +00:00
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:
committed by
Nicolas Charpentier
parent
1e092b5573
commit
e326d51a53
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user