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:
@ -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.");
|
||||||
startCaptureOnCameraThread(width, height, framerate, frameObserver,
|
|
||||||
applicationContext);
|
|
||||||
}
|
}
|
||||||
});
|
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(
|
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);
|
||||||
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);
|
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();
|
||||||
barrier.countDown();
|
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();
|
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();
|
||||||
|
|||||||
Reference in New Issue
Block a user