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}
This commit is contained in:
magjed
2016-03-10 06:53:53 -08:00
committed by Commit bot
parent 1069cac518
commit a97e3cfe49

View File

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