Camera2Basic, Camera2Video: Bug fixes
The camera is now closed synchronously before the apps quit.
Bug: 17769434
Change-Id: I2db116b5043c22ed56421688ccacbfe4dc92d3c6
diff --git a/media/Camera2Basic/Application/src/main/java/com/example/android/camera2basic/Camera2BasicFragment.java b/media/Camera2Basic/Application/src/main/java/com/example/android/camera2basic/Camera2BasicFragment.java
index a97da80..f4bf220 100644
--- a/media/Camera2Basic/Application/src/main/java/com/example/android/camera2basic/Camera2BasicFragment.java
+++ b/media/Camera2Basic/Application/src/main/java/com/example/android/camera2basic/Camera2BasicFragment.java
@@ -63,6 +63,8 @@
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
public class Camera2BasicFragment extends Fragment implements View.OnClickListener {
@@ -167,18 +169,21 @@
@Override
public void onOpened(CameraDevice cameraDevice) {
// This method is called when the camera is opened. We start camera preview here.
+ mCameraOpenCloseLock.release();
mCameraDevice = cameraDevice;
createCameraPreviewSession();
}
@Override
public void onDisconnected(CameraDevice cameraDevice) {
+ mCameraOpenCloseLock.release();
cameraDevice.close();
mCameraDevice = null;
}
@Override
public void onError(CameraDevice cameraDevice, int error) {
+ mCameraOpenCloseLock.release();
cameraDevice.close();
mCameraDevice = null;
Activity activity = getActivity();
@@ -241,6 +246,11 @@
private int mState = STATE_PREVIEW;
/**
+ * A {@link Semaphore} to prevent the app from exiting before closing the camera.
+ */
+ private Semaphore mCameraOpenCloseLock = new Semaphore(1);
+
+ /**
* A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture.
*/
private CameraCaptureSession.CaptureCallback mCaptureCallback
@@ -449,9 +459,14 @@
Activity activity = getActivity();
CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
try {
+ if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
+ throw new RuntimeException("Time out waiting to lock camera opening.");
+ }
manager.openCamera(mCameraId, mStateCallback, mBackgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Interrupted while trying to lock camera opening.", e);
}
}
@@ -459,17 +474,24 @@
* Closes the current {@link CameraDevice}.
*/
private void closeCamera() {
- if (null != mCaptureSession) {
- mCaptureSession.close();
- mCaptureSession = null;
- }
- if (null != mCameraDevice) {
- mCameraDevice.close();
- mCameraDevice = null;
- }
- if (null != mImageReader) {
- mImageReader.close();
- mImageReader = null;
+ try {
+ mCameraOpenCloseLock.acquire();
+ if (null != mCaptureSession) {
+ mCaptureSession.close();
+ mCaptureSession = null;
+ }
+ if (null != mCameraDevice) {
+ mCameraDevice.close();
+ mCameraDevice = null;
+ }
+ if (null != mImageReader) {
+ mImageReader.close();
+ mImageReader = null;
+ }
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Interrupted while trying to lock camera closing.", e);
+ } finally {
+ mCameraOpenCloseLock.release();
}
}
@@ -521,6 +543,11 @@
@Override
public void onConfigured(CameraCaptureSession cameraCaptureSession) {
+ // The camera is already closed
+ if (null == mCameraDevice) {
+ return;
+ }
+
// When the session is ready, we start displaying the preview.
mCaptureSession = cameraCaptureSession;
try {
diff --git a/media/Camera2Video/Application/src/main/java/com/example/android/camera2video/Camera2VideoFragment.java b/media/Camera2Video/Application/src/main/java/com/example/android/camera2video/Camera2VideoFragment.java
index 903dd18..78e276a 100644
--- a/media/Camera2Video/Application/src/main/java/com/example/android/camera2video/Camera2VideoFragment.java
+++ b/media/Camera2Video/Application/src/main/java/com/example/android/camera2video/Camera2VideoFragment.java
@@ -39,6 +39,7 @@
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
+import android.util.Log;
import android.util.Size;
import android.util.SparseIntArray;
import android.view.LayoutInflater;
@@ -52,13 +53,18 @@
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
-import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
import java.util.List;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
public class Camera2VideoFragment extends Fragment implements View.OnClickListener {
private static final SparseIntArray ORIENTATIONS = new SparseIntArray();
+ private static final String TAG = "Camera2VideoFragment";
+
static {
ORIENTATIONS.append(Surface.ROTATION_0, 90);
ORIENTATIONS.append(Surface.ROTATION_90, 0);
@@ -96,15 +102,13 @@
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture,
int width, int height) {
- configureTransform(width, height);
- startPreview();
+ openCamera(width, height);
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture,
int width, int height) {
configureTransform(width, height);
- startPreview();
}
@Override
@@ -124,6 +128,11 @@
private Size mPreviewSize;
/**
+ * The {@link android.util.Size} of video recording.
+ */
+ private Size mVideoSize;
+
+ /**
* Camera preview.
*/
private CaptureRequest.Builder mPreviewBuilder;
@@ -139,9 +148,19 @@
private boolean mIsRecordingVideo;
/**
- * Whether the app is currently trying to open camera
+ * An additional thread for running tasks that shouldn't block the UI.
*/
- private boolean mOpeningCamera;
+ private HandlerThread mBackgroundThread;
+
+ /**
+ * A {@link Handler} for running tasks in the background.
+ */
+ private Handler mBackgroundHandler;
+
+ /**
+ * A {@link Semaphore} to prevent the app from exiting before closing the camera.
+ */
+ private Semaphore mCameraOpenCloseLock = new Semaphore(1);
/**
* {@link CameraDevice.StateCallback} is called when {@link CameraDevice} changes its status.
@@ -152,7 +171,7 @@
public void onOpened(CameraDevice cameraDevice) {
mCameraDevice = cameraDevice;
startPreview();
- mOpeningCamera = false;
+ mCameraOpenCloseLock.release();
if (null != mTextureView) {
configureTransform(mTextureView.getWidth(), mTextureView.getHeight());
}
@@ -160,20 +179,20 @@
@Override
public void onDisconnected(CameraDevice cameraDevice) {
+ mCameraOpenCloseLock.release();
cameraDevice.close();
mCameraDevice = null;
- mOpeningCamera = false;
}
@Override
public void onError(CameraDevice cameraDevice, int error) {
+ mCameraOpenCloseLock.release();
cameraDevice.close();
mCameraDevice = null;
Activity activity = getActivity();
if (null != activity) {
activity.finish();
}
- mOpeningCamera = false;
}
};
@@ -184,6 +203,55 @@
return fragment;
}
+ /**
+ * In this sample, we choose a video size with 3x4 aspect ratio. Also, we don't use sizes larger
+ * than 1080p, since MediaRecorder cannot handle such a high-resolution video.
+ *
+ * @param choices The list of available sizes
+ * @return The video size
+ */
+ private static Size chooseVideoSize(Size[] choices) {
+ for (Size size : choices) {
+ if (size.getWidth() == size.getHeight() * 4 / 3 && size.getWidth() <= 1080) {
+ return size;
+ }
+ }
+ Log.e(TAG, "Couldn't find any suitable video size");
+ return choices[choices.length - 1];
+ }
+
+ /**
+ * Given {@code choices} of {@code Size}s supported by a camera, chooses the smallest one whose
+ * width and height are at least as large as the respective requested values, and whose aspect
+ * ratio matches with the specified value.
+ *
+ * @param choices The list of sizes that the camera supports for the intended output class
+ * @param width The minimum desired width
+ * @param height The minimum desired height
+ * @param aspectRatio The aspect ratio
+ * @return The optimal {@code Size}, or an arbitrary one if none were big enough
+ */
+ private static Size chooseOptimalSize(Size[] choices, int width, int height, Size aspectRatio) {
+ // Collect the supported resolutions that are at least as big as the preview Surface
+ List<Size> bigEnough = new ArrayList<Size>();
+ int w = aspectRatio.getWidth();
+ int h = aspectRatio.getHeight();
+ for (Size option : choices) {
+ if (option.getHeight() == option.getWidth() * h / w &&
+ option.getWidth() >= width && option.getHeight() >= height) {
+ bigEnough.add(option);
+ }
+ }
+
+ // Pick the smallest of those, assuming we found any
+ if (bigEnough.size() > 0) {
+ return Collections.min(bigEnough, new CompareSizesByArea());
+ } else {
+ Log.e(TAG, "Couldn't find any suitable preview size");
+ return choices[0];
+ }
+ }
+
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
@@ -193,7 +261,6 @@
@Override
public void onViewCreated(final View view, Bundle savedInstanceState) {
mTextureView = (AutoFitTextureView) view.findViewById(R.id.texture);
- mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);
mButtonVideo = (Button) view.findViewById(R.id.video);
mButtonVideo.setOnClickListener(this);
view.findViewById(R.id.info).setOnClickListener(this);
@@ -202,16 +269,19 @@
@Override
public void onResume() {
super.onResume();
- openCamera();
+ startBackgroundThread();
+ if (mTextureView.isAvailable()) {
+ openCamera(mTextureView.getWidth(), mTextureView.getHeight());
+ } else {
+ mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);
+ }
}
@Override
public void onPause() {
+ closeCamera();
+ stopBackgroundThread();
super.onPause();
- if (null != mCameraDevice) {
- mCameraDevice.close();
- mCameraDevice = null;
- }
}
@Override
@@ -239,27 +309,59 @@
}
/**
+ * Starts a background thread and its {@link Handler}.
+ */
+ private void startBackgroundThread() {
+ mBackgroundThread = new HandlerThread("CameraBackground");
+ mBackgroundThread.start();
+ mBackgroundHandler = new Handler(mBackgroundThread.getLooper());
+ }
+
+ /**
+ * Stops the background thread and its {@link Handler}.
+ */
+ private void stopBackgroundThread() {
+ mBackgroundThread.quitSafely();
+ try {
+ mBackgroundThread.join();
+ mBackgroundThread = null;
+ mBackgroundHandler = null;
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
* Tries to open a {@link CameraDevice}. The result is listened by `mStateCallback`.
*/
- private void openCamera() {
+ private void openCamera(int width, int height) {
final Activity activity = getActivity();
- if (null == activity || activity.isFinishing() || mOpeningCamera) {
+ if (null == activity || activity.isFinishing()) {
return;
}
- mOpeningCamera = true;
CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
try {
+ if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
+ throw new RuntimeException("Time out waiting to lock camera opening.");
+ }
String cameraId = manager.getCameraIdList()[0];
+
+ // Choose the sizes for camera preview and video recording
CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId);
StreamConfigurationMap map = characteristics
.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
- mPreviewSize = map.getOutputSizes(SurfaceTexture.class)[0];
+ mVideoSize = chooseVideoSize(map.getOutputSizes(MediaRecorder.class));
+ mPreviewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class),
+ width, height, mVideoSize);
+
int orientation = getResources().getConfiguration().orientation;
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
mTextureView.setAspectRatio(mPreviewSize.getWidth(), mPreviewSize.getHeight());
} else {
mTextureView.setAspectRatio(mPreviewSize.getHeight(), mPreviewSize.getWidth());
}
+ configureTransform(width, height);
+ mMediaRecorder = new MediaRecorder();
manager.openCamera(cameraId, mStateCallback, null);
} catch (CameraAccessException e) {
Toast.makeText(activity, "Cannot access the camera.", Toast.LENGTH_SHORT).show();
@@ -268,6 +370,26 @@
// Currently an NPE is thrown when the Camera2API is used but not supported on the
// device this code runs.
new ErrorDialog().show(getFragmentManager(), "dialog");
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Interrupted while trying to lock camera opening.");
+ }
+ }
+
+ private void closeCamera() {
+ try {
+ mCameraOpenCloseLock.acquire();
+ if (null != mCameraDevice) {
+ mCameraDevice.close();
+ mCameraDevice = null;
+ }
+ if (null != mMediaRecorder) {
+ mMediaRecorder.release();
+ mMediaRecorder = null;
+ }
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Interrupted while trying to lock camera closing.");
+ } finally {
+ mCameraOpenCloseLock.release();
}
}
@@ -279,14 +401,21 @@
return;
}
try {
+ setUpMediaRecorder();
SurfaceTexture texture = mTextureView.getSurfaceTexture();
assert texture != null;
texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
- mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
- Surface surface = new Surface(texture);
+ mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
List<Surface> surfaces = new ArrayList<Surface>();
- surfaces.add(surface);
- mPreviewBuilder.addTarget(surface);
+
+ Surface previewSurface = new Surface(texture);
+ surfaces.add(previewSurface);
+ mPreviewBuilder.addTarget(previewSurface);
+
+ Surface recorderSurface = mMediaRecorder.getSurface();
+ surfaces.add(recorderSurface);
+ mPreviewBuilder.addTarget(recorderSurface);
+
mCameraDevice.createCaptureSession(surfaces, new CameraCaptureSession.StateCallback() {
@Override
@@ -302,9 +431,11 @@
Toast.makeText(activity, "Failed", Toast.LENGTH_SHORT).show();
}
}
- }, null);
+ }, mBackgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
}
}
@@ -319,8 +450,7 @@
setUpCaptureRequestBuilder(mPreviewBuilder);
HandlerThread thread = new HandlerThread("CameraPreview");
thread.start();
- Handler backgroundHandler = new Handler(thread.getLooper());
- mPreviewSession.setRepeatingRequest(mPreviewBuilder.build(), null, backgroundHandler);
+ mPreviewSession.setRepeatingRequest(mPreviewBuilder.build(), null, mBackgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
@@ -361,78 +491,50 @@
mTextureView.setTransform(matrix);
}
- private void startRecordingVideo() {
+ private void setUpMediaRecorder() throws IOException {
final Activity activity = getActivity();
if (null == activity) {
return;
}
- mMediaRecorder = new MediaRecorder();
- final File file = getVideoFile(activity);
- try {
- // UI
- mButtonVideo.setText(R.string.stop);
- mIsRecordingVideo = true;
- // Configure the MediaRecorder
- mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
- mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
- mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
- mMediaRecorder.setOutputFile(file.getAbsolutePath());
- mMediaRecorder.setVideoEncodingBitRate(10000000);
- mMediaRecorder.setVideoFrameRate(30);
- mMediaRecorder.setVideoSize(1440, 1080);
- mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
- mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
- int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
- int orientation = ORIENTATIONS.get(rotation);
- mMediaRecorder.setOrientationHint(orientation);
- mMediaRecorder.prepare();
- Surface surface = mMediaRecorder.getSurface();
- // Set up the CaptureRequest
- final CaptureRequest.Builder builder =
- mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
- builder.addTarget(surface);
- Surface previewSurface = new Surface(mTextureView.getSurfaceTexture());
- builder.addTarget(previewSurface);
- mCameraDevice.createCaptureSession(Arrays.asList(surface, previewSurface),
- new CameraCaptureSession.StateCallback() {
- @Override
- public void onConfigured(CameraCaptureSession session) {
- // Start recording
- try {
- session.setRepeatingRequest(builder.build(), null, null);
- mMediaRecorder.start();
- } catch (CameraAccessException e) {
- e.printStackTrace();
- }
- }
-
- @Override
- public void onConfigureFailed(CameraCaptureSession session) {
- Toast.makeText(activity, "Failed.", Toast.LENGTH_SHORT).show();
- }
- }, null
- );
- } catch (IllegalStateException e) {
- e.printStackTrace();
- } catch (IOException e) {
- e.printStackTrace();
- } catch (CameraAccessException e) {
- e.printStackTrace();
- }
+ mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
+ mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
+ mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
+ mMediaRecorder.setOutputFile(getVideoFile(activity).getAbsolutePath());
+ mMediaRecorder.setVideoEncodingBitRate(10000000);
+ mMediaRecorder.setVideoFrameRate(30);
+ mMediaRecorder.setVideoSize(mVideoSize.getWidth(), mVideoSize.getHeight());
+ mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
+ mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
+ int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
+ int orientation = ORIENTATIONS.get(rotation);
+ mMediaRecorder.setOrientationHint(orientation);
+ mMediaRecorder.prepare();
}
private File getVideoFile(Context context) {
return new File(context.getExternalFilesDir(null), "video.mp4");
}
+ private void startRecordingVideo() {
+ try {
+ // UI
+ mButtonVideo.setText(R.string.stop);
+ mIsRecordingVideo = true;
+
+ // Start recording
+ mMediaRecorder.start();
+ } catch (IllegalStateException e) {
+ e.printStackTrace();
+ }
+ }
+
private void stopRecordingVideo() {
// UI
mIsRecordingVideo = false;
mButtonVideo.setText(R.string.record);
// Stop recording
mMediaRecorder.stop();
- mMediaRecorder.release();
- mMediaRecorder = null;
+ mMediaRecorder.reset();
Activity activity = getActivity();
if (null != activity) {
Toast.makeText(activity, "Video saved: " + getVideoFile(activity),
@@ -441,6 +543,20 @@
startPreview();
}
+ /**
+ * Compares two {@code Size}s based on their areas.
+ */
+ static class CompareSizesByArea implements Comparator<Size> {
+
+ @Override
+ public int compare(Size lhs, Size rhs) {
+ // We cast here to ensure the multiplications won't overflow
+ return Long.signum((long) lhs.getWidth() * lhs.getHeight() -
+ (long) rhs.getWidth() * rhs.getHeight());
+ }
+
+ }
+
public static class ErrorDialog extends DialogFragment {
@Override