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(); |