/** * 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.annotation.TargetApi; import android.content.ContentValues; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Matrix; 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.Build; import android.os.Environment; import android.provider.MediaStore; import android.util.Base64; import android.util.Log; import android.view.Surface; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableNativeMap; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.text.SimpleDateFormat; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.annotation.Nullable; public class RCTCameraModule extends ReactContextBaseJavaModule implements MediaRecorder.OnInfoListener, LifecycleEventListener { private static final String TAG = "RCTCameraModule"; public static final int RCT_CAMERA_ASPECT_FILL = 0; public static final int RCT_CAMERA_ASPECT_FIT = 1; public static final int RCT_CAMERA_ASPECT_STRETCH = 2; public static final int RCT_CAMERA_CAPTURE_MODE_STILL = 0; public static final int RCT_CAMERA_CAPTURE_MODE_VIDEO = 1; public static final int RCT_CAMERA_CAPTURE_TARGET_MEMORY = 0; public static final int RCT_CAMERA_CAPTURE_TARGET_DISK = 1; public static final int RCT_CAMERA_CAPTURE_TARGET_CAMERA_ROLL = 2; public static final int RCT_CAMERA_CAPTURE_TARGET_TEMP = 3; public static final int RCT_CAMERA_ORIENTATION_AUTO = Integer.MAX_VALUE; public static final int RCT_CAMERA_ORIENTATION_PORTRAIT = Surface.ROTATION_0; public static final int RCT_CAMERA_ORIENTATION_PORTRAIT_UPSIDE_DOWN = Surface.ROTATION_180; public static final int RCT_CAMERA_ORIENTATION_LANDSCAPE_LEFT = Surface.ROTATION_90; public static final int RCT_CAMERA_ORIENTATION_LANDSCAPE_RIGHT = Surface.ROTATION_270; public static final int RCT_CAMERA_TYPE_FRONT = 1; public static final int RCT_CAMERA_TYPE_BACK = 2; public static final int RCT_CAMERA_FLASH_MODE_OFF = 0; public static final int RCT_CAMERA_FLASH_MODE_ON = 1; public static final int RCT_CAMERA_FLASH_MODE_AUTO = 2; 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); _reactContext.addLifecycleEventListener(this); } 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"; } @Nullable @Override public Map getConstants() { return Collections.unmodifiableMap(new HashMap() { { put("Aspect", getAspectConstants()); put("BarCodeType", getBarCodeConstants()); put("Type", getTypeConstants()); put("CaptureQuality", getCaptureQualityConstants()); put("CaptureMode", getCaptureModeConstants()); put("CaptureTarget", getCaptureTargetConstants()); put("Orientation", getOrientationConstants()); put("FlashMode", getFlashModeConstants()); put("TorchMode", getTorchModeConstants()); } private Map getAspectConstants() { return Collections.unmodifiableMap(new HashMap() { { put("stretch", RCT_CAMERA_ASPECT_STRETCH); put("fit", RCT_CAMERA_ASPECT_FIT); put("fill", RCT_CAMERA_ASPECT_FILL); } }); } private Map getBarCodeConstants() { return Collections.unmodifiableMap(new HashMap() { { // @TODO add barcode types } }); } private Map getTypeConstants() { return Collections.unmodifiableMap(new HashMap() { { put("front", RCT_CAMERA_TYPE_FRONT); put("back", RCT_CAMERA_TYPE_BACK); } }); } private Map getCaptureQualityConstants() { return Collections.unmodifiableMap(new HashMap() { { 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); } }); } private Map getCaptureModeConstants() { return Collections.unmodifiableMap(new HashMap() { { put("still", RCT_CAMERA_CAPTURE_MODE_STILL); put("video", RCT_CAMERA_CAPTURE_MODE_VIDEO); } }); } private Map getCaptureTargetConstants() { return Collections.unmodifiableMap(new HashMap() { { put("memory", RCT_CAMERA_CAPTURE_TARGET_MEMORY); put("disk", RCT_CAMERA_CAPTURE_TARGET_DISK); put("cameraRoll", RCT_CAMERA_CAPTURE_TARGET_CAMERA_ROLL); put("temp", RCT_CAMERA_CAPTURE_TARGET_TEMP); } }); } private Map getOrientationConstants() { return Collections.unmodifiableMap(new HashMap() { { put("auto", RCT_CAMERA_ORIENTATION_AUTO); put("landscapeLeft", RCT_CAMERA_ORIENTATION_LANDSCAPE_LEFT); put("landscapeRight", RCT_CAMERA_ORIENTATION_LANDSCAPE_RIGHT); put("portrait", RCT_CAMERA_ORIENTATION_PORTRAIT); put("portraitUpsideDown", RCT_CAMERA_ORIENTATION_PORTRAIT_UPSIDE_DOWN); } }); } private Map getFlashModeConstants() { return Collections.unmodifiableMap(new HashMap() { { put("off", RCT_CAMERA_FLASH_MODE_OFF); put("on", RCT_CAMERA_FLASH_MODE_ON); put("auto", RCT_CAMERA_FLASH_MODE_AUTO); } }); } private Map getTorchModeConstants() { return Collections.unmodifiableMap(new HashMap() { { put("off", RCT_CAMERA_TORCH_MODE_OFF); put("on", RCT_CAMERA_TORCH_MODE_ON); put("auto", RCT_CAMERA_TORCH_MODE_AUTO); } }); } }); } 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; } 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; } private byte[] mirrorImage(byte[] data) { ByteArrayInputStream inputStream = new ByteArrayInputStream(data); Bitmap photo = BitmapFactory.decodeStream(inputStream); Matrix m = new Matrix(); m.preScale(-1, 1); Bitmap mirroredImage = Bitmap.createBitmap(photo, 0, 0, photo.getWidth(), photo.getHeight(), m, false); byte[] result = null; try { result = compress(mirroredImage, 85); } catch (OutOfMemoryError e) { try { result = compress(mirroredImage, 70); } catch (OutOfMemoryError e2) { e.printStackTrace(); } } try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } return result; } private byte[] compress(Bitmap bitmap, int quality) throws OutOfMemoryError { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream); try { return outputStream.toByteArray(); } finally { try { outputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } @ReactMethod public void capture(final ReadableMap options, final Promise promise) { int orientation = options.hasKey("orientation") ? options.getInt("orientation") : RCTCamera.getInstance().getOrientation(); if (orientation == RCT_CAMERA_ORIENTATION_AUTO) { _sensorOrientationChecker.onResume(); _sensorOrientationChecker.registerOrientationListener(new RCTSensorOrientationListener() { @Override public void orientationEvent() { int deviceOrientation = _sensorOrientationChecker.getOrientation(); _sensorOrientationChecker.unregisterOrientationListener(); _sensorOrientationChecker.onPause(); captureWithOrientation(options, promise, deviceOrientation); } }); } else { captureWithOrientation(options, promise, orientation); } } 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); } if (options.hasKey("quality")) { RCTCamera.getInstance().setCaptureQuality(options.getInt("type"), options.getString("quality")); } final Boolean shouldMirror = options.hasKey("mirrorImage") && options.getBoolean("mirrorImage"); RCTCamera.getInstance().adjustCameraRotationToDeviceOrientation(options.getInt("type"), deviceOrientation); camera.takePicture(null, null, new Camera.PictureCallback() { @Override public void onPictureTaken(byte[] data, Camera camera) { if (shouldMirror) { data = mirrorImage(data); if (data == null) { promise.reject("Error mirroring image"); } } camera.stopPreview(); camera.startPreview(); WritableMap response = new WritableNativeMap(); switch (options.getInt("target")) { case RCT_CAMERA_CAPTURE_TARGET_MEMORY: String encoded = Base64.encodeToString(data, Base64.DEFAULT); response.putString("data", encoded); promise.resolve(response); break; 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: { File pictureFile = getOutputMediaFile(MEDIA_TYPE_IMAGE); if (pictureFile == null) { promise.reject("Error creating media file."); return; } 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: { File tempFile = getTempMediaFile(MEDIA_TYPE_IMAGE); if (tempFile == null) { promise.reject("Error creating media file."); return; } 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 Promise promise) { if (mRecordingPromise != null) { releaseMediaRecorder(); // release the MediaRecorder object promise.resolve("Finished recording."); } else { promise.resolve("Not recording."); } } @ReactMethod public void hasFlash(ReadableMap options, final Promise promise) { Camera camera = RCTCamera.getInstance().acquireCameraInstance(options.getInt("type")); if (null == camera) { promise.reject("No camera found."); return; } List flashModes = camera.getParameters().getSupportedFlashModes(); promise.resolve(null != flashModes && !flashModes.isEmpty()); } 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 (!storageDir.exists()) { if (!storageDir.mkdirs()) { Log.e(TAG, "failed to create directory:" + storageDir.getAbsolutePath()); return null; } } // Create a media file name String photoName = String.format("%s", new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date())); if (type == MEDIA_TYPE_IMAGE) { photoName = String.format("IMG_%s.jpg", photoName); } else if (type == MEDIA_TYPE_VIDEO) { photoName = String.format("VID_%s.mp4", photoName); } else { Log.e(TAG, "Unsupported media type:" + type); return null; } return new File(String.format("%s%s%s", storageDir.getPath(), File.separator, photoName)); } private File getTempMediaFile(int type) { try { String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); File outputDir = _reactContext.getCacheDir(); File outputFile; if (type == MEDIA_TYPE_IMAGE) { outputFile = File.createTempFile("IMG_" + timeStamp, ".jpg", outputDir); } else if (type == MEDIA_TYPE_VIDEO) { outputFile = File.createTempFile("VID_" + timeStamp, ".mp4", outputDir); } else { Log.e(TAG, "Unsupported media type:" + type); return null; } return outputFile; } catch (Exception e) { Log.e(TAG, e.getMessage()); return null; } } private void addToMediaStore(String path) { MediaScannerConnection.scanFile(_reactContext, new String[] { path }, null, null); } /** * LifecycleEventListener overrides */ @Override public void onHostResume() { // ... do nothing } @Override public void onHostPause() { // On pause, we stop any pending recording session if (mRecordingPromise != null) { releaseMediaRecorder(); } } @Override public void onHostDestroy() { // ... do nothing } }