| Index: webrtc/api/java/android/org/webrtc/VideoCapturerAndroid.java
|
| diff --git a/webrtc/api/java/android/org/webrtc/VideoCapturerAndroid.java b/webrtc/api/java/android/org/webrtc/VideoCapturerAndroid.java
|
| index 5c9b77b4cccfd0175dd15816ffff2fb827e5de58..659a48440a7532a11b214980fabafc848d11f181 100644
|
| --- a/webrtc/api/java/android/org/webrtc/VideoCapturerAndroid.java
|
| +++ b/webrtc/api/java/android/org/webrtc/VideoCapturerAndroid.java
|
| @@ -51,9 +51,13 @@ public class VideoCapturerAndroid implements
|
| private final static int CAMERA_OBSERVER_PERIOD_MS = 2000;
|
| private final static int CAMERA_FREEZE_REPORT_TIMOUT_MS = 4000;
|
|
|
| + private boolean isDisposed = false;
|
| private android.hardware.Camera camera; // Only non-null while capturing.
|
| - private Thread cameraThread;
|
| - private final Handler cameraThreadHandler;
|
| + private final Object handlerLock = new Object();
|
| + // |cameraThreadHandler| must be synchronized on |handlerLock| when not on the camera thread,
|
| + // or when modifying the reference. Use maybePostOnCameraThread() 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;
|
| // Synchronization lock for |id|.
|
| private final Object cameraIdLock = new Object();
|
| @@ -81,9 +85,6 @@ public class VideoCapturerAndroid implements
|
| // The camera API can output one old frame after the camera has been switched or the resolution
|
| // has been changed. This flag is used for dropping the first frame after camera restart.
|
| private boolean dropNextFrame = false;
|
| - // |openCameraOnCodecThreadRunner| is used for retrying to open the camera if it is in use by
|
| - // another application when startCaptureOnCameraThread is called.
|
| - private Runnable openCameraOnCodecThreadRunner;
|
| private final static int MAX_OPEN_CAMERA_ATTEMPTS = 3;
|
| private final static int OPEN_CAMERA_DELAY_MS = 500;
|
| private int openCameraAttempts;
|
| @@ -132,7 +133,7 @@ public class VideoCapturerAndroid implements
|
| } else {
|
| freezePeriodCount = 0;
|
| }
|
| - cameraThreadHandler.postDelayed(this, CAMERA_OBSERVER_PERIOD_MS);
|
| + maybePostDelayedOnCameraThread(CAMERA_OBSERVER_PERIOD_MS, this);
|
| }
|
| };
|
|
|
| @@ -199,6 +200,12 @@ public class VideoCapturerAndroid implements
|
| }
|
|
|
| public void printStackTrace() {
|
| + Thread cameraThread = null;
|
| + synchronized (handlerLock) {
|
| + if (cameraThreadHandler != null) {
|
| + cameraThread = cameraThreadHandler.getLooper().getThread();
|
| + }
|
| + }
|
| if (cameraThread != null) {
|
| StackTraceElement[] cameraStackTraces = cameraThread.getStackTrace();
|
| if (cameraStackTraces.length > 0) {
|
| @@ -212,10 +219,10 @@ public class VideoCapturerAndroid implements
|
|
|
| // Switch camera to the next valid camera id. This can only be called while
|
| // the camera is running.
|
| - public void switchCamera(final CameraSwitchHandler handler) {
|
| + public void switchCamera(final CameraSwitchHandler switchEventsHandler) {
|
| if (android.hardware.Camera.getNumberOfCameras() < 2) {
|
| - if (handler != null) {
|
| - handler.onCameraSwitchError("No camera to switch to.");
|
| + if (switchEventsHandler != null) {
|
| + switchEventsHandler.onCameraSwitchError("No camera to switch to.");
|
| }
|
| return;
|
| }
|
| @@ -224,31 +231,29 @@ public class VideoCapturerAndroid implements
|
| // Do not handle multiple camera switch request to avoid blocking
|
| // camera thread by handling too many switch request from a queue.
|
| Logging.w(TAG, "Ignoring camera switch request.");
|
| - if (handler != null) {
|
| - handler.onCameraSwitchError("Pending camera switch already in progress.");
|
| + if (switchEventsHandler != null) {
|
| + switchEventsHandler.onCameraSwitchError("Pending camera switch already in progress.");
|
| }
|
| return;
|
| }
|
| pendingCameraSwitch = true;
|
| }
|
| - cameraThreadHandler.post(new Runnable() {
|
| - @Override public void run() {
|
| - if (camera == null) {
|
| - if (handler != null) {
|
| - handler.onCameraSwitchError("Camera is stopped.");
|
| - }
|
| - return;
|
| - }
|
| + final boolean didPost = maybePostOnCameraThread(new Runnable() {
|
| + @Override
|
| + public void run() {
|
| switchCameraOnCameraThread();
|
| synchronized (pendingCameraSwitchLock) {
|
| pendingCameraSwitch = false;
|
| }
|
| - if (handler != null) {
|
| - handler.onCameraSwitchDone(
|
| + if (switchEventsHandler != null) {
|
| + switchEventsHandler.onCameraSwitchDone(
|
| info.facing == android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT);
|
| }
|
| }
|
| });
|
| + if (!didPost && switchEventsHandler != null) {
|
| + switchEventsHandler.onCameraSwitchError("Camera is stopped.");
|
| + }
|
| }
|
|
|
| // Requests a new output format from the video capturer. Captured frames
|
| @@ -257,7 +262,7 @@ public class VideoCapturerAndroid implements
|
| // the same result as |width| = 480, |height| = 640.
|
| // TODO(magjed/perkj): Document what this function does. Change name?
|
| public void onOutputFormatRequest(final int width, final int height, final int framerate) {
|
| - cameraThreadHandler.post(new Runnable() {
|
| + maybePostOnCameraThread(new Runnable() {
|
| @Override public void run() {
|
| onOutputFormatRequestOnCameraThread(width, height, framerate);
|
| }
|
| @@ -267,7 +272,7 @@ public class VideoCapturerAndroid implements
|
| // Reconfigure the camera to capture in a new format. This should only be called while the camera
|
| // is running.
|
| public void changeCaptureFormat(final int width, final int height, final int framerate) {
|
| - cameraThreadHandler.post(new Runnable() {
|
| + maybePostOnCameraThread(new Runnable() {
|
| @Override public void run() {
|
| startPreviewOnCameraThread(width, height, framerate);
|
| }
|
| @@ -304,13 +309,11 @@ public class VideoCapturerAndroid implements
|
| isCapturingToTexture = (sharedContext != null);
|
| cameraStatistics = new CameraStatistics();
|
| surfaceHelper = SurfaceTextureHelper.create(sharedContext);
|
| - cameraThreadHandler = surfaceHelper.getHandler();
|
| - cameraThread = cameraThreadHandler.getLooper().getThread();
|
| Logging.d(TAG, "VideoCapturerAndroid isCapturingToTexture : " + isCapturingToTexture);
|
| }
|
|
|
| private void checkIsOnCameraThread() {
|
| - if (Thread.currentThread() != cameraThread) {
|
| + if (Thread.currentThread() != cameraThreadHandler.getLooper().getThread()) {
|
| throw new IllegalStateException("Wrong thread");
|
| }
|
| }
|
| @@ -333,29 +336,38 @@ public class VideoCapturerAndroid implements
|
| return -1;
|
| }
|
|
|
| - // Quits the camera thread. This needs to be done manually, otherwise the thread and handler will
|
| - // not be garbage collected.
|
| + private boolean maybePostOnCameraThread(Runnable runnable) {
|
| + return maybePostDelayedOnCameraThread(0 /* delayMs */, runnable);
|
| + }
|
| +
|
| + private boolean maybePostDelayedOnCameraThread(int delayMs, Runnable runnable) {
|
| + synchronized (handlerLock) {
|
| + return cameraThreadHandler != null
|
| + && cameraThreadHandler.postAtTime(
|
| + runnable, this /* token */, SystemClock.uptimeMillis() + delayMs);
|
| + }
|
| + }
|
| +
|
| + // Dispose the SurfaceTextureHelper. This needs to be done manually, otherwise the
|
| + // SurfaceTextureHelper thread and resources will not be garbage collected.
|
| @Override
|
| public void dispose() {
|
| Logging.d(TAG, "release");
|
| if (isDisposed()) {
|
| throw new IllegalStateException("Already released");
|
| }
|
| - ThreadUtils.invokeUninterruptibly(cameraThreadHandler, new Runnable() {
|
| - @Override
|
| - public void run() {
|
| - if (camera != null) {
|
| - throw new IllegalStateException("Release called while camera is running");
|
| - }
|
| + synchronized (handlerLock) {
|
| + if (cameraThreadHandler != null) {
|
| + throw new IllegalStateException("dispose() called while camera is running");
|
| }
|
| - });
|
| + }
|
| surfaceHelper.dispose();
|
| - cameraThread = null;
|
| + isDisposed = true;
|
| }
|
|
|
| // Used for testing purposes to check if dispose() has been called.
|
| public boolean isDisposed() {
|
| - return (cameraThread == null);
|
| + return isDisposed;
|
| }
|
|
|
| // Note that this actually opens the camera, and Camera callbacks run on the
|
| @@ -364,21 +376,33 @@ public class VideoCapturerAndroid implements
|
| public void startCapture(
|
| final int width, final int height, final int framerate,
|
| final Context applicationContext, final CapturerObserver frameObserver) {
|
| - Logging.d(TAG, "startCapture requested: " + width + "x" + height
|
| - + "@" + framerate);
|
| + Logging.d(TAG, "startCapture requested: " + width + "x" + height + "@" + framerate);
|
| if (applicationContext == null) {
|
| - throw new RuntimeException("applicationContext not set.");
|
| + throw new IllegalArgumentException("applicationContext not set.");
|
| }
|
| if (frameObserver == null) {
|
| - throw new RuntimeException("frameObserver not set.");
|
| + throw new IllegalArgumentException("frameObserver not set.");
|
| }
|
| -
|
| - cameraThreadHandler.post(new Runnable() {
|
| - @Override public void run() {
|
| - startCaptureOnCameraThread(width, height, framerate, frameObserver,
|
| - applicationContext);
|
| + synchronized (handlerLock) {
|
| + if (this.cameraThreadHandler != null) {
|
| + throw new RuntimeException("Camera has already been started.");
|
| }
|
| - });
|
| + this.cameraThreadHandler = surfaceHelper.getHandler();
|
| + final boolean didPost = maybePostOnCameraThread(new Runnable() {
|
| + @Override
|
| + public void run() {
|
| + openCameraAttempts = 0;
|
| + startCaptureOnCameraThread(width, height, framerate, frameObserver,
|
| + applicationContext);
|
| + }
|
| + });
|
| + if (!didPost) {
|
| + frameObserver.onCapturerStarted(false);
|
| + if (eventsHandler != null) {
|
| + eventsHandler.onCameraError("Could not post task to camera thread.");
|
| + }
|
| + }
|
| + }
|
| }
|
|
|
| private void startCaptureOnCameraThread(
|
| @@ -408,16 +432,14 @@ public class VideoCapturerAndroid implements
|
| openCameraAttempts++;
|
| if (openCameraAttempts < MAX_OPEN_CAMERA_ATTEMPTS) {
|
| Logging.e(TAG, "Camera.open failed, retrying", e);
|
| - openCameraOnCodecThreadRunner = new Runnable() {
|
| + maybePostDelayedOnCameraThread(OPEN_CAMERA_DELAY_MS, new Runnable() {
|
| @Override public void run() {
|
| startCaptureOnCameraThread(width, height, framerate, frameObserver,
|
| applicationContext);
|
| }
|
| - };
|
| - cameraThreadHandler.postDelayed(openCameraOnCodecThreadRunner, OPEN_CAMERA_DELAY_MS);
|
| + });
|
| return;
|
| }
|
| - openCameraAttempts = 0;
|
| throw e;
|
| }
|
|
|
| @@ -438,13 +460,21 @@ public class VideoCapturerAndroid implements
|
| }
|
|
|
| // Start camera observer.
|
| - cameraThreadHandler.postDelayed(cameraObserver, CAMERA_OBSERVER_PERIOD_MS);
|
| + maybePostDelayedOnCameraThread(CAMERA_OBSERVER_PERIOD_MS, cameraObserver);
|
| return;
|
| } catch (RuntimeException e) {
|
| error = e;
|
| }
|
| Logging.e(TAG, "startCapture failed", error);
|
| - stopCaptureOnCameraThread();
|
| + if (camera != null) {
|
| + // Make sure the camera is released.
|
| + stopCaptureOnCameraThread();
|
| + }
|
| + synchronized (handlerLock) {
|
| + // Remove all pending Runnables posted from |this|.
|
| + cameraThreadHandler.removeCallbacksAndMessages(this /* token */);
|
| + cameraThreadHandler = null;
|
| + }
|
| frameObserver.onCapturerStarted(false);
|
| if (eventsHandler != null) {
|
| eventsHandler.onCameraError("Camera can not be started.");
|
| @@ -542,12 +572,21 @@ public class VideoCapturerAndroid implements
|
| public void stopCapture() throws InterruptedException {
|
| Logging.d(TAG, "stopCapture");
|
| final CountDownLatch barrier = new CountDownLatch(1);
|
| - cameraThreadHandler.post(new Runnable() {
|
| - @Override public void run() {
|
| - stopCaptureOnCameraThread();
|
| - barrier.countDown();
|
| + final boolean didPost = maybePostOnCameraThread(new Runnable() {
|
| + @Override public void run() {
|
| + stopCaptureOnCameraThread();
|
| + synchronized (handlerLock) {
|
| + // Remove all pending Runnables posted from |this|.
|
| + cameraThreadHandler.removeCallbacksAndMessages(this /* token */);
|
| + cameraThreadHandler = null;
|
| }
|
| + barrier.countDown();
|
| + }
|
| });
|
| + if (!didPost) {
|
| + Logging.e(TAG, "Calling stopCapture() for already stopped camera.");
|
| + return;
|
| + }
|
| barrier.await();
|
| Logging.d(TAG, "stopCapture done");
|
| }
|
| @@ -555,18 +594,9 @@ public class VideoCapturerAndroid implements
|
| private void stopCaptureOnCameraThread() {
|
| checkIsOnCameraThread();
|
| Logging.d(TAG, "stopCaptureOnCameraThread");
|
| - if (openCameraOnCodecThreadRunner != null) {
|
| - cameraThreadHandler.removeCallbacks(openCameraOnCodecThreadRunner);
|
| - }
|
| - openCameraAttempts = 0;
|
| - if (camera == null) {
|
| - Logging.e(TAG, "Calling stopCapture() for already stopped camera.");
|
| - return;
|
| - }
|
|
|
| // Make sure onTextureFrameAvailable() is not called anymore.
|
| surfaceHelper.stopListening();
|
| - cameraThreadHandler.removeCallbacks(cameraObserver);
|
| cameraStatistics.getAndResetFrameCount();
|
| Logging.d(TAG, "Stop preview.");
|
| camera.stopPreview();
|
| @@ -645,9 +675,13 @@ public class VideoCapturerAndroid implements
|
| // Called on cameraThread so must not "synchronized".
|
| @Override
|
| public void onPreviewFrame(byte[] data, android.hardware.Camera callbackCamera) {
|
| + if (cameraThreadHandler == null) {
|
| + // The camera has been stopped.
|
| + return;
|
| + }
|
| checkIsOnCameraThread();
|
| - if (camera == null || !queuedBuffers.contains(data)) {
|
| - // The camera has been stopped or |data| is an old invalid buffer.
|
| + if (!queuedBuffers.contains(data)) {
|
| + // |data| is an old invalid buffer.
|
| return;
|
| }
|
| if (camera != callbackCamera) {
|
| @@ -671,7 +705,7 @@ public class VideoCapturerAndroid implements
|
| @Override
|
| public void onTextureFrameAvailable(
|
| int oesTextureId, float[] transformMatrix, long timestampNs) {
|
| - if (camera == null) {
|
| + if (cameraThreadHandler == null) {
|
| throw new RuntimeException("onTextureFrameAvailable() called after stopCapture().");
|
| }
|
| checkIsOnCameraThread();
|
|
|