feat(android): Improve Android Camera1 error and concurrency handling. (#2471)

These changes include the following:
- use atomic boolean for capturing photo flag just like video
- add more exception catching and checks
- raise error instead of failing silently if can't capture photo - improve error handling here
- synchronize stop to avoid race conditions and crashes
- delay params updates (surface) if capturing or recording to avoid bugs
- do not allow video or photo capture if already doing video or photo
This commit is contained in:
cristianoccazinsp 2019-09-09 09:15:41 -03:00 committed by Sibelius Seraphini
parent add1c0c01f
commit a20eb06139
2 changed files with 144 additions and 79 deletions

View File

@ -86,7 +86,7 @@ class Camera1 extends CameraViewImpl implements MediaRecorder.OnInfoListener,
private String mVideoPath;
private boolean mIsRecording;
private final AtomicBoolean mIsRecording = new AtomicBoolean(false);
private final SizeMap mPreviewSizes = new SizeMap();
@ -120,6 +120,8 @@ class Camera1 extends CameraViewImpl implements MediaRecorder.OnInfoListener,
private boolean mIsScanning;
private boolean mustUpdateSurface;
private SurfaceTexture mPreviewTexture;
Camera1(Callback callback, PreviewImpl preview) {
@ -127,11 +129,7 @@ class Camera1 extends CameraViewImpl implements MediaRecorder.OnInfoListener,
preview.setCallback(new PreviewImpl.Callback() {
@Override
public void onSurfaceChanged() {
if (mCamera != null) {
setUpPreview();
mIsPreviewActive = false;
adjustCameraParameters();
}
updateSurface();
}
@Override
@ -141,6 +139,25 @@ class Camera1 extends CameraViewImpl implements MediaRecorder.OnInfoListener,
});
}
private void updateSurface(){
if (mCamera != null) {
// do not update surface if we are currently capturing
// since it will break capture events/video due to the
// pause preview calls
// capture callbacks will handle it if needed afterwards.
if(!isPictureCaptureInProgress.get() && !mIsRecording.get()){
mustUpdateSurface = false;
setUpPreview();
mIsPreviewActive = false;
adjustCameraParameters();
}
else{
mustUpdateSurface = true;
}
}
}
@Override
boolean start() {
chooseCamera();
@ -160,25 +177,43 @@ class Camera1 extends CameraViewImpl implements MediaRecorder.OnInfoListener,
@Override
void stop() {
mShowingPreview = false;
if (mMediaRecorder != null) {
mMediaRecorder.stop();
mMediaRecorder.release();
mMediaRecorder = null;
// make sure no other threads are trying to do this at the same time
// such as another call to stop() from surface destroyed
// or host destroyed. Should avoid crashes with concurrent calls
if (mIsRecording) {
int deviceOrientation = displayOrientationToOrientationEnum(mDeviceOrientation);
mCallback.onVideoRecorded(mVideoPath, mOrientation != Constants.ORIENTATION_AUTO ? mOrientation : deviceOrientation, deviceOrientation);
mIsRecording = false;
synchronized (this) {
if (mMediaRecorder != null) {
try{
mMediaRecorder.stop();
}
catch(RuntimeException e){
Log.e("CAMERA_1::", "mMediaRecorder.stop() failed", e);
}
try{
mMediaRecorder.reset();
mMediaRecorder.release();
}
catch(RuntimeException e){
Log.e("CAMERA_1::", "mMediaRecorder.release() failed", e);
}
mMediaRecorder = null;
if (mIsRecording.get()) {
int deviceOrientation = displayOrientationToOrientationEnum(mDeviceOrientation);
mCallback.onVideoRecorded(mVideoPath, mOrientation != Constants.ORIENTATION_AUTO ? mOrientation : deviceOrientation, deviceOrientation);
}
}
}
if (mCamera != null) {
mCamera.stopPreview();
mCamera.setPreviewCallback(null);
}
if (mCamera != null) {
mCamera.stopPreview();
mCamera.setPreviewCallback(null);
}
mShowingPreview = false;
releaseCamera();
releaseCamera();
}
}
// Suppresses Camera#setPreviewTexture
@ -493,61 +528,77 @@ class Camera1 extends CameraViewImpl implements MediaRecorder.OnInfoListener,
}
void takePictureInternal(final ReadableMap options) {
if (!isPictureCaptureInProgress.getAndSet(true)) {
// if not capturing already, atomically set it to true
if (!mIsRecording.get() && isPictureCaptureInProgress.compareAndSet(false, true)) {
if (options.hasKey("orientation") && options.getInt("orientation") != Constants.ORIENTATION_AUTO) {
mOrientation = options.getInt("orientation");
int rotation = orientationEnumToRotation(mOrientation);
mCameraParameters.setRotation(calcCameraRotation(rotation));
try{
mCamera.setParameters(mCameraParameters);
}
catch(RuntimeException e ) {
Log.e("CAMERA_1::", "setParameters failed", e);
}
}
mCamera.takePicture(null, null, null, new Camera.PictureCallback() {
@Override
public void onPictureTaken(byte[] data, Camera camera) {
isPictureCaptureInProgress.set(false);
// this shouldn't be needed and messes up autoFocusPointOfInterest
// camera.cancelAutoFocus();
if (options.hasKey("pauseAfterCapture") && !options.getBoolean("pauseAfterCapture")) {
camera.startPreview();
mIsPreviewActive = true;
if (mIsScanning) {
camera.setPreviewCallback(Camera1.this);
}
} else {
camera.stopPreview();
mIsPreviewActive = false;
camera.setPreviewCallback(null);
try{
if (options.hasKey("orientation") && options.getInt("orientation") != Constants.ORIENTATION_AUTO) {
mOrientation = options.getInt("orientation");
int rotation = orientationEnumToRotation(mOrientation);
mCameraParameters.setRotation(calcCameraRotation(rotation));
try{
mCamera.setParameters(mCameraParameters);
}
catch(RuntimeException e ) {
Log.e("CAMERA_1::", "setParameters failed", e);
}
mOrientation = Constants.ORIENTATION_AUTO;
mCallback.onPictureTaken(data, displayOrientationToOrientationEnum(mDeviceOrientation));
}
});
mCamera.takePicture(null, null, null, new Camera.PictureCallback() {
@Override
public void onPictureTaken(byte[] data, Camera camera) {
isPictureCaptureInProgress.set(false);
// this shouldn't be needed and messes up autoFocusPointOfInterest
// camera.cancelAutoFocus();
if (options.hasKey("pauseAfterCapture") && !options.getBoolean("pauseAfterCapture")) {
camera.startPreview();
mIsPreviewActive = true;
if (mIsScanning) {
camera.setPreviewCallback(Camera1.this);
}
} else {
camera.stopPreview();
mIsPreviewActive = false;
camera.setPreviewCallback(null);
}
mOrientation = Constants.ORIENTATION_AUTO;
mCallback.onPictureTaken(data, displayOrientationToOrientationEnum(mDeviceOrientation));
if(mustUpdateSurface){
updateSurface();
}
}
});
}
catch(Exception e){
isPictureCaptureInProgress.set(false);
throw e;
}
}
else{
throw new IllegalStateException("Camera capture failed. Camera is already capturing.");
}
}
@Override
boolean record(String path, int maxDuration, int maxFileSize, boolean recordAudio, CamcorderProfile profile, int orientation) {
if (!mIsRecording) {
// make sure compareAndSet is last because we are setting it
if (!isPictureCaptureInProgress.get() && mIsRecording.compareAndSet(false, true)) {
if (orientation != Constants.ORIENTATION_AUTO) {
mOrientation = orientation;
}
setUpMediaRecorder(path, maxDuration, maxFileSize, recordAudio, profile);
try {
setUpMediaRecorder(path, maxDuration, maxFileSize, recordAudio, profile);
mMediaRecorder.prepare();
mMediaRecorder.start();
mIsRecording = true;
return true;
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
mIsRecording.set(false);
Log.e("CAMERA_1::", "Record start failed", e);
return false;
}
}
@ -556,11 +607,14 @@ class Camera1 extends CameraViewImpl implements MediaRecorder.OnInfoListener,
@Override
void stopRecording() {
if (mIsRecording) {
if (mIsRecording.compareAndSet(true, false)) {
stopMediaRecorder();
if (mCamera != null) {
mCamera.lock();
}
if(mustUpdateSurface){
updateSurface();
}
}
}
@ -594,7 +648,7 @@ class Camera1 extends CameraViewImpl implements MediaRecorder.OnInfoListener,
return;
}
mDeviceOrientation = deviceOrientation;
if (isCameraOpened() && mOrientation == Constants.ORIENTATION_AUTO && !mIsRecording) {
if (isCameraOpened() && mOrientation == Constants.ORIENTATION_AUTO && !mIsRecording.get() && !isPictureCaptureInProgress.get()) {
mCameraParameters.setRotation(calcCameraRotation(deviceOrientation));
try{
mCamera.setParameters(mCameraParameters);
@ -602,7 +656,7 @@ class Camera1 extends CameraViewImpl implements MediaRecorder.OnInfoListener,
catch(RuntimeException e ) {
Log.e("CAMERA_1::", "setParameters failed", e);
}
}
}
}
@Override
@ -763,6 +817,10 @@ class Camera1 extends CameraViewImpl implements MediaRecorder.OnInfoListener,
mCamera = null;
mPictureSize = null;
mCallback.onCameraClosed();
// reset these flags
isPictureCaptureInProgress.set(false);
mIsRecording.set(false);
}
}
@ -1074,6 +1132,7 @@ class Camera1 extends CameraViewImpl implements MediaRecorder.OnInfoListener,
}
private void setUpMediaRecorder(String path, int maxDuration, int maxFileSize, boolean recordAudio, CamcorderProfile profile) {
mMediaRecorder = new MediaRecorder();
mCamera.unlock();
@ -1107,15 +1166,16 @@ class Camera1 extends CameraViewImpl implements MediaRecorder.OnInfoListener,
mMediaRecorder.setOnInfoListener(this);
mMediaRecorder.setOnErrorListener(this);
}
private void stopMediaRecorder() {
mIsRecording = false;
if (mMediaRecorder != null) {
try {
mMediaRecorder.stop();
} catch (RuntimeException ex) {
ex.printStackTrace();
Log.e("CAMERA_1::", "stopMediaRecorder failed", ex);
}
mMediaRecorder.reset();
mMediaRecorder.release();
@ -1130,6 +1190,7 @@ class Camera1 extends CameraViewImpl implements MediaRecorder.OnInfoListener,
mCallback.onVideoRecorded(mVideoPath, mOrientation != Constants.ORIENTATION_AUTO ? mOrientation : deviceOrientation, deviceOrientation);
mVideoPath = null;
}
private void setCamcorderProfile(CamcorderProfile profile, boolean recordAudio) {

View File

@ -208,7 +208,7 @@ public class CameraModule extends ReactContextBaseJavaModule {
}
});
}
@ReactMethod
public void pausePreview(final int viewTag) {
final ReactApplicationContext context = getReactApplicationContext();
@ -217,7 +217,7 @@ public class CameraModule extends ReactContextBaseJavaModule {
@Override
public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) {
final RNCameraView cameraView;
try {
cameraView = (RNCameraView) nativeViewHierarchyManager.resolveView(viewTag);
if (cameraView.isCameraOpened()) {
@ -229,7 +229,7 @@ public class CameraModule extends ReactContextBaseJavaModule {
}
});
}
@ReactMethod
public void resumePreview(final int viewTag) {
final ReactApplicationContext context = getReactApplicationContext();
@ -238,7 +238,7 @@ public class CameraModule extends ReactContextBaseJavaModule {
@Override
public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) {
final RNCameraView cameraView;
try {
cameraView = (RNCameraView) nativeViewHierarchyManager.resolveView(viewTag);
if (cameraView.isCameraOpened()) {
@ -261,14 +261,18 @@ public class CameraModule extends ReactContextBaseJavaModule {
public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) {
RNCameraView cameraView = (RNCameraView) nativeViewHierarchyManager.resolveView(viewTag);
try {
if (cameraView.isCameraOpened()) {
cameraView.takePicture(options, promise, cacheDirectory);
} else {
promise.reject("E_CAMERA_UNAVAILABLE", "Camera is not running");
}
} catch (Exception e) {
promise.reject("E_CAMERA_BAD_VIEWTAG", "takePictureAsync: Expected a Camera component");
}
if (cameraView.isCameraOpened()) {
cameraView.takePicture(options, promise, cacheDirectory);
} else {
promise.reject("E_CAMERA_UNAVAILABLE", "Camera is not running");
}
}
catch(IllegalStateException e){
promise.reject("E_CAMERA_UNAVAILABLE", e.getMessage());
}
catch (Exception e) {
promise.reject("E_CAMERA_BAD_VIEWTAG", e.getMessage());
}
}
});
}
@ -353,7 +357,7 @@ public class CameraModule extends ReactContextBaseJavaModule {
@Override
public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) {
final RNCameraView cameraView;
try {
cameraView = (RNCameraView) nativeViewHierarchyManager.resolveView(viewTag);
WritableArray result = Arguments.createArray();