/* * libjingle * Copyright 2013, 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 junit.framework.TestCase; import org.junit.Test; import org.webrtc.PeerConnection.IceConnectionState; import org.webrtc.PeerConnection.IceGatheringState; import org.webrtc.PeerConnection.SignalingState; import java.io.File; import java.lang.ref.WeakReference; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.Arrays; import java.util.EnumSet; import java.util.IdentityHashMap; import java.util.LinkedList; import java.util.Map; import java.util.TreeSet; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; /** End-to-end tests for PeerConnection.java. */ public class PeerConnectionTest extends TestCase { // Set to true to render video. private static final boolean RENDER_TO_GUI = false; private static class ObserverExpectations implements PeerConnection.Observer, VideoRenderer.Callbacks, DataChannel.Observer, StatsObserver { private final String name; private int expectedIceCandidates = 0; private int expectedErrors = 0; private int expectedRenegotiations = 0; private int expectedSetSize = 0; private int previouslySeenWidth = 0; private int previouslySeenHeight = 0; private int expectedFramesDelivered = 0; private LinkedList expectedSignalingChanges = new LinkedList(); private LinkedList expectedIceConnectionChanges = new LinkedList(); private LinkedList expectedIceGatheringChanges = new LinkedList(); private LinkedList expectedAddStreamLabels = new LinkedList(); private LinkedList expectedRemoveStreamLabels = new LinkedList(); public LinkedList gotIceCandidates = new LinkedList(); private Map> renderers = new IdentityHashMap>(); private DataChannel dataChannel; private LinkedList expectedBuffers = new LinkedList(); private LinkedList expectedStateChanges = new LinkedList(); private LinkedList expectedRemoteDataChannelLabels = new LinkedList(); private int expectedStatsCallbacks = 0; private LinkedList gotStatsReports = new LinkedList(); public ObserverExpectations(String name) { this.name = name; } public synchronized void setDataChannel(DataChannel dataChannel) { assertNull(this.dataChannel); this.dataChannel = dataChannel; this.dataChannel.registerObserver(this); assertNotNull(this.dataChannel); } public synchronized void expectIceCandidates(int count) { expectedIceCandidates += count; } @Override public synchronized void onIceCandidate(IceCandidate candidate) { --expectedIceCandidates; // We don't assert expectedIceCandidates >= 0 because it's hard to know // how many to expect, in general. We only use expectIceCandidates to // assert a minimal count. gotIceCandidates.add(candidate); } public synchronized void expectError() { ++expectedErrors; } @Override public synchronized void onError() { assertTrue(--expectedErrors >= 0); } public synchronized void expectSetSize() { if (RENDER_TO_GUI) { // When new frames are delivered to the GUI renderer we don't get // notified of frame size info. return; } ++expectedSetSize; } @Override public synchronized void setSize(int width, int height) { assertFalse(RENDER_TO_GUI); assertTrue(--expectedSetSize >= 0); // Because different camera devices (fake & physical) produce different // resolutions, we only sanity-check the set sizes, assertTrue(width > 0); assertTrue(height > 0); if (previouslySeenWidth > 0) { assertEquals(previouslySeenWidth, width); assertEquals(previouslySeenHeight, height); } else { previouslySeenWidth = width; previouslySeenHeight = height; } } public synchronized void expectFramesDelivered(int count) { assertFalse(RENDER_TO_GUI); expectedFramesDelivered += count; } @Override public synchronized void renderFrame(VideoRenderer.I420Frame frame) { --expectedFramesDelivered; } public synchronized void expectSignalingChange(SignalingState newState) { expectedSignalingChanges.add(newState); } @Override public synchronized void onSignalingChange(SignalingState newState) { assertEquals(expectedSignalingChanges.removeFirst(), newState); } public synchronized void expectIceConnectionChange( IceConnectionState newState) { expectedIceConnectionChanges.add(newState); } @Override public synchronized void onIceConnectionChange( IceConnectionState newState) { // TODO(bemasc): remove once delivery of ICECompleted is reliable // (https://code.google.com/p/webrtc/issues/detail?id=3021). if (newState.equals(IceConnectionState.COMPLETED)) { return; } assertEquals(expectedIceConnectionChanges.removeFirst(), newState); } public synchronized void expectIceGatheringChange( IceGatheringState newState) { expectedIceGatheringChanges.add(newState); } @Override public synchronized void onIceGatheringChange(IceGatheringState newState) { // It's fine to get a variable number of GATHERING messages before // COMPLETE fires (depending on how long the test runs) so we don't assert // any particular count. if (newState == IceGatheringState.GATHERING) { return; } assertEquals(expectedIceGatheringChanges.removeFirst(), newState); } public synchronized void expectAddStream(String label) { expectedAddStreamLabels.add(label); } @Override public synchronized void onAddStream(MediaStream stream) { assertEquals(expectedAddStreamLabels.removeFirst(), stream.label()); assertEquals(1, stream.videoTracks.size()); assertEquals(1, stream.audioTracks.size()); assertTrue(stream.videoTracks.get(0).id().endsWith("VideoTrack")); assertTrue(stream.audioTracks.get(0).id().endsWith("AudioTrack")); assertEquals("video", stream.videoTracks.get(0).kind()); assertEquals("audio", stream.audioTracks.get(0).kind()); VideoRenderer renderer = createVideoRenderer(this); stream.videoTracks.get(0).addRenderer(renderer); assertNull(renderers.put( stream, new WeakReference(renderer))); } public synchronized void expectRemoveStream(String label) { expectedRemoveStreamLabels.add(label); } @Override public synchronized void onRemoveStream(MediaStream stream) { assertEquals(expectedRemoveStreamLabels.removeFirst(), stream.label()); WeakReference renderer = renderers.remove(stream); assertNotNull(renderer); assertNotNull(renderer.get()); assertEquals(1, stream.videoTracks.size()); stream.videoTracks.get(0).removeRenderer(renderer.get()); } public synchronized void expectDataChannel(String label) { expectedRemoteDataChannelLabels.add(label); } @Override public synchronized void onDataChannel(DataChannel remoteDataChannel) { assertEquals(expectedRemoteDataChannelLabels.removeFirst(), remoteDataChannel.label()); setDataChannel(remoteDataChannel); assertEquals(DataChannel.State.CONNECTING, dataChannel.state()); } public synchronized void expectRenegotiationNeeded() { ++expectedRenegotiations; } @Override public synchronized void onRenegotiationNeeded() { assertTrue(--expectedRenegotiations >= 0); } public synchronized void expectMessage(ByteBuffer expectedBuffer, boolean expectedBinary) { expectedBuffers.add( new DataChannel.Buffer(expectedBuffer, expectedBinary)); } @Override public synchronized void onMessage(DataChannel.Buffer buffer) { DataChannel.Buffer expected = expectedBuffers.removeFirst(); assertEquals(expected.binary, buffer.binary); assertTrue(expected.data.equals(buffer.data)); } @Override public synchronized void onStateChange() { assertEquals(expectedStateChanges.removeFirst(), dataChannel.state()); } public synchronized void expectStateChange(DataChannel.State state) { expectedStateChanges.add(state); } @Override public synchronized void onComplete(StatsReport[] reports) { if (--expectedStatsCallbacks < 0) { throw new RuntimeException("Unexpected stats report: " + reports); } gotStatsReports.add(reports); } public synchronized void expectStatsCallback() { ++expectedStatsCallbacks; } public synchronized LinkedList takeStatsReports() { LinkedList got = gotStatsReports; gotStatsReports = new LinkedList(); return got; } // Return a set of expectations that haven't been satisfied yet, possibly // empty if no such expectations exist. public synchronized TreeSet unsatisfiedExpectations() { TreeSet stillWaitingForExpectations = new TreeSet(); if (expectedIceCandidates > 0) { // See comment in onIceCandidate. stillWaitingForExpectations.add("expectedIceCandidates"); } if (expectedErrors != 0) { stillWaitingForExpectations.add("expectedErrors: " + expectedErrors); } if (expectedSignalingChanges.size() != 0) { stillWaitingForExpectations.add( "expectedSignalingChanges: " + expectedSignalingChanges.size()); } if (expectedIceConnectionChanges.size() != 0) { stillWaitingForExpectations.add("expectedIceConnectionChanges: " + expectedIceConnectionChanges.size()); } if (expectedIceGatheringChanges.size() != 0) { stillWaitingForExpectations.add("expectedIceGatheringChanges: " + expectedIceGatheringChanges.size()); } if (expectedAddStreamLabels.size() != 0) { stillWaitingForExpectations.add( "expectedAddStreamLabels: " + expectedAddStreamLabels.size()); } if (expectedRemoveStreamLabels.size() != 0) { stillWaitingForExpectations.add( "expectedRemoveStreamLabels: " + expectedRemoveStreamLabels.size()); } if (expectedSetSize != 0) { stillWaitingForExpectations.add("expectedSetSize"); } if (expectedFramesDelivered > 0) { stillWaitingForExpectations.add( "expectedFramesDelivered: " + expectedFramesDelivered); } if (!expectedBuffers.isEmpty()) { stillWaitingForExpectations.add( "expectedBuffers: " + expectedBuffers.size()); } if (!expectedStateChanges.isEmpty()) { stillWaitingForExpectations.add( "expectedStateChanges: " + expectedStateChanges.size()); } if (!expectedRemoteDataChannelLabels.isEmpty()) { stillWaitingForExpectations.add("expectedRemoteDataChannelLabels: " + expectedRemoteDataChannelLabels.size()); } if (expectedStatsCallbacks != 0) { stillWaitingForExpectations.add( "expectedStatsCallbacks: " + expectedStatsCallbacks); } return stillWaitingForExpectations; } public void waitForAllExpectationsToBeSatisfied() { // TODO(fischman): problems with this approach: // - come up with something better than a poll loop // - avoid serializing expectations explicitly; the test is not as robust // as it could be because it must place expectations between wait // statements very precisely (e.g. frame must not arrive before its // expectation, and expectation must not be registered so early as to // stall a wait). Use callbacks to fire off dependent steps instead of // explicitly waiting, so there can be just a single wait at the end of // the test. TreeSet prev = null; TreeSet stillWaitingForExpectations = unsatisfiedExpectations(); while (!stillWaitingForExpectations.isEmpty()) { if (!stillWaitingForExpectations.equals(prev)) { System.out.println( name + " still waiting at\n " + (new Throwable()).getStackTrace()[1] + "\n for: " + Arrays.toString(stillWaitingForExpectations.toArray())); } try { Thread.sleep(10); } catch (InterruptedException e) { throw new RuntimeException(e); } prev = stillWaitingForExpectations; stillWaitingForExpectations = unsatisfiedExpectations(); } if (prev == null) { System.out.println(name + " didn't need to wait at\n " + (new Throwable()).getStackTrace()[1]); } } } private static class SdpObserverLatch implements SdpObserver { private boolean success = false; private SessionDescription sdp = null; private String error = null; private CountDownLatch latch = new CountDownLatch(1); public SdpObserverLatch() {} @Override public void onCreateSuccess(SessionDescription sdp) { this.sdp = sdp; onSetSuccess(); } @Override public void onSetSuccess() { success = true; latch.countDown(); } @Override public void onCreateFailure(String error) { onSetFailure(error); } @Override public void onSetFailure(String error) { this.error = error; latch.countDown(); } public boolean await() { try { assertTrue(latch.await(1000, TimeUnit.MILLISECONDS)); return getSuccess(); } catch (Exception e) { throw new RuntimeException(e); } } public boolean getSuccess() { return success; } public SessionDescription getSdp() { return sdp; } public String getError() { return error; } } static int videoWindowsMapped = -1; private static class TestRenderer implements VideoRenderer.Callbacks { public int width = -1; public int height = -1; public int numFramesDelivered = 0; public void setSize(int width, int height) { assertEquals(this.width, -1); assertEquals(this.height, -1); this.width = width; this.height = height; } public void renderFrame(VideoRenderer.I420Frame frame) { ++numFramesDelivered; } } private static VideoRenderer createVideoRenderer( VideoRenderer.Callbacks videoCallbacks) { if (!RENDER_TO_GUI) { return new VideoRenderer(videoCallbacks); } ++videoWindowsMapped; assertTrue(videoWindowsMapped < 4); int x = videoWindowsMapped % 2 != 0 ? 700 : 0; int y = videoWindowsMapped >= 2 ? 0 : 500; return VideoRenderer.createGui(x, y); } // Return a weak reference to test that ownership is correctly held by // PeerConnection, not by test code. private static WeakReference addTracksToPC( PeerConnectionFactory factory, PeerConnection pc, VideoSource videoSource, String streamLabel, String videoTrackId, String audioTrackId, VideoRenderer.Callbacks videoCallbacks) { MediaStream lMS = factory.createLocalMediaStream(streamLabel); VideoTrack videoTrack = factory.createVideoTrack(videoTrackId, videoSource); assertNotNull(videoTrack); VideoRenderer videoRenderer = createVideoRenderer(videoCallbacks); assertNotNull(videoRenderer); videoTrack.addRenderer(videoRenderer); lMS.addTrack(videoTrack); // Just for fun, let's remove and re-add the track. lMS.removeTrack(videoTrack); lMS.addTrack(videoTrack); lMS.addTrack(factory.createAudioTrack( audioTrackId, factory.createAudioSource(new MediaConstraints()))); pc.addStream(lMS, new MediaConstraints()); return new WeakReference(lMS); } private static void assertEquals( SessionDescription lhs, SessionDescription rhs) { assertEquals(lhs.type, rhs.type); assertEquals(lhs.description, rhs.description); } @Test public void testCompleteSession() throws Exception { doTest(); } @Test public void testCompleteSessionOnNonMainThread() throws Exception { final Exception[] exceptionHolder = new Exception[1]; Thread nonMainThread = new Thread("PeerConnectionTest-nonMainThread") { @Override public void run() { try { doTest(); } catch (Exception e) { exceptionHolder[0] = e; } } }; nonMainThread.start(); nonMainThread.join(); if (exceptionHolder[0] != null) throw exceptionHolder[0]; } private void doTest() throws Exception { CountDownLatch testDone = new CountDownLatch(1); System.gc(); // Encourage any GC-related threads to start up. TreeSet threadsBeforeTest = allThreads(); PeerConnectionFactory factory = new PeerConnectionFactory(); // Uncomment to get ALL WebRTC tracing and SENSITIVE libjingle logging. // NOTE: this _must_ happen while |factory| is alive! // Logging.enableTracing( // "/tmp/PeerConnectionTest-log.txt", // EnumSet.of(Logging.TraceLevel.TRACE_ALL), // Logging.Severity.LS_SENSITIVE); MediaConstraints pcConstraints = new MediaConstraints(); pcConstraints.mandatory.add( new MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true")); LinkedList iceServers = new LinkedList(); iceServers.add(new PeerConnection.IceServer( "stun:stun.l.google.com:19302")); iceServers.add(new PeerConnection.IceServer( "turn:fake.example.com", "fakeUsername", "fakePassword")); ObserverExpectations offeringExpectations = new ObserverExpectations("PCTest:offerer"); PeerConnection offeringPC = factory.createPeerConnection( iceServers, pcConstraints, offeringExpectations); assertNotNull(offeringPC); ObserverExpectations answeringExpectations = new ObserverExpectations("PCTest:answerer"); PeerConnection answeringPC = factory.createPeerConnection( iceServers, pcConstraints, answeringExpectations); assertNotNull(answeringPC); // We want to use the same camera for offerer & answerer, so create it here // instead of in addTracksToPC. VideoSource videoSource = factory.createVideoSource( VideoCapturer.create(""), new MediaConstraints()); offeringExpectations.expectSetSize(); offeringExpectations.expectRenegotiationNeeded(); WeakReference oLMS = addTracksToPC( factory, offeringPC, videoSource, "offeredMediaStream", "offeredVideoTrack", "offeredAudioTrack", offeringExpectations); offeringExpectations.expectRenegotiationNeeded(); DataChannel offeringDC = offeringPC.createDataChannel( "offeringDC", new DataChannel.Init()); assertEquals("offeringDC", offeringDC.label()); offeringExpectations.setDataChannel(offeringDC); SdpObserverLatch sdpLatch = new SdpObserverLatch(); offeringPC.createOffer(sdpLatch, new MediaConstraints()); assertTrue(sdpLatch.await()); SessionDescription offerSdp = sdpLatch.getSdp(); assertEquals(offerSdp.type, SessionDescription.Type.OFFER); assertFalse(offerSdp.description.isEmpty()); sdpLatch = new SdpObserverLatch(); answeringExpectations.expectSignalingChange( SignalingState.HAVE_REMOTE_OFFER); answeringExpectations.expectAddStream("offeredMediaStream"); // SCTP DataChannels are announced via OPEN messages over the established // connection (not via SDP), so answeringExpectations can only register // expecting the channel during ICE, below. answeringPC.setRemoteDescription(sdpLatch, offerSdp); assertEquals( PeerConnection.SignalingState.STABLE, offeringPC.signalingState()); assertTrue(sdpLatch.await()); assertNull(sdpLatch.getSdp()); answeringExpectations.expectSetSize(); answeringExpectations.expectRenegotiationNeeded(); WeakReference aLMS = addTracksToPC( factory, answeringPC, videoSource, "answeredMediaStream", "answeredVideoTrack", "answeredAudioTrack", answeringExpectations); sdpLatch = new SdpObserverLatch(); answeringPC.createAnswer(sdpLatch, new MediaConstraints()); assertTrue(sdpLatch.await()); SessionDescription answerSdp = sdpLatch.getSdp(); assertEquals(answerSdp.type, SessionDescription.Type.ANSWER); assertFalse(answerSdp.description.isEmpty()); offeringExpectations.expectIceCandidates(2); answeringExpectations.expectIceCandidates(2); offeringExpectations.expectIceGatheringChange(IceGatheringState.COMPLETE); answeringExpectations.expectIceGatheringChange(IceGatheringState.COMPLETE); sdpLatch = new SdpObserverLatch(); answeringExpectations.expectSignalingChange(SignalingState.STABLE); answeringPC.setLocalDescription(sdpLatch, answerSdp); assertTrue(sdpLatch.await()); assertNull(sdpLatch.getSdp()); sdpLatch = new SdpObserverLatch(); offeringExpectations.expectSignalingChange(SignalingState.HAVE_LOCAL_OFFER); offeringPC.setLocalDescription(sdpLatch, offerSdp); assertTrue(sdpLatch.await()); assertNull(sdpLatch.getSdp()); sdpLatch = new SdpObserverLatch(); offeringExpectations.expectSignalingChange(SignalingState.STABLE); offeringExpectations.expectAddStream("answeredMediaStream"); offeringPC.setRemoteDescription(sdpLatch, answerSdp); assertTrue(sdpLatch.await()); assertNull(sdpLatch.getSdp()); offeringExpectations.waitForAllExpectationsToBeSatisfied(); answeringExpectations.waitForAllExpectationsToBeSatisfied(); assertEquals(offeringPC.getLocalDescription().type, offerSdp.type); assertEquals(offeringPC.getRemoteDescription().type, answerSdp.type); assertEquals(answeringPC.getLocalDescription().type, answerSdp.type); assertEquals(answeringPC.getRemoteDescription().type, offerSdp.type); if (!RENDER_TO_GUI) { // Wait for at least some frames to be delivered at each end (number // chosen arbitrarily). offeringExpectations.expectFramesDelivered(10); answeringExpectations.expectFramesDelivered(10); offeringExpectations.expectSetSize(); answeringExpectations.expectSetSize(); } offeringExpectations.expectIceConnectionChange( IceConnectionState.CHECKING); offeringExpectations.expectIceConnectionChange( IceConnectionState.CONNECTED); // TODO(bemasc): uncomment once delivery of ICECompleted is reliable // (https://code.google.com/p/webrtc/issues/detail?id=3021). // // offeringExpectations.expectIceConnectionChange( // IceConnectionState.COMPLETED); answeringExpectations.expectIceConnectionChange( IceConnectionState.CHECKING); answeringExpectations.expectIceConnectionChange( IceConnectionState.CONNECTED); offeringExpectations.expectStateChange(DataChannel.State.OPEN); // See commentary about SCTP DataChannels above for why this is here. answeringExpectations.expectDataChannel("offeringDC"); answeringExpectations.expectStateChange(DataChannel.State.OPEN); for (IceCandidate candidate : offeringExpectations.gotIceCandidates) { answeringPC.addIceCandidate(candidate); } offeringExpectations.gotIceCandidates.clear(); for (IceCandidate candidate : answeringExpectations.gotIceCandidates) { offeringPC.addIceCandidate(candidate); } answeringExpectations.gotIceCandidates.clear(); offeringExpectations.waitForAllExpectationsToBeSatisfied(); answeringExpectations.waitForAllExpectationsToBeSatisfied(); assertEquals( PeerConnection.SignalingState.STABLE, offeringPC.signalingState()); assertEquals( PeerConnection.SignalingState.STABLE, answeringPC.signalingState()); // Test send & receive UTF-8 text. answeringExpectations.expectMessage( ByteBuffer.wrap("hello!".getBytes(Charset.forName("UTF-8"))), false); DataChannel.Buffer buffer = new DataChannel.Buffer( ByteBuffer.wrap("hello!".getBytes(Charset.forName("UTF-8"))), false); assertTrue(offeringExpectations.dataChannel.send(buffer)); answeringExpectations.waitForAllExpectationsToBeSatisfied(); // Construct this binary message two different ways to ensure no // shortcuts are taken. ByteBuffer expectedBinaryMessage = ByteBuffer.allocateDirect(5); for (byte i = 1; i < 6; ++i) { expectedBinaryMessage.put(i); } expectedBinaryMessage.flip(); offeringExpectations.expectMessage(expectedBinaryMessage, true); assertTrue(answeringExpectations.dataChannel.send( new DataChannel.Buffer( ByteBuffer.wrap(new byte[] { 1, 2, 3, 4, 5 }), true))); offeringExpectations.waitForAllExpectationsToBeSatisfied(); offeringExpectations.expectStateChange(DataChannel.State.CLOSING); answeringExpectations.expectStateChange(DataChannel.State.CLOSING); offeringExpectations.expectStateChange(DataChannel.State.CLOSED); answeringExpectations.expectStateChange(DataChannel.State.CLOSED); answeringExpectations.dataChannel.close(); offeringExpectations.dataChannel.close(); if (RENDER_TO_GUI) { try { Thread.sleep(3000); } catch (Throwable t) { throw new RuntimeException(t); } } // TODO(fischman) MOAR test ideas: // - Test that PC.removeStream() works; requires a second // createOffer/createAnswer dance. // - audit each place that uses |constraints| for specifying non-trivial // constraints (and ensure they're honored). // - test error cases // - ensure reasonable coverage of _jni.cc is achieved. Coverage is // extra-important because of all the free-text (class/method names, etc) // in JNI-style programming; make sure no typos! // - Test that shutdown mid-interaction is crash-free. // Free the Java-land objects, collect them, and sleep a bit to make sure we // don't get late-arrival crashes after the Java-land objects have been // freed. shutdownPC(offeringPC, offeringExpectations); offeringPC = null; shutdownPC(answeringPC, answeringExpectations); answeringPC = null; videoSource.dispose(); factory.dispose(); System.gc(); TreeSet threadsAfterTest = allThreads(); assertEquals(threadsBeforeTest, threadsAfterTest); Thread.sleep(100); } private static void shutdownPC( PeerConnection pc, ObserverExpectations expectations) { expectations.dataChannel.unregisterObserver(); expectations.dataChannel.dispose(); expectations.expectStatsCallback(); assertTrue(pc.getStats(expectations, null)); expectations.waitForAllExpectationsToBeSatisfied(); expectations.expectIceConnectionChange(IceConnectionState.CLOSED); expectations.expectSignalingChange(SignalingState.CLOSED); pc.close(); expectations.waitForAllExpectationsToBeSatisfied(); expectations.expectStatsCallback(); assertTrue(pc.getStats(expectations, null)); expectations.waitForAllExpectationsToBeSatisfied(); System.out.println("FYI stats: "); int reportIndex = -1; for (StatsReport[] reports : expectations.takeStatsReports()) { System.out.println(" Report #" + (++reportIndex)); for (int i = 0; i < reports.length; ++i) { System.out.println(" " + reports[i].toString()); } } assertEquals(1, reportIndex); System.out.println("End stats."); pc.dispose(); } // Returns a set of thread IDs belonging to this process, as Strings. private static TreeSet allThreads() { TreeSet threads = new TreeSet(); // This pokes at /proc instead of using the Java APIs because we're also // looking for libjingle/webrtc native threads, most of which won't have // attached to the JVM. for (String threadId : (new File("/proc/self/task")).list()) { threads.add(threadId); } return threads; } // Return a String form of |strings| joined by |separator|. private static String joinStrings(String separator, TreeSet strings) { StringBuilder builder = new StringBuilder(); for (String s : strings) { if (builder.length() > 0) { builder.append(separator); } builder.append(s); } return builder.toString(); } }