From a97e3cfe49201595767b474beebbd676d14ae575 Mon Sep 17 00:00:00 2001 From: magjed Date: Thu, 10 Mar 2016 06:53:53 -0800 Subject: [PATCH] Reland of Android VideoCapturerAndroid: Move stopListening() call to stopCaptureOnCameraThread(): https://codereview.webrtc.org/1763673002/ This reland includes a fix for the cameraObserver bug. BUG=webrtc:5519 ,b/27497950 TBR=perkj@webrtc.org Review URL: https://codereview.webrtc.org/1777273002 Cr-Commit-Position: refs/heads/master@{#11943} --- .../org/webrtc/VideoCapturerAndroid.java | 171 +++++++++++------- 1 file changed, 103 insertions(+), 68 deletions(-) diff --git a/webrtc/api/java/android/org/webrtc/VideoCapturerAndroid.java b/webrtc/api/java/android/org/webrtc/VideoCapturerAndroid.java index 5c9b77b4cc..23e2fddd4b 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,14 +594,6 @@ 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(); @@ -645,9 +676,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 +706,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();