Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(542)

Unified Diff: talk/app/webrtc/java/android/org/webrtc/VideoCapturerAndroid.java

Issue 1350863002: VideoCapturerAndroid: Fix threading issues (Closed) Base URL: https://chromium.googlesource.com/external/webrtc.git@master
Patch Set: Created 5 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: talk/app/webrtc/java/android/org/webrtc/VideoCapturerAndroid.java
diff --git a/talk/app/webrtc/java/android/org/webrtc/VideoCapturerAndroid.java b/talk/app/webrtc/java/android/org/webrtc/VideoCapturerAndroid.java
index c88f1c55c6c6b3bc7129ee4ad7505000865786a9..dffbb3ce710eea5a625b76c31f860273e03c67f4 100644
--- a/talk/app/webrtc/java/android/org/webrtc/VideoCapturerAndroid.java
+++ b/talk/app/webrtc/java/android/org/webrtc/VideoCapturerAndroid.java
@@ -34,7 +34,7 @@ import android.hardware.Camera.PreviewCallback;
import android.opengl.GLES11Ext;
import android.opengl.GLES20;
import android.os.Handler;
-import android.os.Looper;
+import android.os.HandlerThread;
import android.os.SystemClock;
import android.view.Surface;
import android.view.WindowManager;
@@ -50,7 +50,7 @@ import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
-import java.util.concurrent.Exchanger;
+import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
// Android specific implementation of VideoCapturer.
@@ -60,28 +60,26 @@ import java.util.concurrent.TimeUnit;
// front and back camera. It also provides methods for enumerating valid device
// names.
//
-// Threading notes: this class is called from C++ code, and from Camera
-// Java callbacks. Since these calls happen on different threads,
-// the entry points to this class are all synchronized. This shouldn't present
-// a performance bottleneck because only onPreviewFrame() is called more than
-// once (and is called serially on a single thread), so the lock should be
-// uncontended. Note that each of these synchronized methods must check
-// |camera| for null to account for having possibly waited for stopCapture() to
-// complete.
+// Threading notes: this class is called from C++ code, Android Camera callbacks, and possibly
+// arbitrary Java threads. All public entry points are thread safe, and delegate the work to the
+// camera thread. The internal *OnCameraThread() methods must check |camera| for null to check if
+// the camera has been stopped.
@SuppressWarnings("deprecation")
public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallback {
private final static String TAG = "VideoCapturerAndroid";
private final static int CAMERA_OBSERVER_PERIOD_MS = 5000;
private Camera camera; // Only non-null while capturing.
- private CameraThread cameraThread;
- private Handler cameraThreadHandler;
+ private final HandlerThread cameraThread;
+ private final Handler cameraThreadHandler;
private Context applicationContext;
+ // Synchronization lock for |id| in getCurrentCameraId() and switchCameraOnCameraThread().
+ private final Object cameraIdLock = new Object();
private int id;
private Camera.CameraInfo info;
private SurfaceTexture cameraSurfaceTexture;
private int cameraGlTexture = 0;
- private final FramePool videoBuffers = new FramePool();
+ private final FramePool videoBuffers;
// Remember the requested format in case we want to switch cameras.
private int requestedWidth;
private int requestedHeight;
@@ -90,6 +88,7 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
private CaptureFormat captureFormat;
private int cameraFramesCount;
private int captureBuffersCount;
+ private final Object pendingCameraSwitchLock = new Object();
private volatile boolean pendingCameraSwitch;
private CapturerObserver frameObserver = null;
private CameraErrorHandler errorHandler = null;
@@ -135,9 +134,7 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
} else {
cameraFramesCount = 0;
captureBuffersCount = 0;
- if (cameraThreadHandler != null) {
- cameraThreadHandler.postDelayed(this, CAMERA_OBSERVER_PERIOD_MS);
- }
+ cameraThreadHandler.postDelayed(this, CAMERA_OBSERVER_PERIOD_MS);
}
}
};
@@ -148,6 +145,14 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
public void onCameraError(String errorDescription);
}
+ // Camera switch handler - one of these functions are invoked with the result of switchCamera().
perkj_webrtc 2015/09/18 09:17:57 Document threading.
magjed_webrtc 2015/09/21 08:19:57 Done.
+ public interface CameraSwitchHandler {
+ // Invoked on success. |isFrontCamera| is true if the new camera is front facing.
+ void onCameraSwitchDone(boolean isFrontCamera);
+ // Invoked on failure, e.g. camera is stopped or only one camera available.
+ void onCameraSwitchError(String errorDescription);
+ }
+
public static VideoCapturerAndroid create(String name,
CameraErrorHandler errorHandler) {
VideoCapturer capturer = VideoCapturer.create(name);
@@ -163,39 +168,47 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
// the camera is running.
// Returns true on success. False if the next camera does not support the
perkj_webrtc 2015/09/18 09:17:57 Does not return anything now.
magjed_webrtc 2015/09/21 08:19:57 Done.
// current resolution.
- public synchronized boolean switchCamera(final Runnable switchDoneEvent) {
- if (Camera.getNumberOfCameras() < 2 )
- return false;
-
- if (cameraThreadHandler == null) {
- Logging.e(TAG, "Calling switchCamera() for stopped camera.");
- return false;
+ public void switchCamera(final CameraSwitchHandler handler) {
+ if (Camera.getNumberOfCameras() < 2) {
+ if (handler != null) {
+ handler.onCameraSwitchError("No camera to switch to.");
+ }
+ return;
}
- if (pendingCameraSwitch) {
- // 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.");
- return false;
+ synchronized (pendingCameraSwitchLock) {
+ if (pendingCameraSwitch) {
+ // 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.");
+ }
+ return;
+ }
+ pendingCameraSwitch = true;
}
-
- pendingCameraSwitch = true;
- id = (id + 1) % Camera.getNumberOfCameras();
cameraThreadHandler.post(new Runnable() {
@Override public void run() {
- switchCameraOnCameraThread(switchDoneEvent);
+ if (camera == null) {
+ if (handler != null) {
+ handler.onCameraSwitchError("Camera is stopped.");
+ }
+ return;
+ }
+ switchCameraOnCameraThread();
+ synchronized (pendingCameraSwitchLock) {
+ pendingCameraSwitch = false;
+ }
+ if (handler != null) {
+ handler.onCameraSwitchDone(info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT);
+ }
}
});
- return true;
}
// Requests a new output format from the video capturer. Captured frames
// by the camera will be scaled/or dropped by the video capturer.
- public synchronized void onOutputFormatRequest(
- final int width, final int height, final int fps) {
- if (cameraThreadHandler == null) {
- Logging.e(TAG, "Calling onOutputFormatRequest() for already stopped camera.");
- return;
- }
+ public void onOutputFormatRequest(final int width, final int height, final int fps) {
perkj_webrtc 2015/09/18 09:17:57 Todo: Document what this does. Change name?
magjed_webrtc 2015/09/21 08:19:57 Done.
cameraThreadHandler.post(new Runnable() {
@Override public void run() {
onOutputFormatRequestOnCameraThread(width, height, fps);
@@ -205,12 +218,7 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
// Reconfigure the camera to capture in a new format. This should only be called while the camera
// is running.
- public synchronized void changeCaptureFormat(
- final int width, final int height, final int framerate) {
- if (cameraThreadHandler == null) {
- Logging.e(TAG, "Calling changeCaptureFormat() for already stopped camera.");
- return;
- }
+ public void changeCaptureFormat(final int width, final int height, final int framerate) {
cameraThreadHandler.post(new Runnable() {
@Override public void run() {
startPreviewOnCameraThread(width, height, framerate);
@@ -218,67 +226,63 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
});
}
- public synchronized List<CaptureFormat> getSupportedFormats() {
- return CameraEnumerationAndroid.getSupportedFormats(id);
+ // Helper function to retrieve the current camera id synchronously. Note that the camera id might
+ // change at any point by switchCamera() calls.
+ private int getCurrentCameraId() {
+ synchronized (cameraIdLock) {
+ return id;
+ }
+ }
+
+ public List<CaptureFormat> getSupportedFormats() {
+ return CameraEnumerationAndroid.getSupportedFormats(getCurrentCameraId());
}
- // Return a list of timestamps for the frames that have been sent out, but not returned yet.
- // Useful for logging and testing.
- public String pendingFramesTimeStamps() {
- return videoBuffers.pendingFramesTimeStamps();
+ // Called from native code.
+ private String getSupportedFormatsAsJson() throws JSONException {
+ return CameraEnumerationAndroid.getSupportedFormatsAsJson(getCurrentCameraId());
}
private VideoCapturerAndroid() {
Logging.d(TAG, "VideoCapturerAndroid");
+ cameraThread = new HandlerThread(TAG);
+ cameraThread.start();
+ cameraThreadHandler = new Handler(cameraThread.getLooper());
+ videoBuffers = new FramePool(cameraThread);
}
// Called by native code.
// Initializes local variables for the camera named |deviceName|. If |deviceName| is empty, the
// first available device is used in order to be compatible with the generic VideoCapturer class.
- synchronized boolean init(String deviceName) {
+ boolean init(String deviceName) {
Logging.d(TAG, "init: " + deviceName);
if (deviceName == null)
return false;
- boolean foundDevice = false;
if (deviceName.isEmpty()) {
this.id = 0;
- foundDevice = true;
+ return true;
} else {
for (int i = 0; i < Camera.getNumberOfCameras(); ++i) {
- String existing_device = CameraEnumerationAndroid.getDeviceName(i);
- if (existing_device != null && deviceName.equals(existing_device)) {
+ if (deviceName.equals(CameraEnumerationAndroid.getDeviceName(i))) {
this.id = i;
- foundDevice = true;
+ return true;
}
}
}
- return foundDevice;
- }
-
- String getSupportedFormatsAsJson() throws JSONException {
- return CameraEnumerationAndroid.getSupportedFormatsAsJson(id);
+ return false;
}
- private class CameraThread extends Thread {
- private Exchanger<Handler> handlerExchanger;
- public CameraThread(Exchanger<Handler> handlerExchanger) {
- this.handlerExchanger = handlerExchanger;
- }
-
- @Override public void run() {
- Looper.prepare();
- exchange(handlerExchanger, new Handler());
- Looper.loop();
- }
+ // Called by native code.
+ private void release() {
perkj_webrtc 2015/09/18 09:17:57 Document why this need to be called.
magjed_webrtc 2015/09/21 08:19:57 Done.
+ cameraThread.quit();
}
- // Called by native code. Returns true if capturer is started.
+ // Called by native code.
//
// Note that this actually opens the camera, and Camera callbacks run on the
- // thread that calls open(), so this is done on the CameraThread. Since the
- // API needs a synchronous success return value we wait for the result.
- synchronized void startCapture(
+ // thread that calls open(), so this is done on the CameraThread.
+ 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
@@ -289,14 +293,6 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
if (frameObserver == null) {
throw new RuntimeException("frameObserver not set.");
}
- if (cameraThreadHandler != null) {
- throw new RuntimeException("Camera has already been started.");
- }
-
- Exchanger<Handler> handlerExchanger = new Exchanger<Handler>();
- cameraThread = new CameraThread(handlerExchanger);
- cameraThread.start();
- cameraThreadHandler = exchange(handlerExchanger, null);
cameraThreadHandler.post(new Runnable() {
@Override public void run() {
startCaptureOnCameraThread(width, height, framerate, frameObserver,
@@ -309,6 +305,9 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
int width, int height, int framerate, CapturerObserver frameObserver,
Context applicationContext) {
Throwable error = null;
+ if (camera != null) {
+ throw new RuntimeException("Camera has already been started.");
+ }
this.applicationContext = applicationContext;
this.frameObserver = frameObserver;
try {
@@ -348,7 +347,6 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
}
Logging.e(TAG, "startCapture failed", error);
stopCaptureOnCameraThread();
- cameraThreadHandler = null;
frameObserver.OnCapturerStarted(false);
if (errorHandler != null) {
errorHandler.onCameraError("Camera can not be started.");
@@ -421,31 +419,23 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
}
// Called by native code. Returns true when camera is known to be stopped.
- synchronized void stopCapture() throws InterruptedException {
- if (cameraThreadHandler == null) {
- Logging.e(TAG, "Calling stopCapture() for already stopped camera.");
- return;
- }
+ 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();
}
});
- cameraThread.join();
- cameraThreadHandler = null;
+ barrier.await();
Logging.d(TAG, "stopCapture done");
}
private void stopCaptureOnCameraThread() {
- doStopCaptureOnCameraThread();
- Looper.myLooper().quit();
- return;
- }
-
- private void doStopCaptureOnCameraThread() {
Logging.d(TAG, "stopCaptureOnCameraThread");
if (camera == null) {
+ Logging.e(TAG, "Calling stopCapture() for already stopped camera.");
return;
}
try {
@@ -463,6 +453,7 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
cameraGlTexture = 0;
}
Logging.d(TAG, "Release camera.");
+ // TODO(magjed): camera.release() should probably be in a finally clause.
perkj_webrtc 2015/09/18 09:17:57 done in separte cl
magjed_webrtc 2015/09/21 08:19:57 Acknowledged. Rebased.
camera.release();
camera = null;
} catch (IOException e) {
@@ -470,22 +461,20 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
}
}
- private void switchCameraOnCameraThread(Runnable switchDoneEvent) {
+ private void switchCameraOnCameraThread() {
Logging.d(TAG, "switchCameraOnCameraThread");
-
- doStopCaptureOnCameraThread();
+ stopCaptureOnCameraThread();
+ synchronized (cameraIdLock) {
+ id = (id + 1) % Camera.getNumberOfCameras();
+ }
startCaptureOnCameraThread(requestedWidth, requestedHeight, requestedFramerate, frameObserver,
applicationContext);
- pendingCameraSwitch = false;
Logging.d(TAG, "switchCameraOnCameraThread done");
- if (switchDoneEvent != null) {
- switchDoneEvent.run();
- }
}
- private void onOutputFormatRequestOnCameraThread(
- int width, int height, int fps) {
+ private void onOutputFormatRequestOnCameraThread(int width, int height, int fps) {
if (camera == null) {
+ Logging.e(TAG, "Calling onOutputFormatRequest() on stopped camera.");
return;
}
Logging.d(TAG, "onOutputFormatRequestOnCameraThread: " + width + "x" + height +
@@ -493,8 +482,12 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
frameObserver.OnOutputFormatRequest(width, height, fps);
}
- void returnBuffer(long timeStamp) {
- videoBuffers.returnBuffer(timeStamp);
+ public void returnBuffer(final long timeStamp) {
+ cameraThreadHandler.post(new Runnable() {
+ @Override public void run() {
+ videoBuffers.returnBuffer(timeStamp);
+ }
+ });
}
private int getDeviceOrientation() {
@@ -554,37 +547,12 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
}
}
- // runCameraThreadUntilIdle make sure all posted messages to the cameraThread
- // is processed before returning. It does that by itself posting a message to
- // to the message queue and waits until is has been processed.
- // It is used in tests.
- void runCameraThreadUntilIdle() {
- if (cameraThreadHandler == null)
- return;
- final Exchanger<Boolean> result = new Exchanger<Boolean>();
- cameraThreadHandler.post(new Runnable() {
- @Override public void run() {
- exchange(result, true); // |true| is a dummy here.
- }
- });
- exchange(result, false); // |false| is a dummy value here.
- return;
- }
-
- // Exchanges |value| with |exchanger|, converting InterruptedExceptions to
- // RuntimeExceptions (since we expect never to see these).
- private static <T> T exchange(Exchanger<T> exchanger, T value) {
- try {
- return exchanger.exchange(value);
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
- }
-
// Class used for allocating and bookkeeping video frames. All buffers are
// direct allocated so that they can be directly used from native code. This class is
- // synchronized and can be called from multiple threads.
+ // not thread-safe, and enforces single thread use.
private static class FramePool {
+ // Thread that all calls should be made on.
+ private final Thread thread;
// Arbitrary queue depth. Higher number means more memory allocated & held,
// lower number means more sensitivity to processing time in the client (and
// potentially stalling the capturer if it runs out of buffers to write to).
@@ -598,12 +566,24 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
private int frameSize = 0;
private Camera camera;
- synchronized int numCaptureBuffersAvailable() {
+ public FramePool(Thread thread) {
+ this.thread = thread;
+ }
+
+ private void checkIsOnValidThread() {
+ if (Thread.currentThread() != thread) {
+ throw new IllegalStateException("Wrong thread");
+ }
+ }
+
+ public int numCaptureBuffersAvailable() {
+ checkIsOnValidThread();
return queuedBuffers.size();
}
// Discards previous queued buffers and adds new callback buffers to camera.
- synchronized void queueCameraBuffers(int frameSize, Camera camera) {
+ public void queueCameraBuffers(int frameSize, Camera camera) {
+ checkIsOnValidThread();
this.camera = camera;
this.frameSize = frameSize;
@@ -617,7 +597,8 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
+ " buffers of size " + frameSize + ".");
}
- synchronized String pendingFramesTimeStamps() {
+ public String pendingFramesTimeStamps() {
+ checkIsOnValidThread();
List<Long> timeStampsMs = new ArrayList<Long>();
for (Long timeStampNs : pendingBuffers.keySet()) {
timeStampsMs.add(TimeUnit.NANOSECONDS.toMillis(timeStampNs));
@@ -625,7 +606,8 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
return timeStampsMs.toString();
}
- synchronized void stopReturnBuffersToCamera() {
+ public void stopReturnBuffersToCamera() {
+ checkIsOnValidThread();
this.camera = null;
queuedBuffers.clear();
// Frames in |pendingBuffers| need to be kept alive until they are returned.
@@ -635,7 +617,8 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
: " Pending buffers: " + pendingFramesTimeStamps() + "."));
}
- synchronized boolean reserveByteBuffer(byte[] data, long timeStamp) {
+ public boolean reserveByteBuffer(byte[] data, long timeStamp) {
+ checkIsOnValidThread();
final ByteBuffer buffer = queuedBuffers.remove(data);
if (buffer == null) {
// Frames might be posted to |onPreviewFrame| with the previous format while changing
@@ -659,7 +642,8 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba
return true;
}
- synchronized void returnBuffer(long timeStamp) {
+ public void returnBuffer(long timeStamp) {
+ checkIsOnValidThread();
final ByteBuffer returnedFrame = pendingBuffers.remove(timeStamp);
if (returnedFrame == null) {
throw new RuntimeException("unknown data buffer with time stamp "

Powered by Google App Engine
This is Rietveld 408576698