From d30e0ad3e759493f3765215a76396d828be55462 Mon Sep 17 00:00:00 2001 From: sakal Date: Thu, 11 Aug 2016 01:00:37 -0700 Subject: [PATCH] Session based capturing for Camera2Capturer. BUG=webrtc:6148 TBR=tommi@webrtc.org Review-Url: https://codereview.webrtc.org/2186253002 Cr-Commit-Position: refs/heads/master@{#13723} --- webrtc/api/BUILD.gn | 3 + .../java/src/org/webrtc/Camera2Capturer.java | 878 +----------------- .../java/src/org/webrtc/Camera2Session.java | 409 ++++++++ .../java/src/org/webrtc/CameraCapturer.java | 285 ++++++ .../java/src/org/webrtc/CameraSession.java | 24 + .../Camera1CapturerUsingByteBufferTest.java | 13 +- .../Camera1CapturerUsingTextureTest.java | 4 +- .../src/org/webrtc/Camera2CapturerTest.java | 4 +- .../CameraVideoCapturerTestFixtures.java | 49 +- .../base/java/src/org/webrtc/ThreadUtils.java | 9 + 10 files changed, 790 insertions(+), 888 deletions(-) create mode 100644 webrtc/api/android/java/src/org/webrtc/Camera2Session.java create mode 100644 webrtc/api/android/java/src/org/webrtc/CameraCapturer.java create mode 100644 webrtc/api/android/java/src/org/webrtc/CameraSession.java diff --git a/webrtc/api/BUILD.gn b/webrtc/api/BUILD.gn index 8b346929d4..c4dc5f53d6 100644 --- a/webrtc/api/BUILD.gn +++ b/webrtc/api/BUILD.gn @@ -240,8 +240,11 @@ if (is_android && !build_with_chromium) { "android/java/src/org/webrtc/Camera1Enumerator.java", "android/java/src/org/webrtc/Camera2Capturer.java", "android/java/src/org/webrtc/Camera2Enumerator.java", + "android/java/src/org/webrtc/Camera2Session.java", + "android/java/src/org/webrtc/CameraCapturer.java", "android/java/src/org/webrtc/CameraEnumerationAndroid.java", "android/java/src/org/webrtc/CameraEnumerator.java", + "android/java/src/org/webrtc/CameraSession.java", "android/java/src/org/webrtc/CameraVideoCapturer.java", "android/java/src/org/webrtc/DataChannel.java", "android/java/src/org/webrtc/EglBase.java", diff --git a/webrtc/api/android/java/src/org/webrtc/Camera2Capturer.java b/webrtc/api/android/java/src/org/webrtc/Camera2Capturer.java index d26275cb0d..f1c37a155b 100644 --- a/webrtc/api/android/java/src/org/webrtc/Camera2Capturer.java +++ b/webrtc/api/android/java/src/org/webrtc/Camera2Capturer.java @@ -14,879 +14,41 @@ import org.webrtc.CameraEnumerationAndroid.CaptureFormat; import android.annotation.TargetApi; import android.content.Context; -import android.graphics.SurfaceTexture; -import android.hardware.camera2.CameraAccessException; -import android.hardware.camera2.CameraCaptureSession; -import android.hardware.camera2.CameraCharacteristics; -import android.hardware.camera2.CameraDevice; import android.hardware.camera2.CameraManager; -import android.hardware.camera2.CameraMetadata; -import android.hardware.camera2.CaptureFailure; -import android.hardware.camera2.CaptureRequest; -import android.hardware.camera2.TotalCaptureResult; import android.os.Handler; -import android.os.SystemClock; -import android.util.Range; -import android.view.Surface; -import android.view.WindowManager; -import java.util.Arrays; import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.atomic.AtomicBoolean; @TargetApi(21) -public class Camera2Capturer implements - CameraVideoCapturer, - SurfaceTextureHelper.OnTextureFrameAvailableListener { - private final static String TAG = "Camera2Capturer"; - - private final static int MAX_OPEN_CAMERA_ATTEMPTS = 3; - private final static int OPEN_CAMERA_DELAY_MS = 500; - private final static int STOP_TIMEOUT = 10000; - private final static int START_TIMEOUT = 10000; - private final static Object STOP_TIMEOUT_RUNNABLE_TOKEN = new Object(); - - // In the Camera2 API, starting a camera is inherently asynchronous, and this state is - // represented with 'STARTING'. Stopping is also asynchronous and this state is 'STOPPING'. - private static enum CameraState { IDLE, STARTING, RUNNING, STOPPING } - - // Thread safe objects. - // -------------------- +public class Camera2Capturer extends CameraCapturer { + private final Context context; private final CameraManager cameraManager; - private final CameraEventsHandler eventsHandler; - // Set once in initialization(), before any other calls, so therefore thread safe. - // --------------------------------------------------------------------------------------------- - private SurfaceTextureHelper surfaceTextureHelper; - private Context applicationContext; - private CapturerObserver capturerObserver; - // Use postOnCameraThread() instead of posting directly to the handler - this way all callbacks - // with a specifed token can be removed at once. - private Handler cameraThreadHandler; + public Camera2Capturer(Context context, String cameraName, CameraEventsHandler eventsHandler) { + super(cameraName, eventsHandler, new Camera2Enumerator(context)); - // Shared state - guarded by cameraStateLock. Will only be edited from camera thread (when it is - // running). - // --------------------------------------------------------------------------------------------- - private final Object cameraStateLock = new Object(); - private volatile CameraState cameraState = CameraState.IDLE; - // Remember the requested format in case we want to switch cameras. - private int requestedWidth; - private int requestedHeight; - private int requestedFramerate; - - // Will only be edited while camera state is IDLE and cameraStateLock is acquired. - private String cameraName; - private boolean isFrontCamera; - private int cameraOrientation; - - // Atomic boolean for allowing only one switch at a time. - private final AtomicBoolean isPendingCameraSwitch = new AtomicBoolean(); - // Guarded by isPendingCameraSwitch. - private CameraSwitchHandler switchEventsHandler; - - // Internal state - must only be modified from camera thread - // --------------------------------------------------------- - private CaptureFormat captureFormat; - private CameraStatistics cameraStatistics; - private CameraCaptureSession captureSession; - private Surface surface; - private CameraDevice cameraDevice; - - // Factor to convert between Android framerates and CaptureFormat.FramerateRange. It will be - // either 1 or 1000. - private int fpsUnitFactor; - private boolean firstFrameReported; - private int consecutiveCameraOpenFailures; - - public Camera2Capturer( - Context context, String cameraName, CameraEventsHandler eventsHandler) { - Logging.d(TAG, "Camera2Capturer ctor, camera name: " + cameraName); - this.cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE); - this.eventsHandler = eventsHandler; - - setCameraName(cameraName); - } - - private boolean isOnCameraThread() { - return Thread.currentThread() == cameraThreadHandler.getLooper().getThread(); - } - - /** - * Helper method for checking method is executed on camera thread. - */ - private void checkIsOnCameraThread() { - if (!isOnCameraThread()) { - throw new IllegalStateException("Not on camera thread"); - } - } - - /** - * Checks method is not invoked on the camera thread. Used in functions waiting for the camera - * state to change since executing them on the camera thread would cause a deadlock. - */ - private void checkNotOnCameraThread() { - if (cameraThreadHandler == null) { - return; - } - - if (Thread.currentThread() == cameraThreadHandler.getLooper().getThread()) { - throw new IllegalStateException( - "Method waiting for camera state to change executed on camera thread"); - } - } - - private void waitForCameraToExitTransitionalState( - CameraState transitionalState, long timeoutMs) { - checkNotOnCameraThread(); - - // We probably should already have the lock when this is called but acquire it in case - // we don't have it. - synchronized (cameraStateLock) { - long timeoutAt = SystemClock.uptimeMillis() + timeoutMs; - - while (cameraState == transitionalState) { - Logging.d(TAG, "waitForCameraToExitTransitionalState waiting: " - + cameraState); - - long timeLeft = timeoutAt - SystemClock.uptimeMillis(); - - if (timeLeft <= 0) { - Logging.e(TAG, "Camera failed to exit transitional state " + transitionalState - + " within the time limit."); - break; - } - - try { - cameraStateLock.wait(timeLeft); - } catch (InterruptedException e) { - Logging.w(TAG, "Trying to interrupt while waiting to exit transitional state " - + transitionalState + ", ignoring: " + e); - } - } - } - } - - /** - * Waits until camera state is not STOPPING. - */ - private void waitForCameraToStopIfStopping() { - waitForCameraToExitTransitionalState(CameraState.STOPPING, STOP_TIMEOUT); - } - - /** - * Wait until camera state is not STARTING. - */ - private void waitForCameraToStartIfStarting() { - waitForCameraToExitTransitionalState(CameraState.STARTING, START_TIMEOUT); - } - - /** - * Sets the name of the camera. Camera must be stopped or stopping when this is called. - */ - private void setCameraName(String cameraName) { - final CameraCharacteristics characteristics; - try { - final String[] cameraIds = cameraManager.getCameraIdList(); - - if (cameraName.isEmpty() && cameraIds.length != 0) { - cameraName = cameraIds[0]; - } - - if (!Arrays.asList(cameraIds).contains(cameraName)) { - throw new IllegalArgumentException( - "Camera name: " + cameraName + " does not match any known camera device:"); - } - - characteristics = cameraManager.getCameraCharacteristics(cameraName); - } catch (CameraAccessException e) { - throw new RuntimeException("Camera access exception: " + e); - } - - synchronized (cameraStateLock) { - waitForCameraToStopIfStopping(); - - if (cameraState != CameraState.IDLE) { - throw new RuntimeException("Changing camera name on running camera."); - } - - // Note: Usually changing camera state from outside camera thread is not allowed. It is - // allowed here because camera is not running. - this.cameraName = cameraName; - isFrontCamera = characteristics.get(CameraCharacteristics.LENS_FACING) - == CameraMetadata.LENS_FACING_FRONT; - - /* - * Clockwise angle through which the output image needs to be rotated to be upright on the - * device screen in its native orientation. - * Also defines the direction of rolling shutter readout, which is from top to bottom in the - * sensor's coordinate system. - * Units: Degrees of clockwise rotation; always a multiple of 90 - */ - cameraOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); - } - } - - /** - * Triggers appropriate error handlers based on the camera state. Must be called on the camera - * thread and camera must not be stopped. - */ - private void reportError(String errorDescription) { - checkIsOnCameraThread(); - Logging.e(TAG, "Error in camera at state " + cameraState + ": " + errorDescription); - - if (switchEventsHandler != null) { - switchEventsHandler.onCameraSwitchError(errorDescription); - switchEventsHandler = null; - } - isPendingCameraSwitch.set(false); - - switch (cameraState) { - case STARTING: - capturerObserver.onCapturerStarted(false /* success */); - // fall through - case RUNNING: - if (eventsHandler != null) { - eventsHandler.onCameraError(errorDescription); - } - break; - case STOPPING: - setCameraState(CameraState.IDLE); - Logging.e(TAG, "Closing camera failed: " + errorDescription); - return; // We don't want to call closeAndRelease in this case. - default: - throw new RuntimeException("Unknown camera state: " + cameraState); - } - closeAndRelease(); - } - - private void closeAndRelease() { - checkIsOnCameraThread(); - - Logging.d(TAG, "Close and release."); - setCameraState(CameraState.STOPPING); - capturerObserver.onCapturerStopped(); - - // Remove all pending Runnables posted from |this|. - cameraThreadHandler.removeCallbacksAndMessages(this /* token */); - if (cameraStatistics != null) { - cameraStatistics.release(); - cameraStatistics = null; - } - if (surfaceTextureHelper != null) { - surfaceTextureHelper.stopListening(); - } - if (captureSession != null) { - captureSession.close(); - captureSession = null; - } - if (surface != null) { - surface.release(); - surface = null; - } - if (cameraDevice != null) { - // Add a timeout for stopping the camera. - cameraThreadHandler.postAtTime(new Runnable() { - @Override - public void run() { - Logging.e(TAG, "Camera failed to stop within the timeout. Force stopping."); - setCameraState(CameraState.IDLE); - if (eventsHandler != null) { - eventsHandler.onCameraError("Camera failed to stop (timeout)."); - } - } - }, STOP_TIMEOUT_RUNNABLE_TOKEN, SystemClock.uptimeMillis() + STOP_TIMEOUT); - - cameraDevice.close(); - cameraDevice = null; - } else { - Logging.w(TAG, "closeAndRelease called while cameraDevice is null"); - setCameraState(CameraState.IDLE); - } - } - - /** - * Sets the camera state while ensuring constraints are followed. - */ - private void setCameraState(CameraState newState) { - // State must only be modified on the camera thread. It can be edited from other threads - // if cameraState is IDLE since the camera thread is idle and not modifying the state. - if (cameraState != CameraState.IDLE) { - checkIsOnCameraThread(); - } - - switch (newState) { - case STARTING: - if (cameraState != CameraState.IDLE) { - throw new IllegalStateException("Only stopped camera can start."); - } - break; - case RUNNING: - if (cameraState != CameraState.STARTING) { - throw new IllegalStateException("Only starting camera can go to running state."); - } - break; - case STOPPING: - if (cameraState != CameraState.STARTING && cameraState != CameraState.RUNNING) { - throw new IllegalStateException("Only starting or running camera can stop."); - } - break; - case IDLE: - if (cameraState != CameraState.STOPPING) { - throw new IllegalStateException("Only stopping camera can go to idle state."); - } - break; - default: - throw new RuntimeException("Unknown camera state: " + newState); - } - - synchronized (cameraStateLock) { - cameraState = newState; - cameraStateLock.notifyAll(); - } - } - - /** - * Internal method for opening the camera. Must be called on the camera thread. - */ - private void openCamera() { - try { - checkIsOnCameraThread(); - - if (cameraState != CameraState.STARTING) { - throw new IllegalStateException("Camera should be in state STARTING in openCamera."); - } - - // Camera is in state STARTING so cameraName will not be edited. - cameraManager.openCamera(cameraName, new CameraStateCallback(), cameraThreadHandler); - } catch (CameraAccessException e) { - reportError("Failed to open camera: " + e); - } - } - - private boolean isInitialized() { - return applicationContext != null && capturerObserver != null; - } - - @Override - public void initialize(SurfaceTextureHelper surfaceTextureHelper, Context applicationContext, - CapturerObserver capturerObserver) { - Logging.d(TAG, "initialize"); - if (applicationContext == null) { - throw new IllegalArgumentException("applicationContext not set."); - } - if (capturerObserver == null) { - throw new IllegalArgumentException("capturerObserver not set."); - } - if (isInitialized()) { - throw new IllegalStateException("Already initialized"); - } - this.applicationContext = applicationContext; - this.capturerObserver = capturerObserver; - this.surfaceTextureHelper = surfaceTextureHelper; - this.cameraThreadHandler = - surfaceTextureHelper == null ? null : surfaceTextureHelper.getHandler(); - } - - private void startCaptureOnCameraThread( - final int requestedWidth, final int requestedHeight, final int requestedFramerate) { - checkIsOnCameraThread(); - - firstFrameReported = false; - consecutiveCameraOpenFailures = 0; - - synchronized (cameraStateLock) { - // Remember the requested format in case we want to switch cameras. - this.requestedWidth = requestedWidth; - this.requestedHeight = requestedHeight; - this.requestedFramerate = requestedFramerate; - } - - final CameraCharacteristics cameraCharacteristics; - try { - // Camera is in state STARTING so cameraName will not be edited. - cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraName); - } catch (CameraAccessException e) { - reportError("getCameraCharacteristics(): " + e.getMessage()); - return; - } - - Range[] fpsRanges = - cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); - fpsUnitFactor = Camera2Enumerator.getFpsUnitFactor(fpsRanges); - List framerateRanges = - Camera2Enumerator.convertFramerates(fpsRanges, fpsUnitFactor); - List sizes = Camera2Enumerator.getSupportedSizes(cameraCharacteristics); - - if (framerateRanges.isEmpty() || sizes.isEmpty()) { - reportError("No supported capture formats."); - } - final CaptureFormat.FramerateRange bestFpsRange = - CameraEnumerationAndroid.getClosestSupportedFramerateRange( - framerateRanges, requestedFramerate); - - final Size bestSize = CameraEnumerationAndroid.getClosestSupportedSize( - sizes, requestedWidth, requestedHeight); - - this.captureFormat = new CaptureFormat(bestSize.width, bestSize.height, bestFpsRange); - Logging.d(TAG, "Using capture format: " + captureFormat); - - Logging.d(TAG, "Opening camera " + cameraName); - if (eventsHandler != null) { - int cameraIndex = -1; - try { - cameraIndex = Integer.parseInt(cameraName); - } catch (NumberFormatException e) { - Logging.d(TAG, "External camera with non-int identifier: " + cameraName); - } - eventsHandler.onCameraOpening(cameraIndex); - } - - openCamera(); - } - - /** - * Starts capture using specified settings. This is automatically called for you by - * VideoCapturerTrackSource if you are just using the camera as source for video track. - */ - @Override - public void startCapture( - final int requestedWidth, final int requestedHeight, final int requestedFramerate) { - Logging.d(TAG, "startCapture requested: " + requestedWidth + "x" + requestedHeight - + "@" + requestedFramerate); - if (!isInitialized()) { - throw new IllegalStateException("startCapture called in uninitialized state"); - } - if (surfaceTextureHelper == null) { - capturerObserver.onCapturerStarted(false /* success */); - if (eventsHandler != null) { - eventsHandler.onCameraError("No SurfaceTexture created."); - } - return; - } - synchronized (cameraStateLock) { - waitForCameraToStopIfStopping(); - if (cameraState != CameraState.IDLE) { - Logging.e(TAG, "Unexpected camera state for startCapture: " + cameraState); - return; - } - setCameraState(CameraState.STARTING); - } - - postOnCameraThread(new Runnable() { - @Override - public void run() { - startCaptureOnCameraThread(requestedWidth, requestedHeight, requestedFramerate); - } - }); - } - - final class CameraStateCallback extends CameraDevice.StateCallback { - private String getErrorDescription(int errorCode) { - switch (errorCode) { - case CameraDevice.StateCallback.ERROR_CAMERA_DEVICE: - return "Camera device has encountered a fatal error."; - case CameraDevice.StateCallback.ERROR_CAMERA_DISABLED: - return "Camera device could not be opened due to a device policy."; - case CameraDevice.StateCallback.ERROR_CAMERA_IN_USE: - return "Camera device is in use already."; - case CameraDevice.StateCallback.ERROR_CAMERA_SERVICE: - return "Camera service has encountered a fatal error."; - case CameraDevice.StateCallback.ERROR_MAX_CAMERAS_IN_USE: - return "Camera device could not be opened because" - + " there are too many other open camera devices."; - default: - return "Unknown camera error: " + errorCode; - } - } - - @Override - public void onDisconnected(CameraDevice camera) { - checkIsOnCameraThread(); - cameraDevice = camera; - reportError("Camera disconnected."); - } - - @Override - public void onError(CameraDevice camera, int errorCode) { - checkIsOnCameraThread(); - cameraDevice = camera; - - if (cameraState == CameraState.STARTING && ( - errorCode == CameraDevice.StateCallback.ERROR_CAMERA_IN_USE || - errorCode == CameraDevice.StateCallback.ERROR_MAX_CAMERAS_IN_USE)) { - consecutiveCameraOpenFailures++; - - if (consecutiveCameraOpenFailures < MAX_OPEN_CAMERA_ATTEMPTS) { - Logging.w(TAG, "Opening camera failed, trying again: " + getErrorDescription(errorCode)); - - postDelayedOnCameraThread(OPEN_CAMERA_DELAY_MS, new Runnable() { - @Override - public void run() { - openCamera(); - } - }); - return; - } else { - Logging.e(TAG, "Opening camera failed too many times. Passing the error."); - } - } - - reportError(getErrorDescription(errorCode)); - } - - @Override - public void onOpened(CameraDevice camera) { - checkIsOnCameraThread(); - - Logging.d(TAG, "Camera opened."); - if (cameraState != CameraState.STARTING) { - throw new IllegalStateException("Unexpected state when camera opened: " + cameraState); - } - - cameraDevice = camera; - final SurfaceTexture surfaceTexture = surfaceTextureHelper.getSurfaceTexture(); - surfaceTexture.setDefaultBufferSize(captureFormat.width, captureFormat.height); - surface = new Surface(surfaceTexture); - try { - camera.createCaptureSession( - Arrays.asList(surface), new CaptureSessionCallback(), cameraThreadHandler); - } catch (CameraAccessException e) { - reportError("Failed to create capture session. " + e); - } - } - - @Override - public void onClosed(CameraDevice camera) { - checkIsOnCameraThread(); - - Logging.d(TAG, "Camera device closed."); - - if (cameraState != CameraState.STOPPING) { - Logging.e(TAG, "Camera state was not STOPPING in onClosed. Most likely camera didn't stop " - + "within timelimit and this method was invoked twice."); - return; - } - - cameraThreadHandler.removeCallbacksAndMessages(STOP_TIMEOUT_RUNNABLE_TOKEN); - setCameraState(CameraState.IDLE); - if (eventsHandler != null) { - eventsHandler.onCameraClosed(); - } - } - } - - final class CaptureSessionCallback extends CameraCaptureSession.StateCallback { - @Override - public void onConfigureFailed(CameraCaptureSession session) { - checkIsOnCameraThread(); - captureSession = session; - reportError("Failed to configure capture session."); - } - - @Override - public void onConfigured(CameraCaptureSession session) { - checkIsOnCameraThread(); - Logging.d(TAG, "Camera capture session configured."); - captureSession = session; - try { - /* - * The viable options for video capture requests are: - * TEMPLATE_PREVIEW: High frame rate is given priority over the highest-quality - * post-processing. - * TEMPLATE_RECORD: Stable frame rate is used, and post-processing is set for recording - * quality. - */ - final CaptureRequest.Builder captureRequestBuilder = - cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD); - // Set auto exposure fps range. - captureRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, new Range( - captureFormat.framerate.min / fpsUnitFactor, - captureFormat.framerate.max / fpsUnitFactor)); - captureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, - CaptureRequest.CONTROL_AE_MODE_ON); - captureRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false); - - captureRequestBuilder.addTarget(surface); - session.setRepeatingRequest( - captureRequestBuilder.build(), new CameraCaptureCallback(), cameraThreadHandler); - } catch (CameraAccessException e) { - reportError("Failed to start capture request. " + e); - return; - } - - Logging.d(TAG, "Camera device successfully started."); - surfaceTextureHelper.startListening(Camera2Capturer.this); - capturerObserver.onCapturerStarted(true /* success */); - cameraStatistics = new CameraStatistics(surfaceTextureHelper, eventsHandler); - setCameraState(CameraState.RUNNING); - - if (switchEventsHandler != null) { - switchEventsHandler.onCameraSwitchDone(isFrontCamera); - switchEventsHandler = null; - } - isPendingCameraSwitch.set(false); - } - } - - final class CameraCaptureCallback extends CameraCaptureSession.CaptureCallback { - static final int MAX_CONSECUTIVE_CAMERA_CAPTURE_FAILURES = 10; - int consecutiveCameraCaptureFailures; - - @Override - public void onCaptureFailed( - CameraCaptureSession session, CaptureRequest request, CaptureFailure failure) { - checkIsOnCameraThread(); - ++consecutiveCameraCaptureFailures; - if (consecutiveCameraCaptureFailures > MAX_CONSECUTIVE_CAMERA_CAPTURE_FAILURES) { - reportError("Capture failed " + consecutiveCameraCaptureFailures + " consecutive times."); - } - } - - @Override - public void onCaptureCompleted( - CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) { - // TODO(sakal): This sometimes gets called after camera has stopped, investigate - checkIsOnCameraThread(); - consecutiveCameraCaptureFailures = 0; - } - } - - - - // Switch camera to the next valid camera id. This can only be called while - // the camera is running. - @Override - public void switchCamera(final CameraSwitchHandler switchEventsHandler) { - final String[] cameraIds; - try { - cameraIds = cameraManager.getCameraIdList(); - } catch (CameraAccessException e) { - if (switchEventsHandler != null) { - switchEventsHandler.onCameraSwitchError("Could not get camera names: " + e); - } - return; - } - if (cameraIds.length < 2) { - if (switchEventsHandler != null) { - switchEventsHandler.onCameraSwitchError("No camera to switch to."); - } - return; - } - // Do not handle multiple camera switch request to avoid blocking camera thread by handling too - // many switch request from a queue. We have to be careful to always release - // |isPendingCameraSwitch| by setting it to false when done. - if (isPendingCameraSwitch.getAndSet(true)) { - Logging.w(TAG, "Ignoring camera switch request."); - if (switchEventsHandler != null) { - switchEventsHandler.onCameraSwitchError("Pending camera switch already in progress."); - } - return; - } - - final String newCameraId; - final int requestedWidth; - final int requestedHeight; - final int requestedFramerate; - - synchronized (cameraStateLock) { - waitForCameraToStartIfStarting(); - - if (cameraState != CameraState.RUNNING) { - Logging.e(TAG, "Calling swithCamera() on stopped camera."); - if (switchEventsHandler != null) { - switchEventsHandler.onCameraSwitchError("Camera is stopped."); - } - isPendingCameraSwitch.set(false); - return; - } - - // Calculate new camera index and camera id. Camera is in state RUNNING so cameraName will - // not be edited. - final int currentCameraIndex = Arrays.asList(cameraIds).indexOf(cameraName); - if (currentCameraIndex == -1) { - Logging.e(TAG, "Couldn't find current camera id " + cameraName - + " in list of camera ids: " + Arrays.toString(cameraIds)); - } - final int newCameraIndex = (currentCameraIndex + 1) % cameraIds.length; - newCameraId = cameraIds[newCameraIndex]; - - requestedWidth = this.requestedWidth; - requestedHeight = this.requestedHeight; - requestedFramerate = this.requestedFramerate; - this.switchEventsHandler = switchEventsHandler; - } - - // Make the switch. - stopCapture(); - setCameraName(newCameraId); - startCapture(requestedWidth, requestedHeight, requestedFramerate); - - // Note: switchEventsHandler will be called from onConfigured / reportError. - } - - // Requests a new output format from the video capturer. Captured frames - // by the camera will be scaled/or dropped by the video capturer. - // It does not matter if width and height are flipped. I.E, |width| = 640, |height| = 480 produce - // the same result as |width| = 480, |height| = 640. - // TODO(magjed/perkj): Document what this function does. Change name? - @Override - public void onOutputFormatRequest(final int width, final int height, final int framerate) { - postOnCameraThread(new Runnable() { - @Override - public void run() { - Logging.d(TAG, - "onOutputFormatRequestOnCameraThread: " + width + "x" + height + "@" + framerate); - capturerObserver.onOutputFormatRequest(width, height, framerate); - } - }); - } - - // Reconfigure the camera to capture in a new format. This should only be called while the camera - // is running. - @Override - public void changeCaptureFormat(final int width, final int height, final int framerate) { - synchronized (cameraStateLock) { - waitForCameraToStartIfStarting(); - - if (cameraState != CameraState.RUNNING) { - Logging.e(TAG, "Calling changeCaptureFormat() on stopped camera."); - return; - } - - requestedWidth = width; - requestedHeight = height; - requestedFramerate = framerate; - } - - // Make the switch. - stopCapture(); - // TODO(magjed/sakal): Just recreate session. - startCapture(width, height, framerate); + this.context = context; + cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE); } @Override public List getSupportedFormats() { - synchronized (cameraState) { - return Camera2Enumerator.getSupportedFormats(this.cameraManager, cameraName); - } + return Camera2Enumerator.getSupportedFormats(cameraManager, getCameraName()); } @Override - public void dispose() { - synchronized (cameraStateLock) { - waitForCameraToStopIfStopping(); - - if (cameraState != CameraState.IDLE) { - throw new IllegalStateException("Unexpected camera state for dispose: " + cameraState); - } - } - } - - // Blocks until camera is known to be stopped. - @Override - public void stopCapture() { - final CountDownLatch cameraStoppingLatch = new CountDownLatch(1); - - Logging.d(TAG, "stopCapture"); - checkNotOnCameraThread(); - - synchronized (cameraStateLock) { - waitForCameraToStartIfStarting(); - - if (cameraState != CameraState.RUNNING) { - Logging.w(TAG, "stopCapture called for already stopped camera."); - return; - } - - postOnCameraThread(new Runnable() { - @Override - public void run() { - Logging.d(TAG, "stopCaptureOnCameraThread"); - - // Stop capture. - closeAndRelease(); - cameraStoppingLatch.countDown(); - } - }); - } - - // Wait for the stopping to start - ThreadUtils.awaitUninterruptibly(cameraStoppingLatch); - - Logging.d(TAG, "stopCapture done"); - } - - private void postOnCameraThread(Runnable runnable) { - postDelayedOnCameraThread(0 /* delayMs */, runnable); - } - - private void postDelayedOnCameraThread(int delayMs, Runnable runnable) { - synchronized (cameraStateLock) { - if ((cameraState != CameraState.STARTING && cameraState != CameraState.RUNNING) - || !cameraThreadHandler.postAtTime( - runnable, this /* token */, SystemClock.uptimeMillis() + delayMs)) { - Logging.w(TAG, "Runnable not scheduled even though it was requested."); - } - } - } - - private int getDeviceOrientation() { - int orientation = 0; - - WindowManager wm = (WindowManager) applicationContext.getSystemService( - Context.WINDOW_SERVICE); - switch(wm.getDefaultDisplay().getRotation()) { - case Surface.ROTATION_90: - orientation = 90; - break; - case Surface.ROTATION_180: - orientation = 180; - break; - case Surface.ROTATION_270: - orientation = 270; - break; - case Surface.ROTATION_0: - default: - orientation = 0; - break; - } - return orientation; - } - - @Override - public void onTextureFrameAvailable( - int oesTextureId, float[] transformMatrix, long timestampNs) { - checkIsOnCameraThread(); - - if (cameraState != CameraState.RUNNING) { - Logging.d(TAG, "Texture frame received while camera was not running."); - return; - } - - if (eventsHandler != null && !firstFrameReported) { - eventsHandler.onFirstFrameAvailable(); - firstFrameReported = true; - } - - int rotation; - if (isFrontCamera) { - // Undo the mirror that the OS "helps" us with. - // http://developer.android.com/reference/android/hardware/Camera.html#setDisplayOrientation(int) - rotation = cameraOrientation + getDeviceOrientation(); - transformMatrix = - RendererCommon.multiplyMatrices(transformMatrix, RendererCommon.horizontalFlipMatrix()); - } else { - rotation = cameraOrientation - getDeviceOrientation(); - } - // Make sure |rotation| is between 0 and 360. - rotation = (360 + rotation % 360) % 360; - - // Undo camera orientation - we report it as rotation instead. - transformMatrix = RendererCommon.rotateTextureMatrix(transformMatrix, -cameraOrientation); - - cameraStatistics.addFrame(); - capturerObserver.onTextureFrameCaptured(captureFormat.width, captureFormat.height, oesTextureId, - transformMatrix, rotation, timestampNs); + protected void createCameraSession( + CameraSession.CreateSessionCallback createSessionCallback, + CameraEventsHandler eventsHandler, Context applicationContext, + CameraVideoCapturer.CapturerObserver capturerObserver, + SurfaceTextureHelper surfaceTextureHelper, + String cameraName, int width, int height, int framerate) { + Camera2Session.create( + cameraManager, + createSessionCallback, + eventsHandler, applicationContext, + capturerObserver, + surfaceTextureHelper, + cameraName, width, height, framerate); } } diff --git a/webrtc/api/android/java/src/org/webrtc/Camera2Session.java b/webrtc/api/android/java/src/org/webrtc/Camera2Session.java new file mode 100644 index 0000000000..5e32c27102 --- /dev/null +++ b/webrtc/api/android/java/src/org/webrtc/Camera2Session.java @@ -0,0 +1,409 @@ +/* + * Copyright 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +package org.webrtc; + +import org.webrtc.CameraEnumerationAndroid.CaptureFormat; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.SurfaceTexture; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.CameraManager; +import android.hardware.camera2.CameraMetadata; +import android.hardware.camera2.CaptureFailure; +import android.hardware.camera2.CaptureRequest; +import android.os.Handler; +import android.util.Range; +import android.view.Surface; +import android.view.WindowManager; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CountDownLatch; + +@TargetApi(21) +public class Camera2Session implements CameraSession { + private static final String TAG = "Camera2Session"; + + private static enum SessionState { RUNNING, STOPPED }; + + private final Handler cameraThreadHandler; + private final CameraManager cameraManager; + private final CreateSessionCallback callback; + private final CameraVideoCapturer.CameraEventsHandler eventsHandler; + private final Context applicationContext; + private final CameraVideoCapturer.CapturerObserver capturerObserver; + private final SurfaceTextureHelper surfaceTextureHelper; + private final String cameraId; + private final int width; + private final int height; + private final int framerate; + + // Initialized at start + private CameraCharacteristics cameraCharacteristics; + private int cameraOrientation; + private boolean isCameraFrontFacing; + private int fpsUnitFactor; + private CaptureFormat captureFormat; + + // Initialized when camera opens + private CameraDevice cameraDevice; + private Surface surface; + + // Initialized when capture session is created + private CameraCaptureSession captureSession; + private CameraVideoCapturer.CameraStatistics cameraStatistics; + + // State + private SessionState state = SessionState.RUNNING; + private boolean firstFrameReported = false; + + private class CameraStateCallback extends CameraDevice.StateCallback { + private String getErrorDescription(int errorCode) { + switch (errorCode) { + case CameraDevice.StateCallback.ERROR_CAMERA_DEVICE: + return "Camera device has encountered a fatal error."; + case CameraDevice.StateCallback.ERROR_CAMERA_DISABLED: + return "Camera device could not be opened due to a device policy."; + case CameraDevice.StateCallback.ERROR_CAMERA_IN_USE: + return "Camera device is in use already."; + case CameraDevice.StateCallback.ERROR_CAMERA_SERVICE: + return "Camera service has encountered a fatal error."; + case CameraDevice.StateCallback.ERROR_MAX_CAMERAS_IN_USE: + return "Camera device could not be opened because" + + " there are too many other open camera devices."; + default: + return "Unknown camera error: " + errorCode; + } + } + + @Override + public void onDisconnected(CameraDevice camera) { + checkIsOnCameraThread(); + reportError("Camera disconnected."); + } + + @Override + public void onError(CameraDevice camera, int errorCode) { + checkIsOnCameraThread(); + reportError(getErrorDescription(errorCode)); + } + + @Override + public void onOpened(CameraDevice camera) { + checkIsOnCameraThread(); + + Logging.d(TAG, "Camera opened."); + cameraDevice = camera; + + final SurfaceTexture surfaceTexture = surfaceTextureHelper.getSurfaceTexture(); + surfaceTexture.setDefaultBufferSize(captureFormat.width, captureFormat.height); + surface = new Surface(surfaceTexture); + try { + camera.createCaptureSession( + Arrays.asList(surface), new CaptureSessionCallback(), cameraThreadHandler); + } catch (CameraAccessException e) { + reportError("Failed to create capture session. " + e); + return; + } + } + + @Override + public void onClosed(CameraDevice camera) { + checkIsOnCameraThread(); + + Logging.d(TAG, "Camera device closed."); + eventsHandler.onCameraClosed(); + } + } + + private class CaptureSessionCallback extends CameraCaptureSession.StateCallback { + @Override + public void onConfigureFailed(CameraCaptureSession session) { + checkIsOnCameraThread(); + session.close(); + reportError("Failed to configure capture session."); + } + + @Override + public void onConfigured(CameraCaptureSession session) { + checkIsOnCameraThread(); + Logging.d(TAG, "Camera capture session configured."); + captureSession = session; + try { + /* + * The viable options for video capture requests are: + * TEMPLATE_PREVIEW: High frame rate is given priority over the highest-quality + * post-processing. + * TEMPLATE_RECORD: Stable frame rate is used, and post-processing is set for recording + * quality. + */ + final CaptureRequest.Builder captureRequestBuilder = + cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD); + // Set auto exposure fps range. + captureRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, new Range( + captureFormat.framerate.min / fpsUnitFactor, + captureFormat.framerate.max / fpsUnitFactor)); + captureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, + CaptureRequest.CONTROL_AE_MODE_ON); + captureRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false); + + captureRequestBuilder.addTarget(surface); + session.setRepeatingRequest( + captureRequestBuilder.build(), new CameraCaptureCallback(), cameraThreadHandler); + } catch (CameraAccessException e) { + reportError("Failed to start capture request. " + e); + return; + } + + surfaceTextureHelper.startListening( + new SurfaceTextureHelper.OnTextureFrameAvailableListener() { + @Override + public void onTextureFrameAvailable( + int oesTextureId, float[] transformMatrix, long timestampNs) { + checkIsOnCameraThread(); + + if (state != SessionState.RUNNING) { + Logging.d(TAG, "Texture frame captured but camera is no longer running."); + return; + } + + if (!firstFrameReported) { + eventsHandler.onFirstFrameAvailable(); + firstFrameReported = true; + } + + int rotation = getFrameOrientation(); + if (isCameraFrontFacing) { + // Undo the mirror that the OS "helps" us with. + // http://developer.android.com/reference/android/hardware/Camera.html#setDisplayOrientation(int) + transformMatrix = RendererCommon.multiplyMatrices( + transformMatrix, RendererCommon.horizontalFlipMatrix()); + } + + // Undo camera orientation - we report it as rotation instead. + transformMatrix = RendererCommon.rotateTextureMatrix( + transformMatrix, -cameraOrientation); + + cameraStatistics.addFrame(); + capturerObserver.onTextureFrameCaptured(captureFormat.width, captureFormat.height, + oesTextureId, transformMatrix, rotation, timestampNs); + } + }); + capturerObserver.onCapturerStarted(true /* success */); + cameraStatistics = new CameraVideoCapturer.CameraStatistics( + surfaceTextureHelper, eventsHandler); + Logging.d(TAG, "Camera device successfully started."); + callback.onDone(Camera2Session.this); + } + } + + private class CameraCaptureCallback extends CameraCaptureSession.CaptureCallback { + @Override + public void onCaptureFailed( + CameraCaptureSession session, CaptureRequest request, CaptureFailure failure) { + Logging.d(TAG, "Capture failed: " + failure); + } + } + + public static void create( + CameraManager cameraManager, CreateSessionCallback callback, + CameraVideoCapturer.CameraEventsHandler eventsHandler, Context applicationContext, + CameraVideoCapturer.CapturerObserver capturerObserver, + SurfaceTextureHelper surfaceTextureHelper, + String cameraId, int width, int height, int framerate) { + new Camera2Session( + cameraManager, callback, + eventsHandler, applicationContext, + capturerObserver, + surfaceTextureHelper, + cameraId, width, height, framerate); + } + + private Camera2Session( + CameraManager cameraManager, CreateSessionCallback callback, + CameraVideoCapturer.CameraEventsHandler eventsHandler, Context applicationContext, + CameraVideoCapturer.CapturerObserver capturerObserver, + SurfaceTextureHelper surfaceTextureHelper, + String cameraId, int width, int height, int framerate) { + Logging.d(TAG, "Create new camera2 session on camera " + cameraId); + + this.cameraThreadHandler = new Handler(); + this.cameraManager = cameraManager; + this.callback = callback; + this.eventsHandler = eventsHandler; + this.applicationContext = applicationContext; + this.capturerObserver = capturerObserver; + this.surfaceTextureHelper = surfaceTextureHelper; + this.cameraId = cameraId; + this.width = width; + this.height = height; + this.framerate = framerate; + + start(); + } + + private void start() { + checkIsOnCameraThread(); + Logging.d(TAG, "start"); + + try { + cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraId); + } catch (final CameraAccessException e) { + reportError("getCameraCharacteristics(): " + e.getMessage()); + } + cameraOrientation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); + isCameraFrontFacing = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) + == CameraMetadata.LENS_FACING_FRONT; + + findCaptureFormat(); + openCamera(); + } + + private void findCaptureFormat() { + checkIsOnCameraThread(); + + Range[] fpsRanges = + cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); + fpsUnitFactor = Camera2Enumerator.getFpsUnitFactor(fpsRanges); + List framerateRanges = + Camera2Enumerator.convertFramerates(fpsRanges, fpsUnitFactor); + List sizes = Camera2Enumerator.getSupportedSizes(cameraCharacteristics); + + if (framerateRanges.isEmpty() || sizes.isEmpty()) { + reportError("No supported capture formats."); + } + + final CaptureFormat.FramerateRange bestFpsRange = + CameraEnumerationAndroid.getClosestSupportedFramerateRange( + framerateRanges, framerate); + + final Size bestSize = CameraEnumerationAndroid.getClosestSupportedSize( + sizes, width, height); + + captureFormat = new CaptureFormat(bestSize.width, bestSize.height, bestFpsRange); + Logging.d(TAG, "Using capture format: " + captureFormat); + } + + private void openCamera() { + checkIsOnCameraThread(); + + Logging.d(TAG, "Opening camera " + cameraId); + int cameraIndex = -1; + try { + cameraIndex = Integer.parseInt(cameraId); + } catch (NumberFormatException e) { + Logging.d(TAG, "External camera with non-int identifier: " + cameraId); + } + eventsHandler.onCameraOpening(cameraIndex); + + try { + cameraManager.openCamera(cameraId, new CameraStateCallback(), cameraThreadHandler); + } catch (CameraAccessException e) { + reportError("Failed to open camera: " + e); + } + } + + @Override + public void stop() { + Logging.d(TAG, "Stop camera2 session on camera " + cameraId); + final CountDownLatch stopLatch = new CountDownLatch(1); + + cameraThreadHandler.post(new Runnable() { + @Override + public void run() { + if (state != SessionState.STOPPED) { + state = SessionState.STOPPED; + capturerObserver.onCapturerStopped(); + stopLatch.countDown(); + stopInternal(); + } + } + }); + + ThreadUtils.awaitUninterruptibly(stopLatch); + } + + private void stopInternal() { + Logging.d(TAG, "Stop internal"); + checkIsOnCameraThread(); + + surfaceTextureHelper.stopListening(); + cameraStatistics.release(); + + captureSession.close(); + captureSession = null; + surface.release(); + surface = null; + cameraDevice.close(); + cameraDevice = null; + + Logging.d(TAG, "Stop done"); + } + + private void reportError(String error) { + checkIsOnCameraThread(); + Logging.e(TAG, "Error: " + error); + + if (captureSession == null) { + if (cameraDevice != null) { + cameraDevice.close(); + cameraDevice = null; + } + + state = SessionState.STOPPED; + callback.onFailure(error); + capturerObserver.onCapturerStarted(false /* success */); + } else { + eventsHandler.onCameraError(error); + } + } + + private int getDeviceOrientation() { + int orientation = 0; + + WindowManager wm = (WindowManager) applicationContext.getSystemService( + Context.WINDOW_SERVICE); + switch(wm.getDefaultDisplay().getRotation()) { + case Surface.ROTATION_90: + orientation = 90; + break; + case Surface.ROTATION_180: + orientation = 180; + break; + case Surface.ROTATION_270: + orientation = 270; + break; + case Surface.ROTATION_0: + default: + orientation = 0; + break; + } + return orientation; + } + + private int getFrameOrientation() { + int rotation = getDeviceOrientation(); + if (!isCameraFrontFacing) { + rotation = 360 - rotation; + } + return (cameraOrientation + rotation) % 360; + } + + private void checkIsOnCameraThread() { + if (Thread.currentThread() != cameraThreadHandler.getLooper().getThread()) { + throw new IllegalStateException("Wrong thread"); + } + } +} diff --git a/webrtc/api/android/java/src/org/webrtc/CameraCapturer.java b/webrtc/api/android/java/src/org/webrtc/CameraCapturer.java new file mode 100644 index 0000000000..75712cc570 --- /dev/null +++ b/webrtc/api/android/java/src/org/webrtc/CameraCapturer.java @@ -0,0 +1,285 @@ +/* + * Copyright 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +package org.webrtc; + +import org.webrtc.CameraEnumerationAndroid.CaptureFormat; + +import android.content.Context; +import android.os.Handler; +import android.os.SystemClock; + +import java.util.Arrays; +import java.util.List; + +@SuppressWarnings("deprecation") +public abstract class CameraCapturer implements CameraVideoCapturer { + private static final String TAG = "CameraCapturer"; + private final static int MAX_OPEN_CAMERA_ATTEMPTS = 3; + private final static int OPEN_CAMERA_DELAY_MS = 500; + + private final CameraEnumerator cameraEnumerator; + private final CameraEventsHandler eventsHandler; + + private final CameraSession.CreateSessionCallback createSessionCallback = + new CameraSession.CreateSessionCallback() { + @Override + public void onDone(CameraSession session) { + Logging.d(TAG, "Create session done"); + + synchronized (stateLock) { + sessionOpening = false; + currentSession = session; + stateLock.notifyAll(); + + if (switchEventsHandler != null) { + switchEventsHandler.onCameraSwitchDone( + cameraEnumerator.isFrontFacing(cameraName)); + switchEventsHandler = null; + } + switchInProgress = false; + } + } + + @Override + public void onFailure(String error) { + synchronized (stateLock) { + openAttemptsRemaining--; + + if (openAttemptsRemaining <= 0) { + Logging.w(TAG, "Opening camera failed, passing: " + error); + sessionOpening = false; + stateLock.notifyAll(); + + if (switchEventsHandler != null) { + switchEventsHandler.onCameraSwitchError(error); + switchEventsHandler = null; + } + switchInProgress = false; + + eventsHandler.onCameraError(error); + } else { + Logging.w(TAG, "Opening camera failed, retry: " + error); + + createSessionInternal(OPEN_CAMERA_DELAY_MS); + } + } + } + }; + + // Initialized on initialize + // ------------------------- + // Use postOnCameraThread() instead of posting directly to the handler - this way all + // callbacks with a specifed token can be removed at once. + private Handler cameraThreadHandler; + private Context applicationContext; + private CapturerObserver capturerObserver; + private SurfaceTextureHelper surfaceHelper; + + private final Object stateLock = new Object(); + private boolean sessionOpening; /* guarded by stateLock */ + private CameraSession currentSession; /* guarded by stateLock */ + private String cameraName; /* guarded by stateLock */ + private int width; /* guarded by stateLock */ + private int height; /* guarded by stateLock */ + private int framerate; /* guarded by stateLock */ + private int openAttemptsRemaining; /* guarded by stateLock */ + private boolean switchInProgress; /* guarded by stateLock */ + private CameraSwitchHandler switchEventsHandler; /* guarded by stateLock */ + + public CameraCapturer( + String cameraName, CameraEventsHandler eventsHandler, CameraEnumerator cameraEnumerator) { + if (eventsHandler == null) { + eventsHandler = new CameraEventsHandler() { + @Override + public void onCameraError(String errorDescription) {} + @Override + public void onCameraFreezed(String errorDescription) {} + @Override + public void onCameraOpening(int cameraId) {} + @Override + public void onFirstFrameAvailable() {} + @Override + public void onCameraClosed() {} + }; + } + + this.eventsHandler = eventsHandler; + this.cameraEnumerator = cameraEnumerator; + this.cameraName = cameraName; + + final String[] deviceNames = cameraEnumerator.getDeviceNames(); + + if (deviceNames.length == 0) { + throw new RuntimeException("No cameras attached."); + } + if (!Arrays.asList(deviceNames).contains(this.cameraName)) { + throw new IllegalArgumentException( + "Camera name " + this.cameraName + " does not match any known camera device."); + } + } + + @Override + public void initialize(SurfaceTextureHelper surfaceTextureHelper, Context applicationContext, + CapturerObserver capturerObserver) { + this.applicationContext = applicationContext; + this.capturerObserver = capturerObserver; + this.surfaceHelper = surfaceTextureHelper; + this.cameraThreadHandler = + surfaceTextureHelper == null ? null : surfaceTextureHelper.getHandler(); + } + + @Override + public void startCapture(int width, int height, int framerate) { + Logging.d(TAG, "startCapture: " + width + "x" + height + "@" + framerate); + + synchronized (stateLock) { + if (sessionOpening || currentSession != null) { + Logging.w(TAG, "Session already open"); + return; + } + + this.width = width; + this.height = height; + this.framerate = framerate; + + sessionOpening = true; + openAttemptsRemaining = MAX_OPEN_CAMERA_ATTEMPTS; + createSessionInternal(0); + } + } + + private void createSessionInternal(int delayMs) { + cameraThreadHandler.postDelayed(new Runnable() { + @Override + public void run() { + createCameraSession( + createSessionCallback, + eventsHandler, applicationContext, capturerObserver, surfaceHelper, + cameraName, width, height, framerate); + } + }, delayMs); + } + + @Override + public void stopCapture() { + Logging.d(TAG, "Stop capture"); + + synchronized (stateLock) { + while (sessionOpening) { + Logging.d(TAG, "Stop capture: Waiting for session to open"); + ThreadUtils.waitUninterruptibly(stateLock); + } + + if (currentSession != null) { + Logging.d(TAG, "Stop capture: Stopping session"); + currentSession.stop(); + currentSession = null; + } else { + Logging.d(TAG, "Stop capture: No session open"); + } + } + + Logging.d(TAG, "Stop capture done"); + } + + @Override + public void onOutputFormatRequest(final int width, final int height, final int framerate) { + cameraThreadHandler.post(new Runnable() { + @Override public void run() { + Logging.d(TAG, "onOutputFormatRequestOnCameraThread: " + width + "x" + height + + "@" + framerate); + capturerObserver.onOutputFormatRequest(width, height, framerate); + } + }); + } + + @Override + public void changeCaptureFormat(int width, int height, int framerate) { + Logging.d(TAG, "changeCaptureFormat: " + width + "x" + height + "@" + framerate); + synchronized (stateLock) { + stopCapture(); + startCapture(width, height, framerate); + } + } + + @Override + public void dispose() { + Logging.d(TAG, "dispose"); + stopCapture(); + } + + @Override + public void switchCamera(final CameraSwitchHandler switchEventsHandler) { + Logging.d(TAG, "switchCamera"); + + final String[] deviceNames = cameraEnumerator.getDeviceNames(); + + if (deviceNames.length < 2) { + if (switchEventsHandler != null) { + switchEventsHandler.onCameraSwitchError("No camera to switch to."); + } + return; + } + + synchronized (stateLock) { + if (switchInProgress) { + Logging.d(TAG, "switchCamera switchInProgress"); + if (switchEventsHandler != null) { + switchEventsHandler.onCameraSwitchError("Camera switch already in progress."); + } + return; + } + + if (sessionOpening) { + Logging.d(TAG, "switchCamera sessionOpening"); + if (switchEventsHandler != null) { + switchEventsHandler.onCameraSwitchError("Session is still opening."); + } + return; + } + + if (currentSession == null) { + Logging.d(TAG, "switchCamera: No session open"); + if (switchEventsHandler != null) { + switchEventsHandler.onCameraSwitchError("Camera is not running."); + } + return; + } + + Logging.d(TAG, "switchCamera: Stopping session"); + currentSession.stop(); + currentSession = null; + + int cameraNameIndex = Arrays.asList(deviceNames).indexOf(cameraName); + cameraName = deviceNames[(cameraNameIndex + 1) % deviceNames.length]; + + switchInProgress = true; + this.switchEventsHandler = switchEventsHandler; + sessionOpening = true; + openAttemptsRemaining = 1; + createSessionInternal(0); + } + Logging.d(TAG, "switchCamera done"); + } + + protected String getCameraName() { + synchronized (stateLock) { + return cameraName; + } + } + + abstract protected void createCameraSession( + CameraSession.CreateSessionCallback createSessionCallback, + CameraEventsHandler eventsHandler, Context applicationContext, + CameraVideoCapturer.CapturerObserver capturerObserver, + SurfaceTextureHelper surfaceTextureHelper, + String cameraName, int width, int height, int framerate); +} diff --git a/webrtc/api/android/java/src/org/webrtc/CameraSession.java b/webrtc/api/android/java/src/org/webrtc/CameraSession.java new file mode 100644 index 0000000000..c6b73a8bf1 --- /dev/null +++ b/webrtc/api/android/java/src/org/webrtc/CameraSession.java @@ -0,0 +1,24 @@ +/* + * Copyright 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +package org.webrtc; + +public interface CameraSession { + public interface CreateSessionCallback { + void onDone(CameraSession session); + void onFailure(String error); + } + + /** + * Stops the capture. Waits until no more calls to capture observer will be made. + * If waitCameraStop is true, also waits for the camera to stop. + */ + void stop(); +} diff --git a/webrtc/api/androidtests/src/org/webrtc/Camera1CapturerUsingByteBufferTest.java b/webrtc/api/androidtests/src/org/webrtc/Camera1CapturerUsingByteBufferTest.java index 2b68702feb..fa827f7208 100644 --- a/webrtc/api/androidtests/src/org/webrtc/Camera1CapturerUsingByteBufferTest.java +++ b/webrtc/api/androidtests/src/org/webrtc/Camera1CapturerUsingByteBufferTest.java @@ -21,13 +21,6 @@ public class Camera1CapturerUsingByteBufferTest extends InstrumentationTestCase private class TestObjectFactory extends CameraVideoCapturerTestFixtures.TestObjectFactory { - @Override - public CameraVideoCapturer createCapturer( - String name, - CameraVideoCapturer.CameraEventsHandler eventsHandler) { - return new VideoCapturerAndroid(name, eventsHandler, isCapturingToTexture()); - } - @Override public boolean isCapturingToTexture() { return false; @@ -35,7 +28,7 @@ public class Camera1CapturerUsingByteBufferTest extends InstrumentationTestCase @Override public CameraEnumerator getCameraEnumerator() { - return new Camera1Enumerator(); + return new Camera1Enumerator(false); } @Override @@ -69,12 +62,12 @@ public class Camera1CapturerUsingByteBufferTest extends InstrumentationTestCase } @SmallTest - public void testCreateAndDispose() { + public void testCreateAndDispose() throws InterruptedException { fixtures.createCapturerAndDispose(); } @SmallTest - public void testCreateNonExistingCamera() { + public void testCreateNonExistingCamera() throws InterruptedException { fixtures.createNonExistingCamera(); } diff --git a/webrtc/api/androidtests/src/org/webrtc/Camera1CapturerUsingTextureTest.java b/webrtc/api/androidtests/src/org/webrtc/Camera1CapturerUsingTextureTest.java index b8d31bdf58..1674edc945 100644 --- a/webrtc/api/androidtests/src/org/webrtc/Camera1CapturerUsingTextureTest.java +++ b/webrtc/api/androidtests/src/org/webrtc/Camera1CapturerUsingTextureTest.java @@ -57,12 +57,12 @@ public class Camera1CapturerUsingTextureTest extends InstrumentationTestCase { } @SmallTest - public void testCreateAndDispose() { + public void testCreateAndDispose() throws InterruptedException { fixtures.createCapturerAndDispose(); } @SmallTest - public void testCreateNonExistingCamera() { + public void testCreateNonExistingCamera() throws InterruptedException { fixtures.createNonExistingCamera(); } diff --git a/webrtc/api/androidtests/src/org/webrtc/Camera2CapturerTest.java b/webrtc/api/androidtests/src/org/webrtc/Camera2CapturerTest.java index f7f900be3f..349b7da4ff 100644 --- a/webrtc/api/androidtests/src/org/webrtc/Camera2CapturerTest.java +++ b/webrtc/api/androidtests/src/org/webrtc/Camera2CapturerTest.java @@ -181,12 +181,12 @@ public class Camera2CapturerTest extends InstrumentationTestCase { } @SmallTest - public void testCreateAndDispose() { + public void testCreateAndDispose() throws InterruptedException { fixtures.createCapturerAndDispose(); } @SmallTest - public void testCreateNonExistingCamera() { + public void testCreateNonExistingCamera() throws InterruptedException { fixtures.createNonExistingCamera(); } diff --git a/webrtc/api/androidtests/src/org/webrtc/CameraVideoCapturerTestFixtures.java b/webrtc/api/androidtests/src/org/webrtc/CameraVideoCapturerTestFixtures.java index 7f89ac8a61..f35d50957c 100644 --- a/webrtc/api/androidtests/src/org/webrtc/CameraVideoCapturerTestFixtures.java +++ b/webrtc/api/androidtests/src/org/webrtc/CameraVideoCapturerTestFixtures.java @@ -191,9 +191,13 @@ class CameraVideoCapturerTestFixtures { public boolean onFirstFrameAvailableCalled; public final Object onCameraFreezedLock = new Object(); private String onCameraFreezedDescription; + public final Object cameraClosedLock = new Object(); + private boolean cameraClosed = true; @Override public void onCameraError(String errorDescription) { + Logging.w(TAG, "Camera error: " + errorDescription); + cameraClosed = true; } @Override @@ -207,6 +211,9 @@ class CameraVideoCapturerTestFixtures { @Override public void onCameraOpening(int cameraId) { onCameraOpeningCalled = true; + synchronized (cameraClosedLock) { + cameraClosed = false; + } } @Override @@ -215,7 +222,12 @@ class CameraVideoCapturerTestFixtures { } @Override - public void onCameraClosed() { } + public void onCameraClosed() { + synchronized (cameraClosedLock) { + cameraClosed = true; + cameraClosedLock.notifyAll(); + } + } public String waitForCameraFreezed() throws InterruptedException { Logging.d(TAG, "Waiting for the camera to freeze"); @@ -224,6 +236,15 @@ class CameraVideoCapturerTestFixtures { return onCameraFreezedDescription; } } + + public void waitForCameraClosed() throws InterruptedException { + synchronized (cameraClosedLock) { + while (!cameraClosed) { + Logging.d(TAG, "Waiting for the camera to close."); + cameraClosedLock.wait(); + } + } + } } /** @@ -332,7 +353,8 @@ class CameraVideoCapturerTestFixtures { } private CapturerInstance createCapturer(boolean initialize) { - return createCapturer("", initialize); + String name = testObjectFactory.cameraEnumerator.getDeviceNames()[0]; + return createCapturer(name, initialize); } private void startCapture(CapturerInstance instance) { @@ -347,12 +369,9 @@ class CameraVideoCapturerTestFixtures { instance.format = format; } - private void disposeCapturer(CapturerInstance instance) { - try { - instance.capturer.stopCapture(); - } catch (InterruptedException e) { - // TODO(sakal): Remove this once stopCapture no longer throws InterruptedException - } + private void disposeCapturer(CapturerInstance instance) throws InterruptedException { + instance.capturer.stopCapture(); + instance.cameraEvents.waitForCameraClosed(); instance.capturer.dispose(); instance.surfaceTextureHelper.returnTextureFrame(); instance.surfaceTextureHelper.dispose(); @@ -416,11 +435,11 @@ class CameraVideoCapturerTestFixtures { } // Test methods - public void createCapturerAndDispose() { + public void createCapturerAndDispose() throws InterruptedException { disposeCapturer(createCapturer(true /* initialize */)); } - public void createNonExistingCamera() { + public void createNonExistingCamera() throws InterruptedException { try { disposeCapturer(createCapturer("non-existing camera", false /* initialize */)); } catch (IllegalArgumentException e) { @@ -431,7 +450,8 @@ class CameraVideoCapturerTestFixtures { } public void createCapturerAndRender() throws InterruptedException { - createCapturerAndRender(""); + String name = testObjectFactory.cameraEnumerator.getDeviceNames()[0]; + createCapturerAndRender(name); } public void createFrontFacingCapturerAndRender() throws InterruptedException { @@ -452,6 +472,8 @@ class CameraVideoCapturerTestFixtures { final CapturerInstance capturerInstance = createCapturer(false /* initialize */); final VideoTrackWithRenderer videoTrackWithRenderer = createVideoTrackWithRenderer(capturerInstance.capturer); + // Wait for the camera to start so we can switch it + assertTrue(videoTrackWithRenderer.rendererCallbacks.waitForNextFrameToRender() > 0); // Array with one element to avoid final problem in nested classes. final boolean[] cameraSwitchSuccessful = new boolean[1]; @@ -485,7 +507,6 @@ class CameraVideoCapturerTestFixtures { // Make sure camera is started and first frame is received and then stop it. assertTrue(capturerInstance.observer.waitForCapturerToStart()); capturerInstance.observer.waitForNextCapturedFrame(); - capturerInstance.capturer.stopCapture(); disposeCapturer(capturerInstance); assertTrue(capturerInstance.cameraEvents.onCameraOpeningCalled); @@ -670,7 +691,6 @@ class CameraVideoCapturerTestFixtures { if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.LOLLIPOP_MR1) { // The first opened camera client will be evicted. assertTrue(capturerInstance.observer.waitForCapturerToStart()); - capturerInstance.capturer.stopCapture(); } else { assertFalse(capturerInstance.observer.waitForCapturerToStart()); } @@ -699,7 +719,6 @@ class CameraVideoCapturerTestFixtures { Logging.d(TAG, "startWhileCameraIsAlreadyOpenAndCloseCamera: Waiting for capture to start."); videoTrackWithRenderer.rendererCallbacks.waitForNextFrameToRender(); Logging.d(TAG, "startWhileCameraIsAlreadyOpenAndCloseCamera: Stopping capture."); - capturerInstance.capturer.stopCapture(); disposeCapturer(capturerInstance); } @@ -711,8 +730,6 @@ class CameraVideoCapturerTestFixtures { final Object competingCamera = testObjectFactory.rawOpenCamera(cameraName); startCapture(capturerInstance); - - capturerInstance.capturer.stopCapture(); disposeCapturer(capturerInstance); testObjectFactory.rawCloseCamera(competingCamera); diff --git a/webrtc/base/java/src/org/webrtc/ThreadUtils.java b/webrtc/base/java/src/org/webrtc/ThreadUtils.java index 95d41ba563..cf814b0f53 100644 --- a/webrtc/base/java/src/org/webrtc/ThreadUtils.java +++ b/webrtc/base/java/src/org/webrtc/ThreadUtils.java @@ -136,6 +136,15 @@ public class ThreadUtils { return result; } + public static void waitUninterruptibly(final Object object) { + executeUninterruptibly(new BlockingOperation() { + @Override + public void run() throws InterruptedException { + object.wait(); + } + }); + } + /** * Post |callable| to |handler| and wait for the result. */