VideoCapturerAndroid: Use one thread per startCapture()/stopCapture() session
Currently, VideoCapturerAndroid sets the thread and handler in the ctor and clears them in dispose(). This CL sets the handler in startCapture() instead and clears it in stopCapture(). The purpose is to prepare for sending in the SurfaceTextureHelper in startCapture() instead of letting VideoCapturerAndroid create it in the ctor. All access to the handler is now synchronized by a lock, and all Runnables are posted with a token so that they can be removed all at once in stopCapture() to guarantee that no pending operation will be executed after stopCapture(). BUG=webrtc:5519 Review URL: https://codereview.webrtc.org/1763673002 Cr-Commit-Position: refs/heads/master@{#11939}
This commit is contained in:
@ -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();
|
||||
|
||||
Reference in New Issue
Block a user