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

View File

@ -145,9 +145,9 @@ The type of capture that will be performed by the camera - either a still image
#### `captureTarget`
Values: `Camera.constants.CaptureTarget.cameraRoll` (ios only default), `Camera.constants.CaptureTarget.disk` (android default), `Camera.constants.CaptureTarget.temp`, ~~`Camera.constants.CaptureTarget.memory`~~ (deprecated),
Values: `Camera.constants.CaptureTarget.cameraRoll` (default), `Camera.constants.CaptureTarget.disk`, `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.
This property allows you to specify the target output of the captured image data. The disk output has been shown to improve capture response time, so that is the recommended value. When using the deprecated memory output, the image binary is sent back as a base64-encoded string.
#### `captureQuality`

View File

@ -109,7 +109,7 @@ public class RCTCamera {
return smallestSize;
}
private List<Camera.Size> getSupportedVideoSizes(Camera camera) {
protected List<Camera.Size> getSupportedVideoSizes(Camera camera) {
Camera.Parameters params = camera.getParameters();
// defer to preview instead of params.getSupportedVideoSizes() http://bit.ly/1rxOsq0
// but prefer SupportedVideoSizes!
@ -170,6 +170,19 @@ public class RCTCamera {
adjustPreviewLayout(RCTCameraModule.RCT_CAMERA_TYPE_BACK);
}
public void setCaptureMode(final int cameraType, final int captureMode) {
Camera camera = _cameras.get(cameraType);
if (camera == null) {
return;
}
// Set (video) recording hint based on camera type. For video recording, setting
// this hint can help reduce the time it takes to start recording.
Camera.Parameters parameters = camera.getParameters();
parameters.setRecordingHint(captureMode == RCTCameraModule.RCT_CAMERA_CAPTURE_MODE_VIDEO);
camera.setParameters(parameters);
}
public void setCaptureQuality(int cameraType, String captureQuality) {
Camera camera = _cameras.get(cameraType);
if (camera == null) {

View File

@ -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) {

View File

@ -19,6 +19,7 @@ public class RCTCameraView extends ViewGroup {
private RCTCameraViewFinder _viewFinder = null;
private int _actualDeviceOrientation = -1;
private int _aspect = RCTCameraModule.RCT_CAMERA_ASPECT_FIT;
private int _captureMode = RCTCameraModule.RCT_CAMERA_CAPTURE_MODE_STILL;
private String _captureQuality = "high";
private int _torchMode = -1;
private int _flashMode = -1;
@ -79,6 +80,13 @@ public class RCTCameraView extends ViewGroup {
}
}
public void setCaptureMode(final int captureMode) {
this._captureMode = captureMode;
if (this._viewFinder != null) {
this._viewFinder.setCaptureMode(captureMode);
}
}
public void setCaptureQuality(String captureQuality) {
this._captureQuality = captureQuality;
if (this._viewFinder != null) {

View File

@ -29,6 +29,7 @@ import com.google.zxing.common.HybridBinarizer;
class RCTCameraViewFinder extends TextureView implements TextureView.SurfaceTextureListener, Camera.PreviewCallback {
private int _cameraType;
private int _captureMode;
private SurfaceTexture _surfaceTexture;
private boolean _isStarting;
private boolean _isStopping;
@ -88,6 +89,11 @@ class RCTCameraViewFinder extends TextureView implements TextureView.SurfaceText
}).start();
}
public void setCaptureMode(final int captureMode) {
RCTCamera.getInstance().setCaptureMode(_cameraType, captureMode);
this._captureMode = captureMode;
}
public void setCaptureQuality(String captureQuality) {
RCTCamera.getInstance().setCaptureQuality(_cameraType, captureQuality);
}
@ -125,8 +131,16 @@ class RCTCameraViewFinder extends TextureView implements TextureView.SurfaceText
}
// set picture size
// defaults to max available size
List<Camera.Size> supportedSizes;
if (_captureMode == RCTCameraModule.RCT_CAMERA_CAPTURE_MODE_STILL) {
supportedSizes = parameters.getSupportedPictureSizes();
} else if (_captureMode == RCTCameraModule.RCT_CAMERA_CAPTURE_MODE_VIDEO) {
supportedSizes = RCTCamera.getInstance().getSupportedVideoSizes(_camera);
} else {
throw new RuntimeException("Unsupported capture mode:" + _captureMode);
}
Camera.Size optimalPictureSize = RCTCamera.getInstance().getBestSize(
parameters.getSupportedPictureSizes(),
supportedSizes,
Integer.MAX_VALUE,
Integer.MAX_VALUE
);

View File

@ -28,8 +28,11 @@ public class RCTCameraViewManager extends ViewGroupManager<RCTCameraView> {
}
@ReactProp(name = "captureMode")
public void setCaptureMode(RCTCameraView view, int captureMode) {
// TODO - implement video mode
public void setCaptureMode(RCTCameraView view, final int captureMode) {
// Note that this in practice only performs any additional setup necessary for each mode;
// the actual indication to capture a still or record a video when capture() is called is
// still ultimately decided upon by what it in the options sent to capture().
view.setCaptureMode(captureMode);
}
@ReactProp(name = "captureTarget")