Minor Android MediaRecorder, sizing, and file improvements (#477)

Android MediaRecorder:
  - Most importantly, call Camera.unlock() before setting the camera on the
    MediaRecorder instance, and release() not just reset() when releasing the MediaRecorder
    instance!
  - Add comments and notes for preparing and releasing MediaRecorder instance.
  - Add onError callback for errors during recording session.

RCTCameraViewManager, RCTCamera, RCTCameraViewFinder, RCTCameraView:
  - Implement setCaptureMode, preparing camera based on captureMode. Currently, the only step that
    needs to be taken here is setting the recording hint for videos.
  - Handle setting _captureMode instance variable where applicable.

Sizing
  - Determine ViewFinder supported sizes based on actual captureMode (i.e., get supported picture
    sizes when in still capture mode, and get supported video sizes when in video capture mode).

Output files:
  - Get appropriate external storage public directory based on media type (image or video).
  - Minor variable renaming to indicate that both images or videos can be saved.

README:
  - Update captureTarget to indicate that cameraRoll is the actual default for both systems.
  - Small clarification for output data type for deprecated memory captureTarget output.
This commit is contained in:
abrahambotros
2016-11-17 22:25:51 -08:00
committed by Zack Story
parent fb3b12b4f1
commit b16ec4ab75
6 changed files with 153 additions and 36 deletions
@@ -48,7 +48,7 @@ import java.util.Map;
import javax.annotation.Nullable;
public class RCTCameraModule extends ReactContextBaseJavaModule
implements MediaRecorder.OnInfoListener, LifecycleEventListener {
implements MediaRecorder.OnInfoListener, MediaRecorder.OnErrorListener, LifecycleEventListener {
private static final String TAG = "RCTCameraModule";
public static final int RCT_CAMERA_ASPECT_FILL = 0;
@@ -82,7 +82,7 @@ public class RCTCameraModule extends ReactContextBaseJavaModule
private static ReactApplicationContext _reactContext;
private RCTSensorOrientationChecker _sensorOrientationChecker;
private MediaRecorder mMediaRecorder = new MediaRecorder();
private MediaRecorder mMediaRecorder;
private long MRStartTime;
private File mVideoFile;
private Camera mCamera = null;
@@ -100,6 +100,16 @@ public class RCTCameraModule extends ReactContextBaseJavaModule
return _reactContext;
}
/**
* Callback invoked on new MediaRecorder info.
*
* See https://developer.android.com/reference/android/media/MediaRecorder.OnInfoListener.html
* for more information.
*
* @param mr MediaRecorder instance for which this callback is being invoked.
* @param what Type of info we have received.
* @param extra Extra code, specific to the info type.
*/
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) {
@@ -109,6 +119,25 @@ public class RCTCameraModule extends ReactContextBaseJavaModule
}
}
/**
* Callback invoked when a MediaRecorder instance encounters an error while recording.
*
* See https://developer.android.com/reference/android/media/MediaRecorder.OnErrorListener.html
* for more information.
*
* @param mr MediaRecorder instance for which this callback is being invoked.
* @param what Type of error that has occurred.
* @param extra Extra code, specific to the error type.
*/
public void onError(MediaRecorder mr, int what, int extra) {
// On any error, release the MediaRecorder object and resolve promise. In particular, this
// prevents leaving the camera in an unrecoverable state if we crash in the middle of
// recording.
if (mRecordingPromise != null) {
releaseMediaRecorder();
}
}
@Override
public String getName() {
return "RCTCameraModule";
@@ -222,28 +251,49 @@ public class RCTCameraModule extends ReactContextBaseJavaModule
});
}
/**
* Prepare media recorder for video capture.
*
* See "Capturing Videos" at https://developer.android.com/guide/topics/media/camera.html for
* a guideline of steps and more information in general.
*
* @param options Options.
* @return Throwable; null if no errors.
*/
private Throwable prepareMediaRecorder(ReadableMap options) {
// Prepare CamcorderProfile instance, setting essential 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.");
}
// Unlock camera to make available for MediaRecorder. Note that this statement must be
// executed before calling setCamera when configuring the MediaRecorder instance.
mCamera.unlock();
// Create new MediaRecorder instance.
mMediaRecorder = new MediaRecorder();
// Attach callback to handle maxDuration (@see onInfo method in this file).
mMediaRecorder.setOnInfoListener(this);
// Attach error listener (@see onError method in this file).
mMediaRecorder.setOnErrorListener(this);
// Set camera.
mMediaRecorder.setCamera(mCamera);
// Set AV sources.
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
// Adjust for orientation.
mMediaRecorder.setOrientationHint(RCTCamera.getInstance().getAdjustedDeviceOrientation());
// Set video output format and encoding using CamcorderProfile.
cm.fileFormat = MediaRecorder.OutputFormat.MPEG_4;
mMediaRecorder.setProfile(cm);
// Set video output file.
mVideoFile = null;
switch (options.getInt("target")) {
case RCT_CAMERA_CAPTURE_TARGET_MEMORY:
@@ -260,11 +310,9 @@ public class RCTCameraModule extends ReactContextBaseJavaModule
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")) {
@@ -277,6 +325,7 @@ public class RCTCameraModule extends ReactContextBaseJavaModule
mMediaRecorder.setMaxFileSize(maxFileSize);
}
// Prepare the MediaRecorder instance with the provided configuration settings.
try {
mMediaRecorder.prepare();
} catch (Exception ex) {
@@ -316,6 +365,12 @@ public class RCTCameraModule extends ReactContextBaseJavaModule
}
}
/**
* Release media recorder following video capture (or failure to start recording session).
*
* See "Capturing Videos" at https://developer.android.com/guide/topics/media/camera.html for
* a guideline of steps and more information in general.
*/
private void releaseMediaRecorder() {
// Must record at least a second or MediaRecorder throws exceptions on some platforms
long duration = System.currentTimeMillis() - MRStartTime;
@@ -327,16 +382,30 @@ public class RCTCameraModule extends ReactContextBaseJavaModule
}
}
try {
mMediaRecorder.stop(); // stop the recording
} catch (RuntimeException ex) {
Log.e(TAG, "Media recorder stop error.", ex);
// Release actual MediaRecorder instance.
if (mMediaRecorder != null) {
// Stop recording video.
try {
mMediaRecorder.stop(); // stop the recording
} catch (RuntimeException ex) {
Log.e(TAG, "Media recorder stop error.", ex);
}
// Optionally, remove the configuration settings from the recorder.
mMediaRecorder.reset();
// Release the MediaRecorder.
mMediaRecorder.release();
// Reset variable.
mMediaRecorder = null;
}
mMediaRecorder.reset(); // clear recorder configuration
// Lock the camera so that future MediaRecorder sessions can use it by calling
// Camera.lock(). Note this is not required on Android 4.0+ unless the
// MediaRecorder.prepare() call fails.
if (mCamera != null) {
mCamera.lock(); // relock camera for later use since we unlocked it
mCamera.lock();
}
if (mRecordingPromise == null) {
@@ -556,7 +625,6 @@ public class RCTCameraModule extends ReactContextBaseJavaModule
return;
}
addToMediaStore(pictureFile.getAbsolutePath());
response.putString("path", Uri.fromFile(pictureFile).toString());
promise.resolve(response);
break;
@@ -618,9 +686,20 @@ public class RCTCameraModule extends ReactContextBaseJavaModule
}
private File getOutputMediaFile(int type) {
// Get environment directory type id from requested media type.
String environmentDirectoryType;
if (type == MEDIA_TYPE_IMAGE) {
environmentDirectoryType = Environment.DIRECTORY_PICTURES;
} else if (type == MEDIA_TYPE_VIDEO) {
environmentDirectoryType = Environment.DIRECTORY_MOVIES;
} else {
Log.e(TAG, "Unsupported media type:" + type);
return null;
}
return getOutputFile(
type,
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
Environment.getExternalStoragePublicDirectory(environmentDirectoryType)
);
}
@@ -641,18 +720,18 @@ public class RCTCameraModule extends ReactContextBaseJavaModule
}
// Create a media file name
String photoName = String.format("%s", new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()));
String fileName = String.format("%s", new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()));
if (type == MEDIA_TYPE_IMAGE) {
photoName = String.format("IMG_%s.jpg", photoName);
fileName = String.format("IMG_%s.jpg", fileName);
} else if (type == MEDIA_TYPE_VIDEO) {
photoName = String.format("VID_%s.mp4", photoName);
fileName = String.format("VID_%s.mp4", fileName);
} else {
Log.e(TAG, "Unsupported media type:" + type);
return null;
}
return new File(String.format("%s%s%s", storageDir.getPath(), File.separator, photoName));
return new File(String.format("%s%s%s", storageDir.getPath(), File.separator, fileName));
}
private File getTempMediaFile(int type) {