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}
This commit is contained in:
parent
bd59c71ff8
commit
d30e0ad3e7
@ -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",
|
||||
|
||||
@ -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<Integer>[] fpsRanges =
|
||||
cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES);
|
||||
fpsUnitFactor = Camera2Enumerator.getFpsUnitFactor(fpsRanges);
|
||||
List<CaptureFormat.FramerateRange> framerateRanges =
|
||||
Camera2Enumerator.convertFramerates(fpsRanges, fpsUnitFactor);
|
||||
List<Size> 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<Integer>(
|
||||
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<CaptureFormat> 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);
|
||||
}
|
||||
}
|
||||
|
||||
409
webrtc/api/android/java/src/org/webrtc/Camera2Session.java
Normal file
409
webrtc/api/android/java/src/org/webrtc/Camera2Session.java
Normal file
@ -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<Integer>(
|
||||
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<Integer>[] fpsRanges =
|
||||
cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES);
|
||||
fpsUnitFactor = Camera2Enumerator.getFpsUnitFactor(fpsRanges);
|
||||
List<CaptureFormat.FramerateRange> framerateRanges =
|
||||
Camera2Enumerator.convertFramerates(fpsRanges, fpsUnitFactor);
|
||||
List<Size> 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
285
webrtc/api/android/java/src/org/webrtc/CameraCapturer.java
Normal file
285
webrtc/api/android/java/src/org/webrtc/CameraCapturer.java
Normal file
@ -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);
|
||||
}
|
||||
24
webrtc/api/android/java/src/org/webrtc/CameraSession.java
Normal file
24
webrtc/api/android/java/src/org/webrtc/CameraSession.java
Normal file
@ -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();
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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.
|
||||
*/
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user