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