From 89ef6cc13e8aa9f16b212dd82124f4297a1f7385 Mon Sep 17 00:00:00 2001 From: perkj Date: Mon, 9 Nov 2015 01:35:20 -0800 Subject: [PATCH] Attempt to open Android camera later if it is already in use. This change VideoCapturerAndroid to attempt 3 times with a period of 300ms to open the camera if it fails. This is so that if another application have it already opened, it would have more time to release it. BUG=b/25190234 Review URL: https://codereview.webrtc.org/1422023007 Cr-Commit-Position: refs/heads/master@{#10559} --- .../org/webrtc/VideoCapturerAndroidTest.java | 37 ++++++++++- .../VideoCapturerAndroidTestFixtures.java | 66 +++++++++++++++++++ .../org/webrtc/VideoCapturerAndroid.java | 57 ++++++++++++---- 3 files changed, 147 insertions(+), 13 deletions(-) diff --git a/talk/app/webrtc/androidtests/src/org/webrtc/VideoCapturerAndroidTest.java b/talk/app/webrtc/androidtests/src/org/webrtc/VideoCapturerAndroidTest.java index 54fa5d4c86..09568d518b 100644 --- a/talk/app/webrtc/androidtests/src/org/webrtc/VideoCapturerAndroidTest.java +++ b/talk/app/webrtc/androidtests/src/org/webrtc/VideoCapturerAndroidTest.java @@ -29,7 +29,6 @@ package org.webrtc; import android.test.ActivityTestCase; import android.test.suitebuilder.annotation.MediumTest; import android.test.suitebuilder.annotation.SmallTest; -import android.util.Log; import android.util.Size; import org.webrtc.CameraEnumerationAndroid.CaptureFormat; @@ -222,6 +221,41 @@ public class VideoCapturerAndroidTest extends ActivityTestCase { getInstrumentation().getContext()); } + @SmallTest + // This test that an error is reported if the camera is already opened + // when VideoCapturerAndroid is started. + public void testStartWhileCameraAlreadyOpened() throws InterruptedException { + String deviceName = CameraEnumerationAndroid.getDeviceName(0); + VideoCapturerAndroid capturer = + VideoCapturerAndroid.create(deviceName, null); + VideoCapturerAndroidTestFixtures.startWhileCameraIsAlreadyOpen( + capturer, getInstrumentation().getContext()); + } + + @SmallTest + // This test that VideoCapturerAndroid can be started, even if the camera is already opened + // if the camera is closed while VideoCapturerAndroid is re-trying to start. + public void testStartWhileCameraIsAlreadyOpenAndCloseCamera() throws InterruptedException { + String deviceName = CameraEnumerationAndroid.getDeviceName(0); + VideoCapturerAndroid capturer = + VideoCapturerAndroid.create(deviceName, null); + VideoCapturerAndroidTestFixtures.startWhileCameraIsAlreadyOpenAndCloseCamera( + capturer, getInstrumentation().getContext()); + } + + @SmallTest + // This test that VideoCapturerAndroid.stop can be called while VideoCapturerAndroid is + // re-trying to start. + public void startWhileCameraIsAlreadyOpenAndStop() throws InterruptedException { + String deviceName = CameraEnumerationAndroid.getDeviceName(0); + VideoCapturerAndroid capturer = + VideoCapturerAndroid.create(deviceName, null); + VideoCapturerAndroidTestFixtures.startWhileCameraIsAlreadyOpenAndStop( + capturer, getInstrumentation().getContext()); + } + + + @SmallTest // This test what happens if buffers are returned after the capturer have // been stopped and restarted. It does not test or use the C++ layer. @@ -259,7 +293,6 @@ public class VideoCapturerAndroidTest extends ActivityTestCase { VideoCapturerAndroidTestFixtures.returnBufferLateEndToEnd(capturer); } - @MediumTest // This test that CameraEventsHandler.onError is triggered if video buffers are not returned to // the capturer. diff --git a/talk/app/webrtc/androidtests/src/org/webrtc/VideoCapturerAndroidTestFixtures.java b/talk/app/webrtc/androidtests/src/org/webrtc/VideoCapturerAndroidTestFixtures.java index 237c6d8a49..da450281aa 100644 --- a/talk/app/webrtc/androidtests/src/org/webrtc/VideoCapturerAndroidTestFixtures.java +++ b/talk/app/webrtc/androidtests/src/org/webrtc/VideoCapturerAndroidTestFixtures.java @@ -29,6 +29,7 @@ package org.webrtc; import android.content.Context; import android.hardware.Camera; +import org.webrtc.VideoCapturerAndroidTestFixtures; import org.webrtc.CameraEnumerationAndroid.CaptureFormat; import org.webrtc.VideoRenderer.I420Frame; @@ -382,6 +383,71 @@ public class VideoCapturerAndroidTestFixtures { assertTrue(capturer.isReleased()); } + static void waitUntilIdle(VideoCapturerAndroid capturer) throws InterruptedException { + final CountDownLatch barrier = new CountDownLatch(1); + capturer.getCameraThreadHandler().post(new Runnable() { + @Override public void run() { + barrier.countDown(); + } + }); + barrier.await(); + } + + static public void startWhileCameraIsAlreadyOpen( + VideoCapturerAndroid capturer, Context appContext) throws InterruptedException { + Camera camera = Camera.open(capturer.getCurrentCameraId()); + final List formats = capturer.getSupportedFormats(); + final CameraEnumerationAndroid.CaptureFormat format = formats.get(0); + + final FakeCapturerObserver observer = new FakeCapturerObserver(); + capturer.startCapture(format.width, format.height, format.maxFramerate, + appContext, observer); + + assertFalse(observer.WaitForCapturerToStart()); + capturer.dispose(); + camera.release(); + } + + static public void startWhileCameraIsAlreadyOpenAndCloseCamera( + VideoCapturerAndroid capturer, Context appContext) throws InterruptedException { + Camera camera = Camera.open(capturer.getCurrentCameraId()); + + final List formats = capturer.getSupportedFormats(); + final CameraEnumerationAndroid.CaptureFormat format = formats.get(0); + + final FakeCapturerObserver observer = new FakeCapturerObserver(); + capturer.startCapture(format.width, format.height, format.maxFramerate, + appContext, observer); + waitUntilIdle(capturer); + + camera.release(); + + // Make sure camera is started and first frame is received and then stop it. + assertTrue(observer.WaitForCapturerToStart()); + observer.WaitForNextCapturedFrame(); + capturer.stopCapture(); + for (long timeStamp : observer.getCopyAndResetListOftimeStamps()) { + capturer.returnBuffer(timeStamp); + } + capturer.dispose(); + assertTrue(capturer.isReleased()); + } + + static public void startWhileCameraIsAlreadyOpenAndStop( + VideoCapturerAndroid capturer, Context appContext) throws InterruptedException { + Camera camera = Camera.open(capturer.getCurrentCameraId()); + final List formats = capturer.getSupportedFormats(); + final CameraEnumerationAndroid.CaptureFormat format = formats.get(0); + + final FakeCapturerObserver observer = new FakeCapturerObserver(); + capturer.startCapture(format.width, format.height, format.maxFramerate, + appContext, observer); + capturer.stopCapture(); + capturer.dispose(); + assertTrue(capturer.isReleased()); + camera.release(); + } + static public void returnBufferLate(VideoCapturerAndroid capturer, Context appContext) throws InterruptedException { FakeCapturerObserver observer = new FakeCapturerObserver(); diff --git a/talk/app/webrtc/java/android/org/webrtc/VideoCapturerAndroid.java b/talk/app/webrtc/java/android/org/webrtc/VideoCapturerAndroid.java index 88163ef6c8..6dbf0039a3 100644 --- a/talk/app/webrtc/java/android/org/webrtc/VideoCapturerAndroid.java +++ b/talk/app/webrtc/java/android/org/webrtc/VideoCapturerAndroid.java @@ -100,6 +100,12 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba // 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 = 300; + private int openCameraAttempts; // Camera error callback. private final Camera.ErrorCallback cameraErrorCallback = @@ -313,7 +319,7 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba // 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() { + int getCurrentCameraId() { synchronized (cameraIdLock) { return id; } @@ -423,6 +429,7 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba if (frameObserver == null) { throw new RuntimeException("frameObserver not set."); } + cameraThreadHandler.post(new Runnable() { @Override public void run() { startCaptureOnCameraThread(width, height, framerate, frameObserver, @@ -432,8 +439,8 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba } private void startCaptureOnCameraThread( - int width, int height, int framerate, CapturerObserver frameObserver, - Context applicationContext) { + final int width, final int height, final int framerate, final CapturerObserver frameObserver, + final Context applicationContext) { Throwable error = null; checkIsOnCameraThread(); if (camera != null) { @@ -441,17 +448,36 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba } this.applicationContext = applicationContext; this.frameObserver = frameObserver; + this.firstFrameReported = false; + try { - synchronized (cameraIdLock) { - Logging.d(TAG, "Opening camera " + id); - firstFrameReported = false; - if (eventsHandler != null) { - eventsHandler.onCameraOpening(id); + try { + synchronized (cameraIdLock) { + Logging.d(TAG, "Opening camera " + id); + if (eventsHandler != null) { + eventsHandler.onCameraOpening(id); + } + camera = Camera.open(id); + info = new Camera.CameraInfo(); + Camera.getCameraInfo(id, info); } - camera = Camera.open(id); - info = new Camera.CameraInfo(); - Camera.getCameraInfo(id, info); + } catch (RuntimeException e) { + openCameraAttempts++; + if (openCameraAttempts < MAX_OPEN_CAMERA_ATTEMPTS) { + Logging.e(TAG, "Camera.open failed, retrying", e); + openCameraOnCodecThreadRunner = new Runnable() { + @Override public void run() { + startCaptureOnCameraThread(width, height, framerate, frameObserver, + applicationContext); + } + }; + cameraThreadHandler.postDelayed(openCameraOnCodecThreadRunner, OPEN_CAMERA_DELAY_MS); + return; + } + openCameraAttempts = 0; + throw new RuntimeException(e); } + try { camera.setPreviewTexture(surfaceHelper.getSurfaceTexture()); } catch (IOException e) { @@ -571,6 +597,10 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba 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; @@ -622,6 +652,11 @@ public class VideoCapturerAndroid extends VideoCapturer implements PreviewCallba frameObserver.onOutputFormatRequest(width, height, framerate); } + // Exposed for testing purposes only. + Handler getCameraThreadHandler() { + return cameraThreadHandler; + } + public void returnBuffer(final long timeStamp) { cameraThreadHandler.post(new Runnable() { @Override public void run() {