diff options
Diffstat (limited to 'talk/app/webrtc/androidtests/src/org/webrtc/VideoCapturerAndroidTestFixtures.java')
-rw-r--r-- | talk/app/webrtc/androidtests/src/org/webrtc/VideoCapturerAndroidTestFixtures.java | 443 |
1 files changed, 443 insertions, 0 deletions
diff --git a/talk/app/webrtc/androidtests/src/org/webrtc/VideoCapturerAndroidTestFixtures.java b/talk/app/webrtc/androidtests/src/org/webrtc/VideoCapturerAndroidTestFixtures.java new file mode 100644 index 0000000000..11b3ce98a0 --- /dev/null +++ b/talk/app/webrtc/androidtests/src/org/webrtc/VideoCapturerAndroidTestFixtures.java @@ -0,0 +1,443 @@ +/* + * libjingle + * Copyright 2015 Google Inc. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.webrtc; + +import android.content.Context; +import android.hardware.Camera; + +import org.webrtc.CameraEnumerationAndroid.CaptureFormat; +import org.webrtc.VideoRenderer.I420Frame; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; + +import static junit.framework.Assert.*; + +public class VideoCapturerAndroidTestFixtures { + static class RendererCallbacks implements VideoRenderer.Callbacks { + private int framesRendered = 0; + private Object frameLock = 0; + + @Override + public void renderFrame(I420Frame frame) { + synchronized (frameLock) { + ++framesRendered; + frameLock.notify(); + } + VideoRenderer.renderFrameDone(frame); + } + + public int WaitForNextFrameToRender() throws InterruptedException { + synchronized (frameLock) { + frameLock.wait(); + return framesRendered; + } + } + } + + static class FakeAsyncRenderer implements VideoRenderer.Callbacks { + private final List<I420Frame> pendingFrames = new ArrayList<I420Frame>(); + + @Override + public void renderFrame(I420Frame frame) { + synchronized (pendingFrames) { + pendingFrames.add(frame); + pendingFrames.notifyAll(); + } + } + + // Wait until at least one frame have been received, before returning them. + public List<I420Frame> waitForPendingFrames() throws InterruptedException { + synchronized (pendingFrames) { + while (pendingFrames.isEmpty()) { + pendingFrames.wait(); + } + return new ArrayList<I420Frame>(pendingFrames); + } + } + } + + static class FakeCapturerObserver implements + VideoCapturerAndroid.CapturerObserver { + private int framesCaptured = 0; + private int frameSize = 0; + private int frameWidth = 0; + private int frameHeight = 0; + private Object frameLock = 0; + private Object capturerStartLock = 0; + private boolean captureStartResult = false; + private List<Long> timestamps = new ArrayList<Long>(); + + @Override + public void onCapturerStarted(boolean success) { + synchronized (capturerStartLock) { + captureStartResult = success; + capturerStartLock.notify(); + } + } + + @Override + public void onByteBufferFrameCaptured(byte[] frame, int length, int width, int height, + int rotation, long timeStamp) { + synchronized (frameLock) { + ++framesCaptured; + frameSize = length; + frameWidth = width; + frameHeight = height; + timestamps.add(timeStamp); + frameLock.notify(); + } + } + @Override + public void onTextureFrameCaptured( + int width, int height, int oesTextureId, float[] transformMatrix, long timeStamp) { + synchronized (frameLock) { + ++framesCaptured; + frameWidth = width; + frameHeight = height; + frameSize = 0; + timestamps.add(timeStamp); + frameLock.notify(); + } + } + + @Override + public void onOutputFormatRequest(int width, int height, int fps) {} + + public boolean WaitForCapturerToStart() throws InterruptedException { + synchronized (capturerStartLock) { + capturerStartLock.wait(); + return captureStartResult; + } + } + + public int WaitForNextCapturedFrame() throws InterruptedException { + synchronized (frameLock) { + frameLock.wait(); + return framesCaptured; + } + } + + int frameSize() { + synchronized (frameLock) { + return frameSize; + } + } + + int frameWidth() { + synchronized (frameLock) { + return frameWidth; + } + } + + int frameHeight() { + synchronized (frameLock) { + return frameHeight; + } + } + + List<Long> getCopyAndResetListOftimeStamps() { + synchronized (frameLock) { + ArrayList<Long> list = new ArrayList<Long>(timestamps); + timestamps.clear(); + return list; + } + } + } + + static class CameraEvents implements + VideoCapturerAndroid.CameraEventsHandler { + public boolean onCameraOpeningCalled; + public boolean onFirstFrameAvailableCalled; + + @Override + public void onCameraError(String errorDescription) { } + + @Override + public void onCameraOpening(int cameraId) { + onCameraOpeningCalled = true; + } + + @Override + public void onFirstFrameAvailable() { + onFirstFrameAvailableCalled = true; + } + + @Override + public void onCameraClosed() { } + } + + static public CameraEvents createCameraEvents() { + return new CameraEvents(); + } + + // Return true if the device under test have at least two cameras. + @SuppressWarnings("deprecation") + static public boolean HaveTwoCameras() { + return (Camera.getNumberOfCameras() >= 2); + } + + static public void release(VideoCapturerAndroid capturer) { + assertNotNull(capturer); + capturer.dispose(); + assertTrue(capturer.isReleased()); + } + + static public void startCapturerAndRender(VideoCapturerAndroid capturer) + throws InterruptedException { + PeerConnectionFactory factory = new PeerConnectionFactory(); + VideoSource source = + factory.createVideoSource(capturer, new MediaConstraints()); + VideoTrack track = factory.createVideoTrack("dummy", source); + RendererCallbacks callbacks = new RendererCallbacks(); + track.addRenderer(new VideoRenderer(callbacks)); + assertTrue(callbacks.WaitForNextFrameToRender() > 0); + track.dispose(); + source.dispose(); + factory.dispose(); + assertTrue(capturer.isReleased()); + } + + static public void switchCamera(VideoCapturerAndroid capturer) throws InterruptedException { + PeerConnectionFactory factory = new PeerConnectionFactory(); + VideoSource source = + factory.createVideoSource(capturer, new MediaConstraints()); + VideoTrack track = factory.createVideoTrack("dummy", source); + + // Array with one element to avoid final problem in nested classes. + final boolean[] cameraSwitchSuccessful = new boolean[1]; + final CountDownLatch barrier = new CountDownLatch(1); + capturer.switchCamera(new VideoCapturerAndroid.CameraSwitchHandler() { + @Override + public void onCameraSwitchDone(boolean isFrontCamera) { + cameraSwitchSuccessful[0] = true; + barrier.countDown(); + } + @Override + public void onCameraSwitchError(String errorDescription) { + cameraSwitchSuccessful[0] = false; + barrier.countDown(); + } + }); + // Wait until the camera has been switched. + barrier.await(); + + // Check result. + if (HaveTwoCameras()) { + assertTrue(cameraSwitchSuccessful[0]); + } else { + assertFalse(cameraSwitchSuccessful[0]); + } + // Ensure that frames are received. + RendererCallbacks callbacks = new RendererCallbacks(); + track.addRenderer(new VideoRenderer(callbacks)); + assertTrue(callbacks.WaitForNextFrameToRender() > 0); + track.dispose(); + source.dispose(); + factory.dispose(); + assertTrue(capturer.isReleased()); + } + + static public void cameraEventsInvoked(VideoCapturerAndroid capturer, CameraEvents events, + Context appContext) throws InterruptedException { + final List<CaptureFormat> 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); + // 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()); + assertTrue(events.onCameraOpeningCalled); + assertTrue(events.onFirstFrameAvailableCalled); + } + + static public void cameraCallsAfterStop( + VideoCapturerAndroid capturer, Context appContext) throws InterruptedException { + final List<CaptureFormat> 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); + // Make sure camera is started and then stop it. + assertTrue(observer.WaitForCapturerToStart()); + capturer.stopCapture(); + for (long timeStamp : observer.getCopyAndResetListOftimeStamps()) { + capturer.returnBuffer(timeStamp); + } + // We can't change |capturer| at this point, but we should not crash. + capturer.switchCamera(null); + capturer.onOutputFormatRequest(640, 480, 15); + capturer.changeCaptureFormat(640, 480, 15); + + capturer.dispose(); + assertTrue(capturer.isReleased()); + } + + static public void stopRestartVideoSource(VideoCapturerAndroid capturer) + throws InterruptedException { + PeerConnectionFactory factory = new PeerConnectionFactory(); + VideoSource source = + factory.createVideoSource(capturer, new MediaConstraints()); + VideoTrack track = factory.createVideoTrack("dummy", source); + RendererCallbacks callbacks = new RendererCallbacks(); + track.addRenderer(new VideoRenderer(callbacks)); + assertTrue(callbacks.WaitForNextFrameToRender() > 0); + assertEquals(MediaSource.State.LIVE, source.state()); + + source.stop(); + assertEquals(MediaSource.State.ENDED, source.state()); + + source.restart(); + assertTrue(callbacks.WaitForNextFrameToRender() > 0); + assertEquals(MediaSource.State.LIVE, source.state()); + track.dispose(); + source.dispose(); + factory.dispose(); + assertTrue(capturer.isReleased()); + } + + static public void startStopWithDifferentResolutions(VideoCapturerAndroid capturer, + Context appContext) throws InterruptedException { + FakeCapturerObserver observer = new FakeCapturerObserver(); + List<CaptureFormat> formats = capturer.getSupportedFormats(); + + for(int i = 0; i < 3 ; ++i) { + CameraEnumerationAndroid.CaptureFormat format = formats.get(i); + capturer.startCapture(format.width, format.height, format.maxFramerate, + appContext, observer); + assertTrue(observer.WaitForCapturerToStart()); + observer.WaitForNextCapturedFrame(); + + // Check the frame size. The actual width and height depend on how the capturer is mounted. + final boolean identicalResolution = (observer.frameWidth() == format.width + && observer.frameHeight() == format.height); + final boolean flippedResolution = (observer.frameWidth() == format.height + && observer.frameHeight() == format.width); + if (!identicalResolution && !flippedResolution) { + fail("Wrong resolution, got: " + observer.frameWidth() + "x" + observer.frameHeight() + + " expected: " + format.width + "x" + format.height + " or " + format.height + "x" + + format.width); + } + + if (capturer.isCapturingToTexture()) { + assertEquals(0, observer.frameSize()); + } else { + assertEquals(format.frameSize(), observer.frameSize()); + } + capturer.stopCapture(); + for (long timestamp : observer.getCopyAndResetListOftimeStamps()) { + capturer.returnBuffer(timestamp); + } + } + capturer.dispose(); + assertTrue(capturer.isReleased()); + } + + static public void returnBufferLate(VideoCapturerAndroid capturer, + Context appContext) throws InterruptedException { + FakeCapturerObserver observer = new FakeCapturerObserver(); + + List<CaptureFormat> formats = capturer.getSupportedFormats(); + CameraEnumerationAndroid.CaptureFormat format = formats.get(0); + capturer.startCapture(format.width, format.height, format.maxFramerate, + appContext, observer); + assertTrue(observer.WaitForCapturerToStart()); + + observer.WaitForNextCapturedFrame(); + capturer.stopCapture(); + List<Long> listOftimestamps = observer.getCopyAndResetListOftimeStamps(); + assertTrue(listOftimestamps.size() >= 1); + + format = formats.get(1); + capturer.startCapture(format.width, format.height, format.maxFramerate, + appContext, observer); + observer.WaitForCapturerToStart(); + + for (Long timeStamp : listOftimestamps) { + capturer.returnBuffer(timeStamp); + } + + observer.WaitForNextCapturedFrame(); + capturer.stopCapture(); + + listOftimestamps = observer.getCopyAndResetListOftimeStamps(); + assertTrue(listOftimestamps.size() >= 1); + for (Long timeStamp : listOftimestamps) { + capturer.returnBuffer(timeStamp); + } + capturer.dispose(); + assertTrue(capturer.isReleased()); + } + + static public void returnBufferLateEndToEnd(VideoCapturerAndroid capturer) + throws InterruptedException { + final PeerConnectionFactory factory = new PeerConnectionFactory(); + final VideoSource source = factory.createVideoSource(capturer, new MediaConstraints()); + final VideoTrack track = factory.createVideoTrack("dummy", source); + final FakeAsyncRenderer renderer = new FakeAsyncRenderer(); + track.addRenderer(new VideoRenderer(renderer)); + // Wait for at least one frame that has not been returned. + assertFalse(renderer.waitForPendingFrames().isEmpty()); + + capturer.stopCapture(); + + // Dispose everything. + track.dispose(); + source.dispose(); + factory.dispose(); + + // The pending frames should keep the JNI parts and |capturer| alive. + assertFalse(capturer.isReleased()); + + // Return the frame(s), on a different thread out of spite. + final List<I420Frame> pendingFrames = renderer.waitForPendingFrames(); + final Thread returnThread = new Thread(new Runnable() { + @Override + public void run() { + for (I420Frame frame : pendingFrames) { + VideoRenderer.renderFrameDone(frame); + } + } + }); + returnThread.start(); + returnThread.join(); + + // Check that frames have successfully returned. This will cause |capturer| to be released. + assertTrue(capturer.isReleased()); + } +} |