Home | History | Annotate | Download | only in test
      1 /*
      2  *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
      3  *
      4  *  Use of this source code is governed by a BSD-style license
      5  *  that can be found in the LICENSE file in the root of the source
      6  *  tree. An additional intellectual property rights grant can be found
      7  *  in the file PATENTS.  All contributing project authors may
      8  *  be found in the AUTHORS file in the root of the source tree.
      9  */
     10 
     11 package org.appspot.apprtc.test;
     12 
     13 import java.util.LinkedList;
     14 import java.util.List;
     15 import java.util.concurrent.CountDownLatch;
     16 import java.util.concurrent.TimeUnit;
     17 
     18 import org.appspot.apprtc.AppRTCClient.SignalingParameters;
     19 import org.appspot.apprtc.PeerConnectionClient;
     20 import org.appspot.apprtc.PeerConnectionClient.PeerConnectionEvents;
     21 import org.appspot.apprtc.PeerConnectionClient.PeerConnectionParameters;
     22 import org.appspot.apprtc.util.LooperExecutor;
     23 import org.webrtc.EglBase;
     24 import org.webrtc.IceCandidate;
     25 import org.webrtc.MediaCodecVideoEncoder;
     26 import org.webrtc.PeerConnection;
     27 import org.webrtc.PeerConnectionFactory;
     28 import org.webrtc.SessionDescription;
     29 import org.webrtc.StatsReport;
     30 import org.webrtc.VideoRenderer;
     31 
     32 import android.os.Build;
     33 import android.test.InstrumentationTestCase;
     34 import android.util.Log;
     35 
     36 public class PeerConnectionClientTest extends InstrumentationTestCase
     37     implements PeerConnectionEvents {
     38   private static final String TAG = "RTCClientTest";
     39   private static final int ICE_CONNECTION_WAIT_TIMEOUT = 10000;
     40   private static final int WAIT_TIMEOUT = 7000;
     41   private static final int CAMERA_SWITCH_ATTEMPTS = 3;
     42   private static final int VIDEO_RESTART_ATTEMPTS = 3;
     43   private static final int VIDEO_RESTART_TIMEOUT = 500;
     44   private static final int EXPECTED_VIDEO_FRAMES = 10;
     45   private static final String VIDEO_CODEC_VP8 = "VP8";
     46   private static final String VIDEO_CODEC_VP9 = "VP9";
     47   private static final String VIDEO_CODEC_H264 = "H264";
     48   private static final int AUDIO_RUN_TIMEOUT = 1000;
     49   private static final String LOCAL_RENDERER_NAME = "Local renderer";
     50   private static final String REMOTE_RENDERER_NAME = "Remote renderer";
     51 
     52   // The peer connection client is assumed to be thread safe in itself; the
     53   // reference is written by the test thread and read by worker threads.
     54   private volatile PeerConnectionClient pcClient;
     55   private volatile boolean loopback;
     56 
     57   // EGL context that can be used by hardware video decoders to decode to a texture.
     58   private EglBase eglBase;
     59 
     60   // These are protected by their respective event objects.
     61   private LooperExecutor signalingExecutor;
     62   private boolean isClosed;
     63   private boolean isIceConnected;
     64   private SessionDescription localSdp;
     65   private List<IceCandidate> iceCandidates = new LinkedList<IceCandidate>();
     66   private final Object localSdpEvent = new Object();
     67   private final Object iceCandidateEvent = new Object();
     68   private final Object iceConnectedEvent = new Object();
     69   private final Object closeEvent = new Object();
     70 
     71   // Mock renderer implementation.
     72   private static class MockRenderer implements VideoRenderer.Callbacks {
     73     // These are protected by 'this' since we gets called from worker threads.
     74     private String rendererName;
     75     private boolean renderFrameCalled = false;
     76 
     77     // Thread-safe in itself.
     78     private CountDownLatch doneRendering;
     79 
     80     public MockRenderer(int expectedFrames, String rendererName) {
     81       this.rendererName = rendererName;
     82       reset(expectedFrames);
     83     }
     84 
     85     // Resets render to wait for new amount of video frames.
     86     public synchronized void reset(int expectedFrames) {
     87       renderFrameCalled = false;
     88       doneRendering = new CountDownLatch(expectedFrames);
     89     }
     90 
     91     @Override
     92     public synchronized void renderFrame(VideoRenderer.I420Frame frame) {
     93       if (!renderFrameCalled) {
     94         if (rendererName != null) {
     95           Log.d(TAG, rendererName + " render frame: "
     96               + frame.rotatedWidth() + " x " + frame.rotatedHeight());
     97         } else {
     98           Log.d(TAG, "Render frame: " + frame.rotatedWidth() + " x " + frame.rotatedHeight());
     99         }
    100       }
    101       renderFrameCalled = true;
    102       VideoRenderer.renderFrameDone(frame);
    103       doneRendering.countDown();
    104     }
    105 
    106 
    107     // This method shouldn't hold any locks or touch member variables since it
    108     // blocks.
    109     public boolean waitForFramesRendered(int timeoutMs)
    110         throws InterruptedException {
    111       doneRendering.await(timeoutMs, TimeUnit.MILLISECONDS);
    112       return (doneRendering.getCount() <= 0);
    113     }
    114   }
    115 
    116   // Peer connection events implementation.
    117   @Override
    118   public void onLocalDescription(SessionDescription sdp) {
    119     Log.d(TAG, "LocalSDP type: " + sdp.type);
    120     synchronized (localSdpEvent) {
    121       localSdp = sdp;
    122       localSdpEvent.notifyAll();
    123     }
    124   }
    125 
    126   @Override
    127   public void onIceCandidate(final IceCandidate candidate) {
    128     synchronized(iceCandidateEvent) {
    129       Log.d(TAG, "IceCandidate #" + iceCandidates.size() + " : " + candidate.toString());
    130       if (loopback) {
    131         // Loopback local ICE candidate in a separate thread to avoid adding
    132         // remote ICE candidate in a local ICE candidate callback.
    133         signalingExecutor.execute(new Runnable() {
    134           @Override
    135           public void run() {
    136             pcClient.addRemoteIceCandidate(candidate);
    137           }
    138         });
    139       }
    140       iceCandidates.add(candidate);
    141       iceCandidateEvent.notifyAll();
    142     }
    143   }
    144 
    145   @Override
    146   public void onIceConnected() {
    147     Log.d(TAG, "ICE Connected");
    148     synchronized(iceConnectedEvent) {
    149       isIceConnected = true;
    150       iceConnectedEvent.notifyAll();
    151     }
    152   }
    153 
    154   @Override
    155   public void onIceDisconnected() {
    156     Log.d(TAG, "ICE Disconnected");
    157     synchronized(iceConnectedEvent) {
    158       isIceConnected = false;
    159       iceConnectedEvent.notifyAll();
    160     }
    161   }
    162 
    163   @Override
    164   public void onPeerConnectionClosed() {
    165     Log.d(TAG, "PeerConnection closed");
    166     synchronized(closeEvent) {
    167       isClosed = true;
    168       closeEvent.notifyAll();
    169     }
    170   }
    171 
    172   @Override
    173   public void onPeerConnectionError(String description) {
    174     fail("PC Error: " + description);
    175   }
    176 
    177   @Override
    178   public void onPeerConnectionStatsReady(StatsReport[] reports) {
    179   }
    180 
    181   // Helper wait functions.
    182   private boolean waitForLocalSDP(int timeoutMs)
    183       throws InterruptedException {
    184     synchronized(localSdpEvent) {
    185       if (localSdp == null) {
    186         localSdpEvent.wait(timeoutMs);
    187       }
    188       return (localSdp != null);
    189     }
    190   }
    191 
    192   private boolean waitForIceCandidates(int timeoutMs)
    193       throws InterruptedException {
    194     synchronized(iceCandidateEvent) {
    195       if (iceCandidates.size() == 0) {
    196         iceCandidateEvent.wait(timeoutMs);
    197       }
    198       return (iceCandidates.size() > 0);
    199     }
    200   }
    201 
    202   private boolean waitForIceConnected(int timeoutMs)
    203       throws InterruptedException {
    204     synchronized(iceConnectedEvent) {
    205       if (!isIceConnected) {
    206         iceConnectedEvent.wait(timeoutMs);
    207       }
    208       if (!isIceConnected) {
    209         Log.e(TAG, "ICE connection failure");
    210       }
    211 
    212       return isIceConnected;
    213     }
    214   }
    215 
    216   private boolean waitForPeerConnectionClosed(int timeoutMs)
    217       throws InterruptedException {
    218     synchronized(closeEvent) {
    219       if (!isClosed) {
    220         closeEvent.wait(timeoutMs);
    221       }
    222       return isClosed;
    223     }
    224   }
    225 
    226   PeerConnectionClient createPeerConnectionClient(
    227       MockRenderer localRenderer, MockRenderer remoteRenderer,
    228       PeerConnectionParameters peerConnectionParameters, boolean useTexures) {
    229     List<PeerConnection.IceServer> iceServers =
    230         new LinkedList<PeerConnection.IceServer>();
    231     SignalingParameters signalingParameters = new SignalingParameters(
    232         iceServers, true, // iceServers, initiator.
    233         null, null, null, // clientId, wssUrl, wssPostUrl.
    234         null, null); // offerSdp, iceCandidates.
    235 
    236     PeerConnectionClient client = PeerConnectionClient.getInstance();
    237     PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
    238     options.networkIgnoreMask = 0;
    239     options.disableNetworkMonitor = true;
    240     client.setPeerConnectionFactoryOptions(options);
    241     client.createPeerConnectionFactory(
    242         getInstrumentation().getContext(), peerConnectionParameters, this);
    243     client.createPeerConnection(useTexures ? eglBase.getEglBaseContext() : null,
    244         localRenderer, remoteRenderer, signalingParameters);
    245     client.createOffer();
    246     return client;
    247   }
    248 
    249   private PeerConnectionParameters createParametersForAudioCall() {
    250     PeerConnectionParameters peerConnectionParameters =
    251         new PeerConnectionParameters(
    252             false, true, false, // videoCallEnabled, loopback, tracing.
    253             0, 0, 0, 0, "", true, false, // video codec parameters.
    254             0, "OPUS", false, false, false); // audio codec parameters.
    255     return peerConnectionParameters;
    256   }
    257 
    258   private PeerConnectionParameters createParametersForVideoCall(
    259       String videoCodec, boolean captureToTexture) {
    260     PeerConnectionParameters peerConnectionParameters =
    261         new PeerConnectionParameters(
    262             true, true, false, // videoCallEnabled, loopback, tracing.
    263             0, 0, 0, 0, videoCodec, true, captureToTexture, // video codec parameters.
    264             0, "OPUS", false, false, false); // audio codec parameters.
    265     return peerConnectionParameters;
    266   }
    267 
    268   @Override
    269   public void setUp() {
    270     signalingExecutor = new LooperExecutor();
    271     signalingExecutor.requestStart();
    272     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
    273       eglBase = EglBase.create();
    274     }
    275   }
    276 
    277   @Override
    278   public void tearDown() {
    279     signalingExecutor.requestStop();
    280     if (eglBase != null) {
    281       eglBase.release();
    282     }
    283   }
    284 
    285   public void testSetLocalOfferMakesVideoFlowLocally()
    286       throws InterruptedException {
    287     Log.d(TAG, "testSetLocalOfferMakesVideoFlowLocally");
    288     MockRenderer localRenderer = new MockRenderer(EXPECTED_VIDEO_FRAMES, LOCAL_RENDERER_NAME);
    289     pcClient = createPeerConnectionClient(
    290         localRenderer, new MockRenderer(0, null),
    291         createParametersForVideoCall(VIDEO_CODEC_VP8, false), false);
    292 
    293     // Wait for local SDP and ice candidates set events.
    294     assertTrue("Local SDP was not set.", waitForLocalSDP(WAIT_TIMEOUT));
    295     assertTrue("ICE candidates were not generated.",
    296         waitForIceCandidates(WAIT_TIMEOUT));
    297 
    298     // Check that local video frames were rendered.
    299     assertTrue("Local video frames were not rendered.",
    300         localRenderer.waitForFramesRendered(WAIT_TIMEOUT));
    301 
    302     pcClient.close();
    303     assertTrue("PeerConnection close event was not received.",
    304         waitForPeerConnectionClosed(WAIT_TIMEOUT));
    305     Log.d(TAG, "testSetLocalOfferMakesVideoFlowLocally Done.");
    306   }
    307 
    308   private void doLoopbackTest(PeerConnectionParameters parameters, boolean decodeToTexure)
    309       throws InterruptedException {
    310     loopback = true;
    311     MockRenderer localRenderer = null;
    312     MockRenderer remoteRenderer = null;
    313     if (parameters.videoCallEnabled) {
    314       Log.d(TAG, "testLoopback for video " + parameters.videoCodec);
    315       localRenderer = new MockRenderer(EXPECTED_VIDEO_FRAMES, LOCAL_RENDERER_NAME);
    316       remoteRenderer = new MockRenderer(EXPECTED_VIDEO_FRAMES, REMOTE_RENDERER_NAME);
    317     } else {
    318       Log.d(TAG, "testLoopback for audio.");
    319     }
    320     pcClient = createPeerConnectionClient(
    321         localRenderer, remoteRenderer, parameters, decodeToTexure);
    322 
    323     // Wait for local SDP, rename it to answer and set as remote SDP.
    324     assertTrue("Local SDP was not set.", waitForLocalSDP(WAIT_TIMEOUT));
    325     SessionDescription remoteSdp = new SessionDescription(
    326         SessionDescription.Type.fromCanonicalForm("answer"),
    327         localSdp.description);
    328     pcClient.setRemoteDescription(remoteSdp);
    329 
    330     // Wait for ICE connection.
    331     assertTrue("ICE connection failure.", waitForIceConnected(ICE_CONNECTION_WAIT_TIMEOUT));
    332 
    333     if (parameters.videoCallEnabled) {
    334       // Check that local and remote video frames were rendered.
    335       assertTrue("Local video frames were not rendered.",
    336           localRenderer.waitForFramesRendered(WAIT_TIMEOUT));
    337       assertTrue("Remote video frames were not rendered.",
    338           remoteRenderer.waitForFramesRendered(WAIT_TIMEOUT));
    339     } else {
    340       // For audio just sleep for 1 sec.
    341       // TODO(glaznev): check how we can detect that remote audio was rendered.
    342       Thread.sleep(AUDIO_RUN_TIMEOUT);
    343     }
    344 
    345     pcClient.close();
    346     assertTrue(waitForPeerConnectionClosed(WAIT_TIMEOUT));
    347     Log.d(TAG, "testLoopback done.");
    348   }
    349 
    350   public void testLoopbackAudio() throws InterruptedException {
    351     doLoopbackTest(createParametersForAudioCall(), false);
    352   }
    353 
    354   public void testLoopbackVp8() throws InterruptedException {
    355     doLoopbackTest(createParametersForVideoCall(VIDEO_CODEC_VP8, false), false);
    356   }
    357 
    358   public void DISABLED_testLoopbackVp9() throws InterruptedException {
    359     doLoopbackTest(createParametersForVideoCall(VIDEO_CODEC_VP9, false), false);
    360   }
    361 
    362   public void testLoopbackH264() throws InterruptedException {
    363     doLoopbackTest(createParametersForVideoCall(VIDEO_CODEC_H264, false), false);
    364   }
    365 
    366   public void testLoopbackVp8DecodeToTexture() throws InterruptedException {
    367     if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
    368       Log.i(TAG, "Decode to textures is not supported, requires SDK version 19.");
    369       return;
    370     }
    371     doLoopbackTest(createParametersForVideoCall(VIDEO_CODEC_VP8, false), true);
    372   }
    373 
    374   public void DISABLED_testLoopbackVp9DecodeToTexture() throws InterruptedException {
    375     if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
    376       Log.i(TAG, "Decode to textures is not supported, requires SDK version 19.");
    377       return;
    378     }
    379     doLoopbackTest(createParametersForVideoCall(VIDEO_CODEC_VP9, false), true);
    380   }
    381 
    382   public void testLoopbackH264DecodeToTexture() throws InterruptedException {
    383     if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
    384       Log.i(TAG, "Decode to textures is not supported, requires SDK version 19.");
    385       return;
    386     }
    387     doLoopbackTest(createParametersForVideoCall(VIDEO_CODEC_H264, false), true);
    388   }
    389 
    390   public void testLoopbackVp8CaptureToTexture() throws InterruptedException {
    391     if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
    392       Log.i(TAG, "Encode to textures is not supported. Requires SDK version 19");
    393       return;
    394     }
    395     // TODO(perkj): If we can always capture to textures, there is no need to check if the
    396     // hardware encoder supports to encode from a texture.
    397     if (!MediaCodecVideoEncoder.isVp8HwSupportedUsingTextures()) {
    398       Log.i(TAG, "VP8 encode to textures is not supported.");
    399       return;
    400     }
    401     doLoopbackTest(createParametersForVideoCall(VIDEO_CODEC_VP8, true), true);
    402   }
    403 
    404   public void testLoopbackH264CaptureToTexture() throws InterruptedException {
    405     if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
    406       Log.i(TAG, "Encode to textures is not supported. Requires KITKAT");
    407       return;
    408     }
    409     // TODO(perkj): If we can always capture to textures, there is no need to check if the
    410     // hardware encoder supports to encode from a texture.
    411     if (!MediaCodecVideoEncoder.isH264HwSupportedUsingTextures()) {
    412       Log.i(TAG, "H264 encode to textures is not supported.");
    413       return;
    414     }
    415     doLoopbackTest(createParametersForVideoCall(VIDEO_CODEC_H264, true), true);
    416   }
    417 
    418 
    419   // Checks if default front camera can be switched to back camera and then
    420   // again to front camera.
    421   public void testCameraSwitch() throws InterruptedException {
    422     Log.d(TAG, "testCameraSwitch");
    423     loopback = true;
    424 
    425     MockRenderer localRenderer = new MockRenderer(EXPECTED_VIDEO_FRAMES, LOCAL_RENDERER_NAME);
    426     MockRenderer remoteRenderer = new MockRenderer(EXPECTED_VIDEO_FRAMES, REMOTE_RENDERER_NAME);
    427 
    428     pcClient = createPeerConnectionClient(
    429         localRenderer, remoteRenderer, createParametersForVideoCall(VIDEO_CODEC_VP8, false), false);
    430 
    431     // Wait for local SDP, rename it to answer and set as remote SDP.
    432     assertTrue("Local SDP was not set.", waitForLocalSDP(WAIT_TIMEOUT));
    433     SessionDescription remoteSdp = new SessionDescription(
    434         SessionDescription.Type.fromCanonicalForm("answer"),
    435         localSdp.description);
    436     pcClient.setRemoteDescription(remoteSdp);
    437 
    438     // Wait for ICE connection.
    439     assertTrue("ICE connection failure.", waitForIceConnected(ICE_CONNECTION_WAIT_TIMEOUT));
    440 
    441     // Check that local and remote video frames were rendered.
    442     assertTrue("Local video frames were not rendered before camera switch.",
    443         localRenderer.waitForFramesRendered(WAIT_TIMEOUT));
    444     assertTrue("Remote video frames were not rendered before camera switch.",
    445         remoteRenderer.waitForFramesRendered(WAIT_TIMEOUT));
    446 
    447     for (int i = 0; i < CAMERA_SWITCH_ATTEMPTS; i++) {
    448       // Try to switch camera
    449       pcClient.switchCamera();
    450 
    451       // Reset video renders and check that local and remote video frames
    452       // were rendered after camera switch.
    453       localRenderer.reset(EXPECTED_VIDEO_FRAMES);
    454       remoteRenderer.reset(EXPECTED_VIDEO_FRAMES);
    455       assertTrue("Local video frames were not rendered after camera switch.",
    456           localRenderer.waitForFramesRendered(WAIT_TIMEOUT));
    457       assertTrue("Remote video frames were not rendered after camera switch.",
    458           remoteRenderer.waitForFramesRendered(WAIT_TIMEOUT));
    459     }
    460     pcClient.close();
    461     assertTrue(waitForPeerConnectionClosed(WAIT_TIMEOUT));
    462     Log.d(TAG, "testCameraSwitch done.");
    463   }
    464 
    465   // Checks if video source can be restarted - simulate app goes to
    466   // background and back to foreground.
    467   public void testVideoSourceRestart() throws InterruptedException {
    468     Log.d(TAG, "testVideoSourceRestart");
    469     loopback = true;
    470 
    471     MockRenderer localRenderer = new MockRenderer(EXPECTED_VIDEO_FRAMES, LOCAL_RENDERER_NAME);
    472     MockRenderer remoteRenderer = new MockRenderer(EXPECTED_VIDEO_FRAMES, REMOTE_RENDERER_NAME);
    473 
    474     pcClient = createPeerConnectionClient(
    475         localRenderer, remoteRenderer, createParametersForVideoCall(VIDEO_CODEC_VP8, false), false);
    476 
    477     // Wait for local SDP, rename it to answer and set as remote SDP.
    478     assertTrue("Local SDP was not set.", waitForLocalSDP(WAIT_TIMEOUT));
    479     SessionDescription remoteSdp = new SessionDescription(
    480         SessionDescription.Type.fromCanonicalForm("answer"),
    481         localSdp.description);
    482     pcClient.setRemoteDescription(remoteSdp);
    483 
    484     // Wait for ICE connection.
    485     assertTrue("ICE connection failure.", waitForIceConnected(ICE_CONNECTION_WAIT_TIMEOUT));
    486 
    487     // Check that local and remote video frames were rendered.
    488     assertTrue("Local video frames were not rendered before video restart.",
    489         localRenderer.waitForFramesRendered(WAIT_TIMEOUT));
    490     assertTrue("Remote video frames were not rendered before video restart.",
    491         remoteRenderer.waitForFramesRendered(WAIT_TIMEOUT));
    492 
    493     // Stop and then start video source a few times.
    494     for (int i = 0; i < VIDEO_RESTART_ATTEMPTS; i++) {
    495       pcClient.stopVideoSource();
    496       Thread.sleep(VIDEO_RESTART_TIMEOUT);
    497       pcClient.startVideoSource();
    498 
    499       // Reset video renders and check that local and remote video frames
    500       // were rendered after video restart.
    501       localRenderer.reset(EXPECTED_VIDEO_FRAMES);
    502       remoteRenderer.reset(EXPECTED_VIDEO_FRAMES);
    503       assertTrue("Local video frames were not rendered after video restart.",
    504           localRenderer.waitForFramesRendered(WAIT_TIMEOUT));
    505       assertTrue("Remote video frames were not rendered after video restart.",
    506           remoteRenderer.waitForFramesRendered(WAIT_TIMEOUT));
    507     }
    508     pcClient.close();
    509     assertTrue(waitForPeerConnectionClosed(WAIT_TIMEOUT));
    510     Log.d(TAG, "testVideoSourceRestart done.");
    511   }
    512 
    513 }
    514