| /* |
| * 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.VideoCapturerAndroidTestFixtures; |
| 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; |
| private int width = 0; |
| private int height = 0; |
| |
| @Override |
| public void renderFrame(I420Frame frame) { |
| synchronized (frameLock) { |
| ++framesRendered; |
| width = frame.rotatedWidth(); |
| height = frame.rotatedHeight(); |
| frameLock.notify(); |
| } |
| VideoRenderer.renderFrameDone(frame); |
| } |
| |
| public int frameWidth() { |
| synchronized (frameLock) { |
| return width; |
| } |
| } |
| |
| public int frameHeight() { |
| synchronized (frameLock) { |
| return height; |
| } |
| } |
| |
| 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 width, int height, int rotation, |
| long timeStamp) { |
| synchronized (frameLock) { |
| ++framesCaptured; |
| frameSize = frame.length; |
| frameWidth = width; |
| frameHeight = height; |
| timestamps.add(timeStamp); |
| frameLock.notify(); |
| } |
| } |
| @Override |
| public void onTextureFrameCaptured( |
| int width, int height, int oesTextureId, float[] transformMatrix, int rotation, |
| 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; |
| public final Object onCameraFreezedLock = new Object(); |
| private String onCameraFreezedDescription; |
| |
| @Override |
| public void onCameraError(String errorDescription) { |
| } |
| |
| @Override |
| public void onCameraFreezed(String errorDescription) { |
| synchronized (onCameraFreezedLock) { |
| onCameraFreezedDescription = errorDescription; |
| onCameraFreezedLock.notifyAll(); |
| } |
| } |
| |
| @Override |
| public void onCameraOpening(int cameraId) { |
| onCameraOpeningCalled = true; |
| } |
| |
| @Override |
| public void onFirstFrameAvailable() { |
| onFirstFrameAvailableCalled = true; |
| } |
| |
| @Override |
| public void onCameraClosed() { } |
| |
| public String WaitForCameraFreezed() throws InterruptedException { |
| synchronized (onCameraFreezedLock) { |
| onCameraFreezedLock.wait(); |
| return onCameraFreezedDescription; |
| } |
| } |
| } |
| |
| 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(); |
| if (capturer.isCapturingToTexture()) { |
| capturer.surfaceHelper.returnTextureFrame(); |
| } |
| 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(); |
| if (capturer.isCapturingToTexture()) { |
| capturer.surfaceHelper.returnTextureFrame(); |
| } |
| |
| // 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 { |
| assertTrue(format.frameSize() <= observer.frameSize()); |
| } |
| capturer.stopCapture(); |
| if (capturer.isCapturingToTexture()) { |
| capturer.surfaceHelper.returnTextureFrame(); |
| } |
| } |
| capturer.dispose(); |
| 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<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); |
| |
| if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.LOLLIPOP_MR1) { |
| // The first opened camera client will be evicted. |
| assertTrue(observer.WaitForCapturerToStart()); |
| capturer.stopCapture(); |
| } else { |
| 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<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); |
| 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(); |
| if (capturer.isCapturingToTexture()) { |
| capturer.surfaceHelper.returnTextureFrame(); |
| } |
| capturer.dispose(); |
| assertTrue(capturer.isReleased()); |
| } |
| |
| static public void startWhileCameraIsAlreadyOpenAndStop( |
| VideoCapturerAndroid capturer, Context appContext) throws InterruptedException { |
| Camera camera = Camera.open(capturer.getCurrentCameraId()); |
| 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); |
| capturer.stopCapture(); |
| capturer.dispose(); |
| assertTrue(capturer.isReleased()); |
| camera.release(); |
| } |
| |
| 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(); |
| if (capturer.isCapturingToTexture()) { |
| capturer.surfaceHelper.returnTextureFrame(); |
| } |
| |
| observer.WaitForNextCapturedFrame(); |
| capturer.stopCapture(); |
| |
| listOftimestamps = observer.getCopyAndResetListOftimeStamps(); |
| assertTrue(listOftimestamps.size() >= 1); |
| if (capturer.isCapturingToTexture()) { |
| capturer.surfaceHelper.returnTextureFrame(); |
| } |
| |
| 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(); |
| assertTrue(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(); |
| } |
| |
| static public void cameraFreezedEventOnBufferStarvationUsingTextures( |
| VideoCapturerAndroid capturer, |
| CameraEvents events, Context appContext) throws InterruptedException { |
| assertTrue("Not capturing to textures.", capturer.isCapturingToTexture()); |
| |
| 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. |
| assertTrue(observer.WaitForCapturerToStart()); |
| // Since we don't return the buffer, we should get a starvation message if we are |
| // capturing to a texture. |
| assertEquals("Camera failure. Client must return video buffers.", |
| events.WaitForCameraFreezed()); |
| |
| capturer.stopCapture(); |
| if (capturer.isCapturingToTexture()) { |
| capturer.surfaceHelper.returnTextureFrame(); |
| } |
| |
| capturer.dispose(); |
| assertTrue(capturer.isReleased()); |
| } |
| |
| static public void scaleCameraOutput(VideoCapturerAndroid capturer) throws InterruptedException { |
| PeerConnectionFactory factory = new PeerConnectionFactory(); |
| VideoSource source = |
| factory.createVideoSource(capturer, new MediaConstraints()); |
| VideoTrack track = factory.createVideoTrack("dummy", source); |
| RendererCallbacks renderer = new RendererCallbacks(); |
| track.addRenderer(new VideoRenderer(renderer)); |
| assertTrue(renderer.WaitForNextFrameToRender() > 0); |
| |
| final int startWidth = renderer.frameWidth(); |
| final int startHeight = renderer.frameHeight(); |
| final int frameRate = 30; |
| final int scaledWidth = startWidth / 2; |
| final int scaledHeight = startHeight / 2; |
| |
| // Request the captured frames to be scaled. |
| capturer.onOutputFormatRequest(scaledWidth, scaledHeight, frameRate); |
| |
| boolean gotExpectedResolution = false; |
| int numberOfInspectedFrames = 0; |
| |
| do { |
| renderer.WaitForNextFrameToRender(); |
| ++numberOfInspectedFrames; |
| |
| gotExpectedResolution = (renderer.frameWidth() == scaledWidth |
| && renderer.frameHeight() == scaledHeight); |
| } while (!gotExpectedResolution && numberOfInspectedFrames < 30); |
| |
| source.stop(); |
| track.dispose(); |
| source.dispose(); |
| factory.dispose(); |
| assertTrue(capturer.isReleased()); |
| |
| assertTrue(gotExpectedResolution); |
| } |
| |
| } |