• Home
  • History
  • Annotate
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
MockRenderer(int expectedFrames, String rendererName)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.
reset(int expectedFrames)86     public synchronized void reset(int expectedFrames) {
87       renderFrameCalled = false;
88       doneRendering = new CountDownLatch(expectedFrames);
89     }
90 
91     @Override
renderFrame(VideoRenderer.I420Frame frame)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.
waitForFramesRendered(int timeoutMs)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
onLocalDescription(SessionDescription sdp)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
onIceCandidate(final IceCandidate candidate)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
onIceConnected()146   public void onIceConnected() {
147     Log.d(TAG, "ICE Connected");
148     synchronized(iceConnectedEvent) {
149       isIceConnected = true;
150       iceConnectedEvent.notifyAll();
151     }
152   }
153 
154   @Override
onIceDisconnected()155   public void onIceDisconnected() {
156     Log.d(TAG, "ICE Disconnected");
157     synchronized(iceConnectedEvent) {
158       isIceConnected = false;
159       iceConnectedEvent.notifyAll();
160     }
161   }
162 
163   @Override
onPeerConnectionClosed()164   public void onPeerConnectionClosed() {
165     Log.d(TAG, "PeerConnection closed");
166     synchronized(closeEvent) {
167       isClosed = true;
168       closeEvent.notifyAll();
169     }
170   }
171 
172   @Override
onPeerConnectionError(String description)173   public void onPeerConnectionError(String description) {
174     fail("PC Error: " + description);
175   }
176 
177   @Override
onPeerConnectionStatsReady(StatsReport[] reports)178   public void onPeerConnectionStatsReady(StatsReport[] reports) {
179   }
180 
181   // Helper wait functions.
waitForLocalSDP(int timeoutMs)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 
waitForIceCandidates(int timeoutMs)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 
waitForIceConnected(int timeoutMs)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 
waitForPeerConnectionClosed(int timeoutMs)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 
createPeerConnectionClient( MockRenderer localRenderer, MockRenderer remoteRenderer, PeerConnectionParameters peerConnectionParameters, boolean useTexures)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 
createParametersForAudioCall()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 
createParametersForVideoCall( String videoCodec, boolean captureToTexture)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
setUp()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
tearDown()278   public void tearDown() {
279     signalingExecutor.requestStop();
280     if (eglBase != null) {
281       eglBase.release();
282     }
283   }
284 
testSetLocalOfferMakesVideoFlowLocally()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 
doLoopbackTest(PeerConnectionParameters parameters, boolean decodeToTexure)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 
testLoopbackAudio()350   public void testLoopbackAudio() throws InterruptedException {
351     doLoopbackTest(createParametersForAudioCall(), false);
352   }
353 
testLoopbackVp8()354   public void testLoopbackVp8() throws InterruptedException {
355     doLoopbackTest(createParametersForVideoCall(VIDEO_CODEC_VP8, false), false);
356   }
357 
DISABLED_testLoopbackVp9()358   public void DISABLED_testLoopbackVp9() throws InterruptedException {
359     doLoopbackTest(createParametersForVideoCall(VIDEO_CODEC_VP9, false), false);
360   }
361 
testLoopbackH264()362   public void testLoopbackH264() throws InterruptedException {
363     doLoopbackTest(createParametersForVideoCall(VIDEO_CODEC_H264, false), false);
364   }
365 
testLoopbackVp8DecodeToTexture()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 
DISABLED_testLoopbackVp9DecodeToTexture()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 
testLoopbackH264DecodeToTexture()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 
testLoopbackVp8CaptureToTexture()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 
testLoopbackH264CaptureToTexture()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.
testCameraSwitch()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.
testVideoSourceRestart()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