This CL fixes the problem of misfired negotiationneeded notifications due to the lack of a NegotiationNeeded slot and the proper procedure to update it. Change-Id: Ie273c691f11316c9846606446f6cf838226b5d5c Bug: chromium:740501 Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/131283 Commit-Queue: Guido Urdaneta <guidou@webrtc.org> Reviewed-by: Steve Anton <steveanton@webrtc.org> Reviewed-by: Henrik Boström <hbos@webrtc.org> Reviewed-by: Magnus Jedvert <magjed@webrtc.org> Cr-Commit-Position: refs/heads/master@{#27594}
1834 lines
80 KiB
Java
1834 lines
80 KiB
Java
/*
|
|
* Copyright 2013 The WebRTC project authors. All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by a BSD-style license
|
|
* that can be found in the LICENSE file in the root of the source
|
|
* tree. An additional intellectual property rights grant can be found
|
|
* in the file PATENTS. All contributing project authors may
|
|
* be found in the AUTHORS file in the root of the source tree.
|
|
*/
|
|
|
|
package org.webrtc;
|
|
|
|
import static com.google.common.truth.Truth.assertThat;
|
|
import static java.util.Collections.singletonList;
|
|
import static org.junit.Assert.assertEquals;
|
|
import static org.junit.Assert.assertFalse;
|
|
import static org.junit.Assert.assertNotEquals;
|
|
import static org.junit.Assert.assertNotNull;
|
|
import static org.junit.Assert.assertNull;
|
|
import static org.junit.Assert.assertTrue;
|
|
import static org.junit.Assert.fail;
|
|
|
|
import android.support.annotation.Nullable;
|
|
import android.support.test.InstrumentationRegistry;
|
|
import android.support.test.filters.MediumTest;
|
|
import android.support.test.filters.SmallTest;
|
|
import java.io.File;
|
|
import java.lang.ref.WeakReference;
|
|
import java.nio.ByteBuffer;
|
|
import java.nio.charset.Charset;
|
|
import java.util.ArrayDeque;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.Collections;
|
|
import java.util.HashSet;
|
|
import java.util.IdentityHashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Queue;
|
|
import java.util.TreeSet;
|
|
import java.util.concurrent.CountDownLatch;
|
|
import java.util.concurrent.TimeUnit;
|
|
import org.chromium.base.test.BaseJUnit4ClassRunner;
|
|
import org.chromium.base.test.util.DisabledTest;
|
|
import org.junit.Before;
|
|
import org.junit.Test;
|
|
import org.junit.runner.RunWith;
|
|
import org.webrtc.Metrics.HistogramInfo;
|
|
import org.webrtc.PeerConnection.IceConnectionState;
|
|
import org.webrtc.PeerConnection.IceGatheringState;
|
|
import org.webrtc.PeerConnection.PeerConnectionState;
|
|
import org.webrtc.PeerConnection.SignalingState;
|
|
import org.webrtc.PeerConnection.TlsCertPolicy;
|
|
import org.webrtc.RtpParameters;
|
|
import org.webrtc.RtpParameters.Encoding;
|
|
import org.webrtc.RtpTransceiver;
|
|
import org.webrtc.RtpTransceiver.RtpTransceiverInit;
|
|
|
|
/** End-to-end tests for PeerConnection.java. */
|
|
@RunWith(BaseJUnit4ClassRunner.class)
|
|
public class PeerConnectionTest {
|
|
private static final int TIMEOUT_SECONDS = 20;
|
|
private @Nullable TreeSet<String> threadsBeforeTest;
|
|
|
|
@Before
|
|
public void setUp() {
|
|
PeerConnectionFactory.initialize(PeerConnectionFactory.InitializationOptions
|
|
.builder(InstrumentationRegistry.getTargetContext())
|
|
.setNativeLibraryName(TestConstants.NATIVE_LIBRARY)
|
|
.createInitializationOptions());
|
|
}
|
|
|
|
private static class ObserverExpectations
|
|
implements PeerConnection.Observer, VideoSink, DataChannel.Observer, StatsObserver,
|
|
RTCStatsCollectorCallback, RtpReceiver.Observer {
|
|
private final String name;
|
|
private int expectedIceCandidates;
|
|
private int expectedErrors;
|
|
private int expectedRenegotiations;
|
|
private int expectedWidth;
|
|
private int expectedHeight;
|
|
private int expectedFramesDelivered;
|
|
private int expectedTracksAdded;
|
|
private Queue<SignalingState> expectedSignalingChanges = new ArrayDeque<>();
|
|
private Queue<IceConnectionState> expectedIceConnectionChanges = new ArrayDeque<>();
|
|
private Queue<PeerConnectionState> expectedConnectionChanges = new ArrayDeque<>();
|
|
private Queue<IceGatheringState> expectedIceGatheringChanges = new ArrayDeque<>();
|
|
private Queue<String> expectedAddStreamLabels = new ArrayDeque<>();
|
|
private Queue<String> expectedRemoveStreamLabels = new ArrayDeque<>();
|
|
private final List<IceCandidate> gotIceCandidates = new ArrayList<>();
|
|
private Map<MediaStream, WeakReference<VideoSink>> videoSinks = new IdentityHashMap<>();
|
|
private DataChannel dataChannel;
|
|
private Queue<DataChannel.Buffer> expectedBuffers = new ArrayDeque<>();
|
|
private Queue<DataChannel.State> expectedStateChanges = new ArrayDeque<>();
|
|
private Queue<String> expectedRemoteDataChannelLabels = new ArrayDeque<>();
|
|
private int expectedOldStatsCallbacks;
|
|
private int expectedNewStatsCallbacks;
|
|
private List<StatsReport[]> gotStatsReports = new ArrayList<>();
|
|
private final HashSet<MediaStream> gotRemoteStreams = new HashSet<>();
|
|
private int expectedFirstAudioPacket;
|
|
private int expectedFirstVideoPacket;
|
|
|
|
public ObserverExpectations(String name) {
|
|
this.name = name;
|
|
}
|
|
|
|
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
|
|
@SuppressWarnings("NoSynchronizedMethodCheck")
|
|
public synchronized void setDataChannel(DataChannel dataChannel) {
|
|
assertNull(this.dataChannel);
|
|
this.dataChannel = dataChannel;
|
|
this.dataChannel.registerObserver(this);
|
|
assertNotNull(this.dataChannel);
|
|
}
|
|
|
|
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
|
|
@SuppressWarnings("NoSynchronizedMethodCheck")
|
|
public synchronized void expectIceCandidates(int count) {
|
|
expectedIceCandidates += count;
|
|
}
|
|
|
|
@Override
|
|
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
|
|
@SuppressWarnings("NoSynchronizedMethodCheck")
|
|
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.
|
|
synchronized (gotIceCandidates) {
|
|
gotIceCandidates.add(candidate);
|
|
gotIceCandidates.notifyAll();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onIceCandidatesRemoved(IceCandidate[] candidates) {}
|
|
|
|
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
|
|
@SuppressWarnings("NoSynchronizedMethodCheck")
|
|
public synchronized void setExpectedResolution(int width, int height) {
|
|
expectedWidth = width;
|
|
expectedHeight = height;
|
|
}
|
|
|
|
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
|
|
@SuppressWarnings("NoSynchronizedMethodCheck")
|
|
public synchronized void expectFramesDelivered(int count) {
|
|
expectedFramesDelivered += count;
|
|
}
|
|
|
|
@Override
|
|
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
|
|
@SuppressWarnings("NoSynchronizedMethodCheck")
|
|
public synchronized void onFrame(VideoFrame frame) {
|
|
if (expectedFramesDelivered <= 0) {
|
|
return;
|
|
}
|
|
assertTrue(expectedWidth > 0);
|
|
assertTrue(expectedHeight > 0);
|
|
assertEquals(expectedWidth, frame.getRotatedWidth());
|
|
assertEquals(expectedHeight, frame.getRotatedHeight());
|
|
--expectedFramesDelivered;
|
|
}
|
|
|
|
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
|
|
@SuppressWarnings("NoSynchronizedMethodCheck")
|
|
public synchronized void expectSignalingChange(SignalingState newState) {
|
|
expectedSignalingChanges.add(newState);
|
|
}
|
|
|
|
@Override
|
|
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
|
|
@SuppressWarnings("NoSynchronizedMethodCheck")
|
|
public synchronized void onSignalingChange(SignalingState newState) {
|
|
assertEquals(expectedSignalingChanges.remove(), newState);
|
|
}
|
|
|
|
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
|
|
@SuppressWarnings("NoSynchronizedMethodCheck")
|
|
public synchronized void expectIceConnectionChange(IceConnectionState newState) {
|
|
expectedIceConnectionChanges.add(newState);
|
|
}
|
|
|
|
@Override
|
|
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
|
|
@SuppressWarnings("NoSynchronizedMethodCheck")
|
|
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;
|
|
}
|
|
|
|
if (expectedIceConnectionChanges.isEmpty()) {
|
|
System.out.println(name + "Got an unexpected ICE connection change " + newState);
|
|
return;
|
|
}
|
|
|
|
assertEquals(expectedIceConnectionChanges.remove(), newState);
|
|
}
|
|
|
|
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
|
|
@SuppressWarnings("NoSynchronizedMethodCheck")
|
|
public synchronized void expectConnectionChange(PeerConnectionState newState) {
|
|
expectedConnectionChanges.add(newState);
|
|
}
|
|
|
|
@Override
|
|
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
|
|
@SuppressWarnings("NoSynchronizedMethodCheck")
|
|
public synchronized void onConnectionChange(PeerConnectionState newState) {
|
|
if (expectedConnectionChanges.isEmpty()) {
|
|
System.out.println(name + " got an unexpected DTLS connection change " + newState);
|
|
return;
|
|
}
|
|
|
|
assertEquals(expectedConnectionChanges.remove(), newState);
|
|
}
|
|
|
|
@Override
|
|
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
|
|
@SuppressWarnings("NoSynchronizedMethodCheck")
|
|
public synchronized void onIceConnectionReceivingChange(boolean receiving) {
|
|
System.out.println(name + " got an ICE connection receiving change " + receiving);
|
|
}
|
|
|
|
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
|
|
@SuppressWarnings("NoSynchronizedMethodCheck")
|
|
public synchronized void expectIceGatheringChange(IceGatheringState newState) {
|
|
expectedIceGatheringChanges.add(newState);
|
|
}
|
|
|
|
@Override
|
|
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
|
|
@SuppressWarnings("NoSynchronizedMethodCheck")
|
|
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;
|
|
}
|
|
if (expectedIceGatheringChanges.isEmpty()) {
|
|
System.out.println(name + "Got an unexpected ICE gathering change " + newState);
|
|
}
|
|
assertEquals(expectedIceGatheringChanges.remove(), newState);
|
|
}
|
|
|
|
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
|
|
@SuppressWarnings("NoSynchronizedMethodCheck")
|
|
public synchronized void expectAddStream(String label) {
|
|
expectedAddStreamLabels.add(label);
|
|
}
|
|
|
|
@Override
|
|
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
|
|
@SuppressWarnings("NoSynchronizedMethodCheck")
|
|
public synchronized void onAddStream(MediaStream stream) {
|
|
assertEquals(expectedAddStreamLabels.remove(), stream.getId());
|
|
for (AudioTrack track : stream.audioTracks) {
|
|
assertEquals("audio", track.kind());
|
|
}
|
|
for (VideoTrack track : stream.videoTracks) {
|
|
assertEquals("video", track.kind());
|
|
track.addSink(this);
|
|
assertNull(videoSinks.put(stream, new WeakReference<VideoSink>(this)));
|
|
}
|
|
gotRemoteStreams.add(stream);
|
|
}
|
|
|
|
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
|
|
@SuppressWarnings("NoSynchronizedMethodCheck")
|
|
public synchronized void expectRemoveStream(String label) {
|
|
expectedRemoveStreamLabels.add(label);
|
|
}
|
|
|
|
@Override
|
|
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
|
|
@SuppressWarnings("NoSynchronizedMethodCheck")
|
|
public synchronized void onRemoveStream(MediaStream stream) {
|
|
assertEquals(expectedRemoveStreamLabels.remove(), stream.getId());
|
|
WeakReference<VideoSink> videoSink = videoSinks.remove(stream);
|
|
assertNotNull(videoSink);
|
|
assertNotNull(videoSink.get());
|
|
for (VideoTrack videoTrack : stream.videoTracks) {
|
|
videoTrack.removeSink(videoSink.get());
|
|
}
|
|
gotRemoteStreams.remove(stream);
|
|
}
|
|
|
|
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
|
|
@SuppressWarnings("NoSynchronizedMethodCheck")
|
|
public synchronized void expectDataChannel(String label) {
|
|
expectedRemoteDataChannelLabels.add(label);
|
|
}
|
|
|
|
@Override
|
|
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
|
|
@SuppressWarnings("NoSynchronizedMethodCheck")
|
|
public synchronized void onDataChannel(DataChannel remoteDataChannel) {
|
|
assertEquals(expectedRemoteDataChannelLabels.remove(), remoteDataChannel.label());
|
|
setDataChannel(remoteDataChannel);
|
|
assertEquals(DataChannel.State.CONNECTING, dataChannel.state());
|
|
}
|
|
|
|
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
|
|
@SuppressWarnings("NoSynchronizedMethodCheck")
|
|
public synchronized void expectRenegotiationNeeded() {
|
|
++expectedRenegotiations;
|
|
}
|
|
|
|
@Override
|
|
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
|
|
@SuppressWarnings("NoSynchronizedMethodCheck")
|
|
public synchronized void onRenegotiationNeeded() {
|
|
assertTrue(--expectedRenegotiations >= 0);
|
|
}
|
|
|
|
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
|
|
@SuppressWarnings("NoSynchronizedMethodCheck")
|
|
public synchronized void expectAddTrack(int expectedTracksAdded) {
|
|
this.expectedTracksAdded = expectedTracksAdded;
|
|
}
|
|
|
|
@Override
|
|
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
|
|
@SuppressWarnings("NoSynchronizedMethodCheck")
|
|
public synchronized void onAddTrack(RtpReceiver receiver, MediaStream[] mediaStreams) {
|
|
expectedTracksAdded--;
|
|
}
|
|
|
|
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
|
|
@SuppressWarnings("NoSynchronizedMethodCheck")
|
|
public synchronized void expectMessage(ByteBuffer expectedBuffer, boolean expectedBinary) {
|
|
expectedBuffers.add(new DataChannel.Buffer(expectedBuffer, expectedBinary));
|
|
}
|
|
|
|
@Override
|
|
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
|
|
@SuppressWarnings("NoSynchronizedMethodCheck")
|
|
public synchronized void onMessage(DataChannel.Buffer buffer) {
|
|
DataChannel.Buffer expected = expectedBuffers.remove();
|
|
assertEquals(expected.binary, buffer.binary);
|
|
assertTrue(expected.data.equals(buffer.data));
|
|
}
|
|
|
|
@Override
|
|
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
|
|
@SuppressWarnings("NoSynchronizedMethodCheck")
|
|
public synchronized void onBufferedAmountChange(long previousAmount) {
|
|
assertFalse(previousAmount == dataChannel.bufferedAmount());
|
|
}
|
|
|
|
@Override
|
|
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
|
|
@SuppressWarnings("NoSynchronizedMethodCheck")
|
|
public synchronized void onStateChange() {
|
|
assertEquals(expectedStateChanges.remove(), dataChannel.state());
|
|
}
|
|
|
|
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
|
|
@SuppressWarnings("NoSynchronizedMethodCheck")
|
|
public synchronized void expectStateChange(DataChannel.State state) {
|
|
expectedStateChanges.add(state);
|
|
}
|
|
|
|
// Old getStats callback.
|
|
@Override
|
|
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
|
|
@SuppressWarnings("NoSynchronizedMethodCheck")
|
|
public synchronized void onComplete(StatsReport[] reports) {
|
|
if (--expectedOldStatsCallbacks < 0) {
|
|
throw new RuntimeException("Unexpected stats report: " + Arrays.toString(reports));
|
|
}
|
|
gotStatsReports.add(reports);
|
|
}
|
|
|
|
// New getStats callback.
|
|
@Override
|
|
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
|
|
@SuppressWarnings("NoSynchronizedMethodCheck")
|
|
public synchronized void onStatsDelivered(RTCStatsReport report) {
|
|
if (--expectedNewStatsCallbacks < 0) {
|
|
throw new RuntimeException("Unexpected stats report: " + report);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
|
|
@SuppressWarnings("NoSynchronizedMethodCheck")
|
|
public synchronized void onFirstPacketReceived(MediaStreamTrack.MediaType mediaType) {
|
|
if (mediaType == MediaStreamTrack.MediaType.MEDIA_TYPE_AUDIO) {
|
|
expectedFirstAudioPacket--;
|
|
} else {
|
|
expectedFirstVideoPacket--;
|
|
}
|
|
if (expectedFirstAudioPacket < 0 || expectedFirstVideoPacket < 0) {
|
|
throw new RuntimeException("Unexpected call of onFirstPacketReceived");
|
|
}
|
|
}
|
|
|
|
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
|
|
@SuppressWarnings("NoSynchronizedMethodCheck")
|
|
public synchronized void expectFirstPacketReceived() {
|
|
expectedFirstAudioPacket = 1;
|
|
expectedFirstVideoPacket = 1;
|
|
}
|
|
|
|
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
|
|
@SuppressWarnings("NoSynchronizedMethodCheck")
|
|
public synchronized void expectOldStatsCallback() {
|
|
++expectedOldStatsCallbacks;
|
|
}
|
|
|
|
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
|
|
@SuppressWarnings("NoSynchronizedMethodCheck")
|
|
public synchronized void expectNewStatsCallback() {
|
|
++expectedNewStatsCallbacks;
|
|
}
|
|
|
|
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
|
|
@SuppressWarnings("NoSynchronizedMethodCheck")
|
|
public synchronized List<StatsReport[]> takeStatsReports() {
|
|
List<StatsReport[]> got = gotStatsReports;
|
|
gotStatsReports = new ArrayList<StatsReport[]>();
|
|
return got;
|
|
}
|
|
|
|
// Return a set of expectations that haven't been satisfied yet, possibly
|
|
// empty if no such expectations exist.
|
|
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
|
|
@SuppressWarnings("NoSynchronizedMethodCheck")
|
|
public synchronized TreeSet<String> unsatisfiedExpectations() {
|
|
TreeSet<String> stillWaitingForExpectations = new TreeSet<String>();
|
|
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 (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 (expectedOldStatsCallbacks != 0) {
|
|
stillWaitingForExpectations.add("expectedOldStatsCallbacks: " + expectedOldStatsCallbacks);
|
|
}
|
|
if (expectedNewStatsCallbacks != 0) {
|
|
stillWaitingForExpectations.add("expectedNewStatsCallbacks: " + expectedNewStatsCallbacks);
|
|
}
|
|
if (expectedFirstAudioPacket > 0) {
|
|
stillWaitingForExpectations.add("expectedFirstAudioPacket: " + expectedFirstAudioPacket);
|
|
}
|
|
if (expectedFirstVideoPacket > 0) {
|
|
stillWaitingForExpectations.add("expectedFirstVideoPacket: " + expectedFirstVideoPacket);
|
|
}
|
|
if (expectedTracksAdded != 0) {
|
|
stillWaitingForExpectations.add("expectedAddedTrack: " + expectedTracksAdded);
|
|
}
|
|
return stillWaitingForExpectations;
|
|
}
|
|
|
|
public boolean waitForAllExpectationsToBeSatisfied(int timeoutSeconds) {
|
|
// 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.
|
|
long endTime = System.currentTimeMillis() + 1000 * timeoutSeconds;
|
|
TreeSet<String> prev = null;
|
|
TreeSet<String> 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()));
|
|
}
|
|
if (endTime < System.currentTimeMillis()) {
|
|
System.out.println(name + " timed out waiting for: "
|
|
+ Arrays.toString(stillWaitingForExpectations.toArray()));
|
|
return false;
|
|
}
|
|
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]);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// This methods return a list of all currently gathered ice candidates or waits until
|
|
// 1 candidate have been gathered.
|
|
public List<IceCandidate> getAtLeastOneIceCandidate() throws InterruptedException {
|
|
synchronized (gotIceCandidates) {
|
|
while (gotIceCandidates.isEmpty()) {
|
|
gotIceCandidates.wait();
|
|
}
|
|
return new ArrayList<IceCandidate>(gotIceCandidates);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sets the expected resolution for an ObserverExpectations once a frame
|
|
// has been captured.
|
|
private static class ExpectedResolutionSetter implements VideoSink {
|
|
private ObserverExpectations observer;
|
|
|
|
public ExpectedResolutionSetter(ObserverExpectations observer) {
|
|
this.observer = observer;
|
|
}
|
|
|
|
@Override
|
|
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
|
|
@SuppressWarnings("NoSynchronizedMethodCheck")
|
|
public synchronized void onFrame(VideoFrame frame) {
|
|
// Because different camera devices (fake & physical) produce different
|
|
// resolutions, we only sanity-check the set sizes,
|
|
assertTrue(frame.getRotatedWidth() > 0);
|
|
assertTrue(frame.getRotatedHeight() > 0);
|
|
observer.setExpectedResolution(frame.getRotatedWidth(), frame.getRotatedHeight());
|
|
frame.retain();
|
|
}
|
|
}
|
|
|
|
private static class SdpObserverLatch implements SdpObserver {
|
|
private boolean success;
|
|
private @Nullable SessionDescription sdp;
|
|
private @Nullable String error;
|
|
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 @Nullable SessionDescription getSdp() {
|
|
return sdp;
|
|
}
|
|
|
|
public @Nullable String getError() {
|
|
return error;
|
|
}
|
|
}
|
|
|
|
static int videoWindowsMapped = -1;
|
|
|
|
// Return a weak reference to test that ownership is correctly held by
|
|
// PeerConnection, not by test code.
|
|
private static WeakReference<MediaStream> addTracksToPC(PeerConnectionFactory factory,
|
|
PeerConnection pc, VideoSource videoSource, String streamLabel, String videoTrackId,
|
|
String audioTrackId, VideoSink videoSink) {
|
|
MediaStream lMS = factory.createLocalMediaStream(streamLabel);
|
|
VideoTrack videoTrack = factory.createVideoTrack(videoTrackId, videoSource);
|
|
assertNotNull(videoTrack);
|
|
assertNotNull(videoSink);
|
|
videoTrack.addSink(videoSink);
|
|
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);
|
|
return new WeakReference<MediaStream>(lMS);
|
|
}
|
|
|
|
// Used for making sure thread handles are not leaked.
|
|
// Call initializeThreadCheck before a test and finalizeThreadCheck after
|
|
// a test.
|
|
void initializeThreadCheck() {
|
|
System.gc(); // Encourage any GC-related threads to start up.
|
|
threadsBeforeTest = allThreads();
|
|
}
|
|
|
|
void finalizeThreadCheck() throws Exception {
|
|
// TreeSet<String> threadsAfterTest = allThreads();
|
|
|
|
// TODO(tommi): Figure out a more reliable way to do this test. As is
|
|
// we're seeing three possible 'normal' situations:
|
|
// 1. before and after sets are equal.
|
|
// 2. before contains 3 threads that do not exist in after.
|
|
// 3. after contains 3 threads that do not exist in before.
|
|
//
|
|
// Maybe it would be better to do the thread enumeration from C++ and get
|
|
// the thread names as well, in order to determine what these 3 threads are.
|
|
|
|
// assertEquals(threadsBeforeTest, threadsAfterTest);
|
|
// Thread.sleep(100);
|
|
}
|
|
|
|
@Test
|
|
@SmallTest
|
|
public void testIceServerChanged() throws Exception {
|
|
PeerConnection.IceServer iceServer1 =
|
|
PeerConnection.IceServer.builder("turn:fake.example.com")
|
|
.setUsername("fakeUsername")
|
|
.setPassword("fakePassword")
|
|
.setTlsCertPolicy(TlsCertPolicy.TLS_CERT_POLICY_SECURE)
|
|
.setHostname("fakeHostname")
|
|
.setTlsAlpnProtocols(singletonList("fakeTlsAlpnProtocol"))
|
|
.setTlsEllipticCurves(singletonList("fakeTlsEllipticCurve"))
|
|
.createIceServer();
|
|
// Same as iceServer1.
|
|
PeerConnection.IceServer iceServer2 =
|
|
PeerConnection.IceServer.builder("turn:fake.example.com")
|
|
.setUsername("fakeUsername")
|
|
.setPassword("fakePassword")
|
|
.setTlsCertPolicy(TlsCertPolicy.TLS_CERT_POLICY_SECURE)
|
|
.setHostname("fakeHostname")
|
|
.setTlsAlpnProtocols(singletonList("fakeTlsAlpnProtocol"))
|
|
.setTlsEllipticCurves(singletonList("fakeTlsEllipticCurve"))
|
|
.createIceServer();
|
|
// Differs from iceServer1 by the url.
|
|
PeerConnection.IceServer iceServer3 =
|
|
PeerConnection.IceServer.builder("turn:fake.example2.com")
|
|
.setUsername("fakeUsername")
|
|
.setPassword("fakePassword")
|
|
.setTlsCertPolicy(TlsCertPolicy.TLS_CERT_POLICY_SECURE)
|
|
.setHostname("fakeHostname")
|
|
.setTlsAlpnProtocols(singletonList("fakeTlsAlpnProtocol"))
|
|
.setTlsEllipticCurves(singletonList("fakeTlsEllipticCurve"))
|
|
.createIceServer();
|
|
// Differs from iceServer1 by the username.
|
|
PeerConnection.IceServer iceServer4 =
|
|
PeerConnection.IceServer.builder("turn:fake.example.com")
|
|
.setUsername("fakeUsername2")
|
|
.setPassword("fakePassword")
|
|
.setTlsCertPolicy(TlsCertPolicy.TLS_CERT_POLICY_SECURE)
|
|
.setHostname("fakeHostname")
|
|
.setTlsAlpnProtocols(singletonList("fakeTlsAlpnProtocol"))
|
|
.setTlsEllipticCurves(singletonList("fakeTlsEllipticCurve"))
|
|
.createIceServer();
|
|
// Differs from iceServer1 by the password.
|
|
PeerConnection.IceServer iceServer5 =
|
|
PeerConnection.IceServer.builder("turn:fake.example.com")
|
|
.setUsername("fakeUsername")
|
|
.setPassword("fakePassword2")
|
|
.setTlsCertPolicy(TlsCertPolicy.TLS_CERT_POLICY_SECURE)
|
|
.setHostname("fakeHostname")
|
|
.setTlsAlpnProtocols(singletonList("fakeTlsAlpnProtocol"))
|
|
.setTlsEllipticCurves(singletonList("fakeTlsEllipticCurve"))
|
|
.createIceServer();
|
|
// Differs from iceServer1 by the TLS certificate policy.
|
|
PeerConnection.IceServer iceServer6 =
|
|
PeerConnection.IceServer.builder("turn:fake.example.com")
|
|
.setUsername("fakeUsername")
|
|
.setPassword("fakePassword")
|
|
.setTlsCertPolicy(TlsCertPolicy.TLS_CERT_POLICY_INSECURE_NO_CHECK)
|
|
.setHostname("fakeHostname")
|
|
.setTlsAlpnProtocols(singletonList("fakeTlsAlpnProtocol"))
|
|
.setTlsEllipticCurves(singletonList("fakeTlsEllipticCurve"))
|
|
.createIceServer();
|
|
// Differs from iceServer1 by the hostname.
|
|
PeerConnection.IceServer iceServer7 =
|
|
PeerConnection.IceServer.builder("turn:fake.example.com")
|
|
.setUsername("fakeUsername")
|
|
.setPassword("fakePassword")
|
|
.setTlsCertPolicy(TlsCertPolicy.TLS_CERT_POLICY_INSECURE_NO_CHECK)
|
|
.setHostname("fakeHostname2")
|
|
.setTlsAlpnProtocols(singletonList("fakeTlsAlpnProtocol"))
|
|
.setTlsEllipticCurves(singletonList("fakeTlsEllipticCurve"))
|
|
.createIceServer();
|
|
// Differs from iceServer1 by the TLS ALPN.
|
|
PeerConnection.IceServer iceServer8 =
|
|
PeerConnection.IceServer.builder("turn:fake.example.com")
|
|
.setUsername("fakeUsername")
|
|
.setPassword("fakePassword")
|
|
.setTlsCertPolicy(TlsCertPolicy.TLS_CERT_POLICY_INSECURE_NO_CHECK)
|
|
.setHostname("fakeHostname")
|
|
.setTlsAlpnProtocols(singletonList("fakeTlsAlpnProtocol2"))
|
|
.setTlsEllipticCurves(singletonList("fakeTlsEllipticCurve"))
|
|
.createIceServer();
|
|
// Differs from iceServer1 by the TLS elliptic curve.
|
|
PeerConnection.IceServer iceServer9 =
|
|
PeerConnection.IceServer.builder("turn:fake.example.com")
|
|
.setUsername("fakeUsername")
|
|
.setPassword("fakePassword")
|
|
.setTlsCertPolicy(TlsCertPolicy.TLS_CERT_POLICY_INSECURE_NO_CHECK)
|
|
.setHostname("fakeHostname")
|
|
.setTlsAlpnProtocols(singletonList("fakeTlsAlpnProtocol"))
|
|
.setTlsEllipticCurves(singletonList("fakeTlsEllipticCurve2"))
|
|
.createIceServer();
|
|
|
|
assertTrue(iceServer1.equals(iceServer2));
|
|
assertFalse(iceServer1.equals(iceServer3));
|
|
assertFalse(iceServer1.equals(iceServer4));
|
|
assertFalse(iceServer1.equals(iceServer5));
|
|
assertFalse(iceServer1.equals(iceServer6));
|
|
assertFalse(iceServer1.equals(iceServer7));
|
|
assertFalse(iceServer1.equals(iceServer8));
|
|
assertFalse(iceServer1.equals(iceServer9));
|
|
}
|
|
|
|
// 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 code 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.
|
|
|
|
// Tests that the JNI glue between Java and C++ does not crash when creating a PeerConnection.
|
|
@Test
|
|
@SmallTest
|
|
public void testCreationWithConfig() throws Exception {
|
|
PeerConnectionFactory factory = PeerConnectionFactory.builder().createPeerConnectionFactory();
|
|
List<PeerConnection.IceServer> iceServers = Arrays.asList(
|
|
PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer(),
|
|
PeerConnection.IceServer.builder("turn:fake.example.com")
|
|
.setUsername("fakeUsername")
|
|
.setPassword("fakePassword")
|
|
.createIceServer());
|
|
PeerConnection.RTCConfiguration config = new PeerConnection.RTCConfiguration(iceServers);
|
|
|
|
// Test configuration options.
|
|
config.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY;
|
|
config.iceRegatherIntervalRange = new PeerConnection.IntervalRange(1000, 2000);
|
|
|
|
ObserverExpectations offeringExpectations = new ObserverExpectations("PCTest:offerer");
|
|
PeerConnection offeringPC = factory.createPeerConnection(config, offeringExpectations);
|
|
assertNotNull(offeringPC);
|
|
}
|
|
|
|
@Test
|
|
@SmallTest
|
|
public void testCreationWithCertificate() throws Exception {
|
|
PeerConnectionFactory factory = PeerConnectionFactory.builder().createPeerConnectionFactory();
|
|
PeerConnection.RTCConfiguration config = new PeerConnection.RTCConfiguration(Arrays.asList());
|
|
|
|
// Test certificate.
|
|
RtcCertificatePem originalCert = RtcCertificatePem.generateCertificate();
|
|
config.certificate = originalCert;
|
|
|
|
ObserverExpectations offeringExpectations = new ObserverExpectations("PCTest:offerer");
|
|
PeerConnection offeringPC = factory.createPeerConnection(config, offeringExpectations);
|
|
|
|
RtcCertificatePem restoredCert = offeringPC.getCertificate();
|
|
assertEquals(originalCert.privateKey, restoredCert.privateKey);
|
|
assertEquals(originalCert.certificate, restoredCert.certificate);
|
|
}
|
|
|
|
@Test
|
|
@SmallTest
|
|
public void testCreationWithCryptoOptions() throws Exception {
|
|
PeerConnectionFactory factory = PeerConnectionFactory.builder().createPeerConnectionFactory();
|
|
PeerConnection.RTCConfiguration config = new PeerConnection.RTCConfiguration(Arrays.asList());
|
|
|
|
assertNull(config.cryptoOptions);
|
|
|
|
CryptoOptions cryptoOptions = CryptoOptions.builder()
|
|
.setEnableGcmCryptoSuites(true)
|
|
.setEnableAes128Sha1_32CryptoCipher(true)
|
|
.setEnableEncryptedRtpHeaderExtensions(true)
|
|
.setRequireFrameEncryption(true)
|
|
.createCryptoOptions();
|
|
config.cryptoOptions = cryptoOptions;
|
|
|
|
ObserverExpectations offeringExpectations = new ObserverExpectations("PCTest:offerer");
|
|
PeerConnection offeringPC = factory.createPeerConnection(config, offeringExpectations);
|
|
assertNotNull(offeringPC);
|
|
}
|
|
|
|
// Test that RIDs get set in the RTP sender when passed in through an RtpTransceiverInit.
|
|
@Test
|
|
@SmallTest
|
|
public void testSetRidInSimulcast() throws Exception {
|
|
PeerConnectionFactory factory = PeerConnectionFactory.builder().createPeerConnectionFactory();
|
|
PeerConnection.RTCConfiguration config = new PeerConnection.RTCConfiguration(Arrays.asList());
|
|
config.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN;
|
|
ObserverExpectations expectations = new ObserverExpectations("PCTest:simulcast_rids");
|
|
expectations.expectRenegotiationNeeded();
|
|
PeerConnection pc = factory.createPeerConnection(config, expectations);
|
|
List<Encoding> encodings = new ArrayList<Encoding>();
|
|
encodings.add(new Encoding("F", true, null));
|
|
encodings.add(new Encoding("H", true, null));
|
|
RtpTransceiverInit init = new RtpTransceiverInit(
|
|
RtpTransceiver.RtpTransceiverDirection.SEND_ONLY, Collections.emptyList(), encodings);
|
|
RtpTransceiver transceiver =
|
|
pc.addTransceiver(MediaStreamTrack.MediaType.MEDIA_TYPE_VIDEO, init);
|
|
RtpSender sender = transceiver.getSender();
|
|
RtpParameters parameters = sender.getParameters();
|
|
assertNotNull(parameters);
|
|
List<Encoding> sendEncodings = parameters.getEncodings();
|
|
assertNotNull(sendEncodings);
|
|
assertEquals(2, sendEncodings.size());
|
|
assertEquals("F", sendEncodings.get(0).getRid());
|
|
assertEquals("H", sendEncodings.get(1).getRid());
|
|
}
|
|
|
|
@Test
|
|
@MediumTest
|
|
public void testCompleteSession() throws Exception {
|
|
Metrics.enable();
|
|
// Allow loopback interfaces too since our Android devices often don't
|
|
// have those.
|
|
PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
|
|
options.networkIgnoreMask = 0;
|
|
PeerConnectionFactory factory = PeerConnectionFactory.builder()
|
|
.setOptions(options)
|
|
.setVideoEncoderFactory(new SoftwareVideoEncoderFactory())
|
|
.setVideoDecoderFactory(new SoftwareVideoDecoderFactory())
|
|
.createPeerConnectionFactory();
|
|
|
|
List<PeerConnection.IceServer> iceServers = new ArrayList<>();
|
|
iceServers.add(
|
|
PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer());
|
|
iceServers.add(PeerConnection.IceServer.builder("turn:fake.example.com")
|
|
.setUsername("fakeUsername")
|
|
.setPassword("fakePassword")
|
|
.createIceServer());
|
|
|
|
PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(iceServers);
|
|
rtcConfig.enableDtlsSrtp = true;
|
|
|
|
ObserverExpectations offeringExpectations = new ObserverExpectations("PCTest:offerer");
|
|
PeerConnection offeringPC = factory.createPeerConnection(rtcConfig, offeringExpectations);
|
|
assertNotNull(offeringPC);
|
|
|
|
ObserverExpectations answeringExpectations = new ObserverExpectations("PCTest:answerer");
|
|
PeerConnection answeringPC = factory.createPeerConnection(rtcConfig, answeringExpectations);
|
|
assertNotNull(answeringPC);
|
|
|
|
// We want to use the same camera for offerer & answerer, so create it here
|
|
// instead of in addTracksToPC.
|
|
final CameraEnumerator enumerator = new Camera1Enumerator(false /* captureToTexture */);
|
|
final VideoCapturer videoCapturer =
|
|
enumerator.createCapturer(enumerator.getDeviceNames()[0], null /* eventsHandler */);
|
|
final SurfaceTextureHelper surfaceTextureHelper =
|
|
SurfaceTextureHelper.create("SurfaceTextureHelper", /* sharedContext= */ null);
|
|
final VideoSource videoSource = factory.createVideoSource(/* isScreencast= */ false);
|
|
videoCapturer.initialize(surfaceTextureHelper, InstrumentationRegistry.getTargetContext(),
|
|
videoSource.getCapturerObserver());
|
|
videoCapturer.startCapture(640, 480, 30);
|
|
|
|
offeringExpectations.expectRenegotiationNeeded();
|
|
WeakReference<MediaStream> oLMS =
|
|
addTracksToPC(factory, offeringPC, videoSource, "offeredMediaStream", "offeredVideoTrack",
|
|
"offeredAudioTrack", new ExpectedResolutionSetter(answeringExpectations));
|
|
|
|
offeringExpectations.expectAddTrack(2);
|
|
answeringExpectations.expectAddTrack(2);
|
|
|
|
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.expectRenegotiationNeeded();
|
|
WeakReference<MediaStream> aLMS = addTracksToPC(factory, answeringPC, videoSource,
|
|
"answeredMediaStream", "answeredVideoTrack", "answeredAudioTrack",
|
|
new ExpectedResolutionSetter(offeringExpectations));
|
|
|
|
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);
|
|
answeringExpectations.expectConnectionChange(PeerConnectionState.CONNECTING);
|
|
answeringPC.setLocalDescription(sdpLatch, answerSdp);
|
|
assertTrue(sdpLatch.await());
|
|
assertNull(sdpLatch.getSdp());
|
|
|
|
sdpLatch = new SdpObserverLatch();
|
|
offeringExpectations.expectSignalingChange(SignalingState.HAVE_LOCAL_OFFER);
|
|
offeringExpectations.expectConnectionChange(PeerConnectionState.CONNECTING);
|
|
offeringPC.setLocalDescription(sdpLatch, offerSdp);
|
|
assertTrue(sdpLatch.await());
|
|
assertNull(sdpLatch.getSdp());
|
|
sdpLatch = new SdpObserverLatch();
|
|
offeringExpectations.expectSignalingChange(SignalingState.STABLE);
|
|
offeringExpectations.expectAddStream("answeredMediaStream");
|
|
|
|
offeringExpectations.expectIceConnectionChange(IceConnectionState.CHECKING);
|
|
offeringExpectations.expectIceConnectionChange(IceConnectionState.CONNECTED);
|
|
offeringExpectations.expectConnectionChange(PeerConnectionState.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);
|
|
answeringExpectations.expectConnectionChange(PeerConnectionState.CONNECTED);
|
|
|
|
offeringPC.setRemoteDescription(sdpLatch, answerSdp);
|
|
assertTrue(sdpLatch.await());
|
|
assertNull(sdpLatch.getSdp());
|
|
|
|
assertEquals(offeringPC.getLocalDescription().type, offerSdp.type);
|
|
assertEquals(offeringPC.getRemoteDescription().type, answerSdp.type);
|
|
assertEquals(answeringPC.getLocalDescription().type, answerSdp.type);
|
|
assertEquals(answeringPC.getRemoteDescription().type, offerSdp.type);
|
|
|
|
assertEquals(offeringPC.getSenders().size(), 2);
|
|
assertEquals(offeringPC.getReceivers().size(), 2);
|
|
assertEquals(answeringPC.getSenders().size(), 2);
|
|
assertEquals(answeringPC.getReceivers().size(), 2);
|
|
|
|
offeringExpectations.expectFirstPacketReceived();
|
|
answeringExpectations.expectFirstPacketReceived();
|
|
|
|
for (RtpReceiver receiver : offeringPC.getReceivers()) {
|
|
receiver.SetObserver(offeringExpectations);
|
|
}
|
|
|
|
for (RtpReceiver receiver : answeringPC.getReceivers()) {
|
|
receiver.SetObserver(answeringExpectations);
|
|
}
|
|
|
|
// Wait for at least some frames to be delivered at each end (number
|
|
// chosen arbitrarily).
|
|
offeringExpectations.expectFramesDelivered(10);
|
|
answeringExpectations.expectFramesDelivered(10);
|
|
|
|
offeringExpectations.expectStateChange(DataChannel.State.OPEN);
|
|
// See commentary about SCTP DataChannels above for why this is here.
|
|
answeringExpectations.expectDataChannel("offeringDC");
|
|
answeringExpectations.expectStateChange(DataChannel.State.OPEN);
|
|
|
|
// Wait for at least one ice candidate from the offering PC and forward them to the answering
|
|
// PC.
|
|
for (IceCandidate candidate : offeringExpectations.getAtLeastOneIceCandidate()) {
|
|
answeringPC.addIceCandidate(candidate);
|
|
}
|
|
|
|
// Wait for at least one ice candidate from the answering PC and forward them to the offering
|
|
// PC.
|
|
for (IceCandidate candidate : answeringExpectations.getAtLeastOneIceCandidate()) {
|
|
offeringPC.addIceCandidate(candidate);
|
|
}
|
|
|
|
assertTrue(offeringExpectations.waitForAllExpectationsToBeSatisfied(TIMEOUT_SECONDS));
|
|
assertTrue(answeringExpectations.waitForAllExpectationsToBeSatisfied(TIMEOUT_SECONDS));
|
|
|
|
assertEquals(PeerConnection.SignalingState.STABLE, offeringPC.signalingState());
|
|
assertEquals(PeerConnection.SignalingState.STABLE, answeringPC.signalingState());
|
|
|
|
// Test some of the RtpSender API.
|
|
RtpSender videoSender = null;
|
|
RtpSender audioSender = null;
|
|
for (RtpSender sender : offeringPC.getSenders()) {
|
|
if (sender.track().kind().equals("video")) {
|
|
videoSender = sender;
|
|
} else {
|
|
audioSender = sender;
|
|
}
|
|
}
|
|
assertNotNull(videoSender);
|
|
assertNotNull(audioSender);
|
|
|
|
// Set a bitrate limit for the outgoing video stream for the offerer.
|
|
RtpParameters rtpParameters = videoSender.getParameters();
|
|
assertNotNull(rtpParameters);
|
|
assertEquals(1, rtpParameters.encodings.size());
|
|
assertNull(rtpParameters.encodings.get(0).maxBitrateBps);
|
|
assertNull(rtpParameters.encodings.get(0).minBitrateBps);
|
|
assertNull(rtpParameters.encodings.get(0).maxFramerate);
|
|
assertNull(rtpParameters.encodings.get(0).numTemporalLayers);
|
|
assertNull(rtpParameters.encodings.get(0).scaleResolutionDownBy);
|
|
assertTrue(rtpParameters.encodings.get(0).rid.isEmpty());
|
|
|
|
rtpParameters.encodings.get(0).maxBitrateBps = 300000;
|
|
rtpParameters.encodings.get(0).minBitrateBps = 100000;
|
|
rtpParameters.encodings.get(0).maxFramerate = 20;
|
|
rtpParameters.encodings.get(0).numTemporalLayers = 2;
|
|
rtpParameters.encodings.get(0).scaleResolutionDownBy = 2.0;
|
|
assertTrue(videoSender.setParameters(rtpParameters));
|
|
|
|
// Create a DTMF sender.
|
|
DtmfSender dtmfSender = audioSender.dtmf();
|
|
assertNotNull(dtmfSender);
|
|
assertTrue(dtmfSender.canInsertDtmf());
|
|
assertTrue(dtmfSender.insertDtmf("123", 300, 100));
|
|
|
|
// Verify that we can read back the updated value.
|
|
rtpParameters = videoSender.getParameters();
|
|
assertEquals(300000, (int) rtpParameters.encodings.get(0).maxBitrateBps);
|
|
assertEquals(100000, (int) rtpParameters.encodings.get(0).minBitrateBps);
|
|
assertEquals(20, (int) rtpParameters.encodings.get(0).maxFramerate);
|
|
assertEquals(2, (int) rtpParameters.encodings.get(0).numTemporalLayers);
|
|
assertThat(rtpParameters.encodings.get(0).scaleResolutionDownBy).isEqualTo(2.0);
|
|
|
|
// 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));
|
|
assertTrue(answeringExpectations.waitForAllExpectationsToBeSatisfied(TIMEOUT_SECONDS));
|
|
|
|
// 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)));
|
|
assertTrue(offeringExpectations.waitForAllExpectationsToBeSatisfied(TIMEOUT_SECONDS));
|
|
|
|
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();
|
|
assertTrue(offeringExpectations.waitForAllExpectationsToBeSatisfied(TIMEOUT_SECONDS));
|
|
assertTrue(answeringExpectations.waitForAllExpectationsToBeSatisfied(TIMEOUT_SECONDS));
|
|
|
|
// Test SetBitrate.
|
|
assertTrue(offeringPC.setBitrate(100000, 5000000, 500000000));
|
|
assertFalse(offeringPC.setBitrate(3, 2, 1));
|
|
|
|
// Free the Java-land objects and collect them.
|
|
shutdownPC(offeringPC, offeringExpectations);
|
|
offeringPC = null;
|
|
shutdownPC(answeringPC, answeringExpectations);
|
|
answeringPC = null;
|
|
getMetrics();
|
|
videoCapturer.stopCapture();
|
|
videoCapturer.dispose();
|
|
videoSource.dispose();
|
|
surfaceTextureHelper.dispose();
|
|
factory.dispose();
|
|
System.gc();
|
|
}
|
|
|
|
// TODO(https://bugs.webrtc.org/10526): Fix and re-enable this test.
|
|
@Test
|
|
@MediumTest
|
|
@DisabledTest
|
|
public void testDataChannelOnlySession() throws Exception {
|
|
// Allow loopback interfaces too since our Android devices often don't
|
|
// have those.
|
|
PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
|
|
options.networkIgnoreMask = 0;
|
|
PeerConnectionFactory factory =
|
|
PeerConnectionFactory.builder().setOptions(options).createPeerConnectionFactory();
|
|
|
|
List<PeerConnection.IceServer> iceServers = new ArrayList<>();
|
|
iceServers.add(
|
|
PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer());
|
|
iceServers.add(PeerConnection.IceServer.builder("turn:fake.example.com")
|
|
.setUsername("fakeUsername")
|
|
.setPassword("fakePassword")
|
|
.createIceServer());
|
|
|
|
PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(iceServers);
|
|
rtcConfig.enableDtlsSrtp = true;
|
|
|
|
ObserverExpectations offeringExpectations = new ObserverExpectations("PCTest:offerer");
|
|
PeerConnection offeringPC = factory.createPeerConnection(rtcConfig, offeringExpectations);
|
|
assertNotNull(offeringPC);
|
|
|
|
ObserverExpectations answeringExpectations = new ObserverExpectations("PCTest:answerer");
|
|
PeerConnection answeringPC = factory.createPeerConnection(rtcConfig, answeringExpectations);
|
|
assertNotNull(answeringPC);
|
|
|
|
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);
|
|
// 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());
|
|
|
|
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);
|
|
answeringExpectations.expectConnectionChange(PeerConnectionState.CONNECTING);
|
|
answeringPC.setLocalDescription(sdpLatch, answerSdp);
|
|
assertTrue(sdpLatch.await());
|
|
assertNull(sdpLatch.getSdp());
|
|
|
|
sdpLatch = new SdpObserverLatch();
|
|
offeringExpectations.expectSignalingChange(SignalingState.HAVE_LOCAL_OFFER);
|
|
offeringExpectations.expectConnectionChange(PeerConnectionState.CONNECTING);
|
|
offeringPC.setLocalDescription(sdpLatch, offerSdp);
|
|
assertTrue(sdpLatch.await());
|
|
assertNull(sdpLatch.getSdp());
|
|
sdpLatch = new SdpObserverLatch();
|
|
offeringExpectations.expectSignalingChange(SignalingState.STABLE);
|
|
|
|
offeringExpectations.expectIceConnectionChange(IceConnectionState.CHECKING);
|
|
offeringExpectations.expectIceConnectionChange(IceConnectionState.CONNECTED);
|
|
offeringExpectations.expectConnectionChange(PeerConnectionState.CONNECTED);
|
|
// TODO(bemasc): uncomment once delivery of ICECompleted is reliable
|
|
// (https://code.google.com/p/webrtc/issues/detail?id=3021).
|
|
answeringExpectations.expectIceConnectionChange(IceConnectionState.CHECKING);
|
|
answeringExpectations.expectIceConnectionChange(IceConnectionState.CONNECTED);
|
|
answeringExpectations.expectConnectionChange(PeerConnectionState.CONNECTED);
|
|
|
|
offeringPC.setRemoteDescription(sdpLatch, answerSdp);
|
|
assertTrue(sdpLatch.await());
|
|
assertNull(sdpLatch.getSdp());
|
|
|
|
assertEquals(offeringPC.getLocalDescription().type, offerSdp.type);
|
|
assertEquals(offeringPC.getRemoteDescription().type, answerSdp.type);
|
|
assertEquals(answeringPC.getLocalDescription().type, answerSdp.type);
|
|
assertEquals(answeringPC.getRemoteDescription().type, offerSdp.type);
|
|
|
|
offeringExpectations.expectStateChange(DataChannel.State.OPEN);
|
|
// See commentary about SCTP DataChannels above for why this is here.
|
|
answeringExpectations.expectDataChannel("offeringDC");
|
|
answeringExpectations.expectStateChange(DataChannel.State.OPEN);
|
|
|
|
// Wait for at least one ice candidate from the offering PC and forward them to the answering
|
|
// PC.
|
|
for (IceCandidate candidate : offeringExpectations.getAtLeastOneIceCandidate()) {
|
|
answeringPC.addIceCandidate(candidate);
|
|
}
|
|
|
|
// Wait for at least one ice candidate from the answering PC and forward them to the offering
|
|
// PC.
|
|
for (IceCandidate candidate : answeringExpectations.getAtLeastOneIceCandidate()) {
|
|
offeringPC.addIceCandidate(candidate);
|
|
}
|
|
|
|
assertTrue(offeringExpectations.waitForAllExpectationsToBeSatisfied(TIMEOUT_SECONDS));
|
|
assertTrue(answeringExpectations.waitForAllExpectationsToBeSatisfied(TIMEOUT_SECONDS));
|
|
|
|
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));
|
|
assertTrue(answeringExpectations.waitForAllExpectationsToBeSatisfied(TIMEOUT_SECONDS));
|
|
|
|
// 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)));
|
|
assertTrue(offeringExpectations.waitForAllExpectationsToBeSatisfied(TIMEOUT_SECONDS));
|
|
|
|
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();
|
|
assertTrue(offeringExpectations.waitForAllExpectationsToBeSatisfied(TIMEOUT_SECONDS));
|
|
assertTrue(answeringExpectations.waitForAllExpectationsToBeSatisfied(TIMEOUT_SECONDS));
|
|
|
|
// Free the Java-land objects and collect them.
|
|
shutdownPC(offeringPC, offeringExpectations);
|
|
offeringPC = null;
|
|
shutdownPC(answeringPC, answeringExpectations);
|
|
answeringPC = null;
|
|
factory.dispose();
|
|
System.gc();
|
|
}
|
|
|
|
// TODO(https://bugs.webrtc.org/10526): Fix and re-enable this test.
|
|
@Test
|
|
@MediumTest
|
|
@DisabledTest
|
|
public void testTrackRemovalAndAddition() throws Exception {
|
|
// Allow loopback interfaces too since our Android devices often don't
|
|
// have those.
|
|
PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
|
|
options.networkIgnoreMask = 0;
|
|
PeerConnectionFactory factory = PeerConnectionFactory.builder()
|
|
.setOptions(options)
|
|
.setVideoEncoderFactory(new SoftwareVideoEncoderFactory())
|
|
.setVideoDecoderFactory(new SoftwareVideoDecoderFactory())
|
|
.createPeerConnectionFactory();
|
|
|
|
List<PeerConnection.IceServer> iceServers = new ArrayList<>();
|
|
iceServers.add(
|
|
PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer());
|
|
|
|
PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(iceServers);
|
|
rtcConfig.enableDtlsSrtp = true;
|
|
|
|
ObserverExpectations offeringExpectations = new ObserverExpectations("PCTest:offerer");
|
|
PeerConnection offeringPC = factory.createPeerConnection(rtcConfig, offeringExpectations);
|
|
assertNotNull(offeringPC);
|
|
|
|
ObserverExpectations answeringExpectations = new ObserverExpectations("PCTest:answerer");
|
|
PeerConnection answeringPC = factory.createPeerConnection(rtcConfig, answeringExpectations);
|
|
assertNotNull(answeringPC);
|
|
|
|
// We want to use the same camera for offerer & answerer, so create it here
|
|
// instead of in addTracksToPC.
|
|
final CameraEnumerator enumerator = new Camera1Enumerator(false /* captureToTexture */);
|
|
final VideoCapturer videoCapturer =
|
|
enumerator.createCapturer(enumerator.getDeviceNames()[0], null /* eventsHandler */);
|
|
final SurfaceTextureHelper surfaceTextureHelper =
|
|
SurfaceTextureHelper.create("SurfaceTextureHelper", /* sharedContext= */ null);
|
|
final VideoSource videoSource = factory.createVideoSource(/* isScreencast= */ false);
|
|
videoCapturer.initialize(surfaceTextureHelper, InstrumentationRegistry.getTargetContext(),
|
|
videoSource.getCapturerObserver());
|
|
videoCapturer.startCapture(640, 480, 30);
|
|
|
|
// Add offerer media stream.
|
|
offeringExpectations.expectRenegotiationNeeded();
|
|
WeakReference<MediaStream> oLMS =
|
|
addTracksToPC(factory, offeringPC, videoSource, "offeredMediaStream", "offeredVideoTrack",
|
|
"offeredAudioTrack", new ExpectedResolutionSetter(answeringExpectations));
|
|
|
|
offeringExpectations.expectAddTrack(2);
|
|
answeringExpectations.expectAddTrack(2);
|
|
// Create offer.
|
|
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());
|
|
|
|
// Set local description for offerer.
|
|
sdpLatch = new SdpObserverLatch();
|
|
offeringExpectations.expectSignalingChange(SignalingState.HAVE_LOCAL_OFFER);
|
|
offeringExpectations.expectIceCandidates(2);
|
|
offeringExpectations.expectIceGatheringChange(IceGatheringState.COMPLETE);
|
|
offeringExpectations.expectConnectionChange(PeerConnectionState.CONNECTING);
|
|
offeringPC.setLocalDescription(sdpLatch, offerSdp);
|
|
assertTrue(sdpLatch.await());
|
|
assertNull(sdpLatch.getSdp());
|
|
|
|
// Set remote description for answerer.
|
|
sdpLatch = new SdpObserverLatch();
|
|
answeringExpectations.expectSignalingChange(SignalingState.HAVE_REMOTE_OFFER);
|
|
answeringExpectations.expectAddStream("offeredMediaStream");
|
|
answeringPC.setRemoteDescription(sdpLatch, offerSdp);
|
|
assertTrue(sdpLatch.await());
|
|
assertNull(sdpLatch.getSdp());
|
|
|
|
// Add answerer media stream.
|
|
answeringExpectations.expectRenegotiationNeeded();
|
|
WeakReference<MediaStream> aLMS = addTracksToPC(factory, answeringPC, videoSource,
|
|
"answeredMediaStream", "answeredVideoTrack", "answeredAudioTrack",
|
|
new ExpectedResolutionSetter(offeringExpectations));
|
|
|
|
// Create answer.
|
|
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());
|
|
|
|
// Set local description for answerer.
|
|
sdpLatch = new SdpObserverLatch();
|
|
answeringExpectations.expectSignalingChange(SignalingState.STABLE);
|
|
answeringExpectations.expectIceCandidates(2);
|
|
answeringExpectations.expectIceGatheringChange(IceGatheringState.COMPLETE);
|
|
answeringExpectations.expectConnectionChange(PeerConnectionState.CONNECTING);
|
|
answeringPC.setLocalDescription(sdpLatch, answerSdp);
|
|
assertTrue(sdpLatch.await());
|
|
assertNull(sdpLatch.getSdp());
|
|
|
|
// Set remote description for offerer.
|
|
sdpLatch = new SdpObserverLatch();
|
|
offeringExpectations.expectSignalingChange(SignalingState.STABLE);
|
|
offeringExpectations.expectAddStream("answeredMediaStream");
|
|
|
|
offeringExpectations.expectIceConnectionChange(IceConnectionState.CHECKING);
|
|
offeringExpectations.expectIceConnectionChange(IceConnectionState.CONNECTED);
|
|
offeringExpectations.expectConnectionChange(PeerConnectionState.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);
|
|
answeringExpectations.expectConnectionChange(PeerConnectionState.CONNECTED);
|
|
|
|
offeringPC.setRemoteDescription(sdpLatch, answerSdp);
|
|
assertTrue(sdpLatch.await());
|
|
assertNull(sdpLatch.getSdp());
|
|
|
|
// Wait for at least one ice candidate from the offering PC and forward them to the answering
|
|
// PC.
|
|
for (IceCandidate candidate : offeringExpectations.getAtLeastOneIceCandidate()) {
|
|
answeringPC.addIceCandidate(candidate);
|
|
}
|
|
|
|
// Wait for at least one ice candidate from the answering PC and forward them to the offering
|
|
// PC.
|
|
for (IceCandidate candidate : answeringExpectations.getAtLeastOneIceCandidate()) {
|
|
offeringPC.addIceCandidate(candidate);
|
|
}
|
|
|
|
// Wait for one frame of the correct size to be delivered.
|
|
// Otherwise we could get a dummy black frame of unexpcted size when the
|
|
// video track is removed.
|
|
offeringExpectations.expectFramesDelivered(1);
|
|
answeringExpectations.expectFramesDelivered(1);
|
|
|
|
assertTrue(offeringExpectations.waitForAllExpectationsToBeSatisfied(TIMEOUT_SECONDS));
|
|
assertTrue(answeringExpectations.waitForAllExpectationsToBeSatisfied(TIMEOUT_SECONDS));
|
|
|
|
assertEquals(PeerConnection.SignalingState.STABLE, offeringPC.signalingState());
|
|
assertEquals(PeerConnection.SignalingState.STABLE, answeringPC.signalingState());
|
|
|
|
// Now do another negotiation, removing the video track from one peer.
|
|
// This previously caused a crash on pc.dispose().
|
|
// See: https://bugs.chromium.org/p/webrtc/issues/detail?id=5128
|
|
VideoTrack offererVideoTrack = oLMS.get().videoTracks.get(0);
|
|
// Note that when we call removeTrack, we regain responsibility for
|
|
// disposing of the track.
|
|
offeringExpectations.expectRenegotiationNeeded();
|
|
oLMS.get().removeTrack(offererVideoTrack);
|
|
negotiate(offeringPC, offeringExpectations, answeringPC, answeringExpectations);
|
|
|
|
// Make sure the track was really removed.
|
|
MediaStream aRMS = answeringExpectations.gotRemoteStreams.iterator().next();
|
|
assertTrue(aRMS.videoTracks.isEmpty());
|
|
|
|
// Add the video track to test if the answeringPC will create a new track
|
|
// for the updated remote description.
|
|
offeringExpectations.expectRenegotiationNeeded();
|
|
oLMS.get().addTrack(offererVideoTrack);
|
|
// The answeringPC sets the updated remote description with a track added.
|
|
// So the onAddTrack callback is expected to be called once.
|
|
answeringExpectations.expectAddTrack(1);
|
|
offeringExpectations.expectAddTrack(0);
|
|
negotiate(offeringPC, offeringExpectations, answeringPC, answeringExpectations);
|
|
|
|
// Finally, remove both the audio and video tracks, which should completely
|
|
// remove the remote stream. This used to trigger an assert.
|
|
// See: https://bugs.chromium.org/p/webrtc/issues/detail?id=5128
|
|
offeringExpectations.expectRenegotiationNeeded();
|
|
oLMS.get().removeTrack(offererVideoTrack);
|
|
AudioTrack offererAudioTrack = oLMS.get().audioTracks.get(0);
|
|
offeringExpectations.expectRenegotiationNeeded();
|
|
oLMS.get().removeTrack(offererAudioTrack);
|
|
|
|
answeringExpectations.expectRemoveStream("offeredMediaStream");
|
|
negotiate(offeringPC, offeringExpectations, answeringPC, answeringExpectations);
|
|
|
|
// Make sure the stream was really removed.
|
|
assertTrue(answeringExpectations.gotRemoteStreams.isEmpty());
|
|
|
|
// Free the Java-land objects and collect them.
|
|
shutdownPC(offeringPC, offeringExpectations);
|
|
offeringPC = null;
|
|
shutdownPC(answeringPC, answeringExpectations);
|
|
answeringPC = null;
|
|
offererVideoTrack.dispose();
|
|
offererAudioTrack.dispose();
|
|
videoCapturer.stopCapture();
|
|
videoCapturer.dispose();
|
|
videoSource.dispose();
|
|
surfaceTextureHelper.dispose();
|
|
factory.dispose();
|
|
System.gc();
|
|
}
|
|
|
|
/**
|
|
* Test that a Java MediaStream is updated when the native stream is.
|
|
* <p>
|
|
* Specifically, test that when remote tracks are indicated as being added or
|
|
* removed from a MediaStream (via "a=ssrc" or "a=msid" in a remote
|
|
* description), the existing remote MediaStream object is updated.
|
|
* <p>
|
|
* This test starts with just an audio track, adds a video track, then
|
|
* removes it. It only applies remote offers, which is sufficient to test
|
|
* this functionality and simplifies the test. This means that no media will
|
|
* actually be sent/received; we're just testing that the Java MediaStream
|
|
* object gets updated when the native object changes.
|
|
*/
|
|
@Test
|
|
@MediumTest
|
|
public void testRemoteStreamUpdatedWhenTracksAddedOrRemoved() throws Exception {
|
|
PeerConnectionFactory factory = PeerConnectionFactory.builder()
|
|
.setVideoEncoderFactory(new SoftwareVideoEncoderFactory())
|
|
.setVideoDecoderFactory(new SoftwareVideoDecoderFactory())
|
|
.createPeerConnectionFactory();
|
|
|
|
// This test is fine with no ICE servers.
|
|
List<PeerConnection.IceServer> iceServers = new ArrayList<>();
|
|
|
|
// Use OfferToReceiveAudio/Video to ensure every offer has an audio and
|
|
// video m= section. Simplifies the test because it means we don't have to
|
|
// actually apply the offer to "offeringPC"; it's just used as an SDP
|
|
// factory.
|
|
MediaConstraints offerConstraints = new MediaConstraints();
|
|
offerConstraints.mandatory.add(
|
|
new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"));
|
|
offerConstraints.mandatory.add(
|
|
new MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true"));
|
|
|
|
// This PeerConnection will only be used to generate offers.
|
|
ObserverExpectations offeringExpectations = new ObserverExpectations("offerer");
|
|
PeerConnection offeringPC = factory.createPeerConnection(iceServers, offeringExpectations);
|
|
assertNotNull(offeringPC);
|
|
|
|
ObserverExpectations expectations = new ObserverExpectations("PC under test");
|
|
PeerConnection pcUnderTest = factory.createPeerConnection(iceServers, expectations);
|
|
assertNotNull(pcUnderTest);
|
|
|
|
// Add offerer media stream with just an audio track.
|
|
MediaStream localStream = factory.createLocalMediaStream("stream");
|
|
AudioTrack localAudioTrack =
|
|
factory.createAudioTrack("audio", factory.createAudioSource(new MediaConstraints()));
|
|
localStream.addTrack(localAudioTrack);
|
|
// TODO(deadbeef): Use addTrack once that's available.
|
|
offeringExpectations.expectRenegotiationNeeded();
|
|
offeringPC.addStream(localStream);
|
|
// Create offer.
|
|
SdpObserverLatch sdpLatch = new SdpObserverLatch();
|
|
offeringPC.createOffer(sdpLatch, offerConstraints);
|
|
assertTrue(sdpLatch.await());
|
|
SessionDescription offerSdp = sdpLatch.getSdp();
|
|
|
|
// Apply remote offer to PC under test.
|
|
sdpLatch = new SdpObserverLatch();
|
|
expectations.expectSignalingChange(SignalingState.HAVE_REMOTE_OFFER);
|
|
expectations.expectAddStream("stream");
|
|
pcUnderTest.setRemoteDescription(sdpLatch, offerSdp);
|
|
assertTrue(sdpLatch.await());
|
|
// Sanity check that we get one remote stream with one audio track.
|
|
MediaStream remoteStream = expectations.gotRemoteStreams.iterator().next();
|
|
assertEquals(remoteStream.audioTracks.size(), 1);
|
|
assertEquals(remoteStream.videoTracks.size(), 0);
|
|
|
|
// Add a video track...
|
|
final CameraEnumerator enumerator = new Camera1Enumerator(false /* captureToTexture */);
|
|
final VideoCapturer videoCapturer =
|
|
enumerator.createCapturer(enumerator.getDeviceNames()[0], null /* eventsHandler */);
|
|
final SurfaceTextureHelper surfaceTextureHelper =
|
|
SurfaceTextureHelper.create("SurfaceTextureHelper", /* sharedContext= */ null);
|
|
final VideoSource videoSource = factory.createVideoSource(/* isScreencast= */ false);
|
|
videoCapturer.initialize(surfaceTextureHelper, InstrumentationRegistry.getTargetContext(),
|
|
videoSource.getCapturerObserver());
|
|
VideoTrack videoTrack = factory.createVideoTrack("video", videoSource);
|
|
offeringExpectations.expectRenegotiationNeeded();
|
|
localStream.addTrack(videoTrack);
|
|
// ... and create an updated offer.
|
|
sdpLatch = new SdpObserverLatch();
|
|
offeringPC.createOffer(sdpLatch, offerConstraints);
|
|
assertTrue(sdpLatch.await());
|
|
offerSdp = sdpLatch.getSdp();
|
|
|
|
// Apply remote offer with new video track to PC under test.
|
|
sdpLatch = new SdpObserverLatch();
|
|
pcUnderTest.setRemoteDescription(sdpLatch, offerSdp);
|
|
assertTrue(sdpLatch.await());
|
|
// The remote stream should now have a video track.
|
|
assertEquals(remoteStream.audioTracks.size(), 1);
|
|
assertEquals(remoteStream.videoTracks.size(), 1);
|
|
|
|
// Finally, create another offer with the audio track removed.
|
|
offeringExpectations.expectRenegotiationNeeded();
|
|
localStream.removeTrack(localAudioTrack);
|
|
localAudioTrack.dispose();
|
|
sdpLatch = new SdpObserverLatch();
|
|
offeringPC.createOffer(sdpLatch, offerConstraints);
|
|
assertTrue(sdpLatch.await());
|
|
offerSdp = sdpLatch.getSdp();
|
|
|
|
// Apply remote offer with just a video track to PC under test.
|
|
sdpLatch = new SdpObserverLatch();
|
|
pcUnderTest.setRemoteDescription(sdpLatch, offerSdp);
|
|
assertTrue(sdpLatch.await());
|
|
// The remote stream should no longer have an audio track.
|
|
assertEquals(remoteStream.audioTracks.size(), 0);
|
|
assertEquals(remoteStream.videoTracks.size(), 1);
|
|
|
|
// Free the Java-land objects. Video capturer and source aren't owned by
|
|
// the PeerConnection and need to be disposed separately.
|
|
// TODO(deadbeef): Should all these events really occur on disposal?
|
|
// "Gathering complete" is especially odd since gathering never started.
|
|
// Note that this test isn't meant to test these events, but we must do
|
|
// this or otherwise it will crash.
|
|
offeringExpectations.expectIceConnectionChange(IceConnectionState.CLOSED);
|
|
offeringExpectations.expectSignalingChange(SignalingState.CLOSED);
|
|
offeringExpectations.expectIceGatheringChange(IceGatheringState.COMPLETE);
|
|
offeringPC.dispose();
|
|
expectations.expectIceConnectionChange(IceConnectionState.CLOSED);
|
|
expectations.expectSignalingChange(SignalingState.CLOSED);
|
|
expectations.expectIceGatheringChange(IceGatheringState.COMPLETE);
|
|
pcUnderTest.dispose();
|
|
videoCapturer.dispose();
|
|
videoSource.dispose();
|
|
surfaceTextureHelper.dispose();
|
|
factory.dispose();
|
|
}
|
|
|
|
@Test
|
|
@MediumTest
|
|
public void testAddingNullVideoSink() {
|
|
final PeerConnectionFactory factory =
|
|
PeerConnectionFactory.builder().createPeerConnectionFactory();
|
|
final VideoSource videoSource = factory.createVideoSource(/* isScreencast= */ false);
|
|
final VideoTrack videoTrack = factory.createVideoTrack("video", videoSource);
|
|
try {
|
|
videoTrack.addSink(/* sink= */ null);
|
|
fail("Should have thrown an IllegalArgumentException.");
|
|
} catch (IllegalArgumentException e) {
|
|
// Expected path.
|
|
}
|
|
}
|
|
|
|
@Test
|
|
@MediumTest
|
|
public void testRemovingNullVideoSink() {
|
|
final PeerConnectionFactory factory =
|
|
PeerConnectionFactory.builder().createPeerConnectionFactory();
|
|
final VideoSource videoSource = factory.createVideoSource(/* isScreencast= */ false);
|
|
final VideoTrack videoTrack = factory.createVideoTrack("video", videoSource);
|
|
videoTrack.removeSink(/* sink= */ null);
|
|
}
|
|
|
|
@Test
|
|
@MediumTest
|
|
public void testRemovingNonExistantVideoSink() {
|
|
final PeerConnectionFactory factory =
|
|
PeerConnectionFactory.builder().createPeerConnectionFactory();
|
|
final VideoSource videoSource = factory.createVideoSource(/* isScreencast= */ false);
|
|
final VideoTrack videoTrack = factory.createVideoTrack("video", videoSource);
|
|
final VideoSink videoSink = new VideoSink() {
|
|
@Override
|
|
public void onFrame(VideoFrame frame) {}
|
|
};
|
|
videoTrack.removeSink(videoSink);
|
|
}
|
|
|
|
@Test
|
|
@MediumTest
|
|
public void testAddingSameVideoSinkMultipleTimes() {
|
|
final PeerConnectionFactory factory =
|
|
PeerConnectionFactory.builder().createPeerConnectionFactory();
|
|
final VideoSource videoSource = factory.createVideoSource(/* isScreencast= */ false);
|
|
final VideoTrack videoTrack = factory.createVideoTrack("video", videoSource);
|
|
|
|
class FrameCounter implements VideoSink {
|
|
private int count;
|
|
|
|
public int getCount() {
|
|
return count;
|
|
}
|
|
|
|
@Override
|
|
public void onFrame(VideoFrame frame) {
|
|
count += 1;
|
|
}
|
|
}
|
|
final FrameCounter frameCounter = new FrameCounter();
|
|
|
|
final VideoFrame videoFrame = new VideoFrame(
|
|
JavaI420Buffer.allocate(/* width= */ 32, /* height= */ 32), /* rotation= */ 0,
|
|
/* timestampNs= */ 0);
|
|
|
|
videoTrack.addSink(frameCounter);
|
|
videoTrack.addSink(frameCounter);
|
|
videoSource.getCapturerObserver().onFrameCaptured(videoFrame);
|
|
|
|
// Even though we called addSink() multiple times, we should only get one frame out.
|
|
assertEquals(1, frameCounter.count);
|
|
}
|
|
|
|
@Test
|
|
@MediumTest
|
|
public void testAddingAndRemovingVideoSink() {
|
|
final PeerConnectionFactory factory =
|
|
PeerConnectionFactory.builder().createPeerConnectionFactory();
|
|
final VideoSource videoSource = factory.createVideoSource(/* isScreencast= */ false);
|
|
final VideoTrack videoTrack = factory.createVideoTrack("video", videoSource);
|
|
final VideoFrame videoFrame = new VideoFrame(
|
|
JavaI420Buffer.allocate(/* width= */ 32, /* height= */ 32), /* rotation= */ 0,
|
|
/* timestampNs= */ 0);
|
|
|
|
final VideoSink failSink = new VideoSink() {
|
|
@Override
|
|
public void onFrame(VideoFrame frame) {
|
|
fail("onFrame() should not be called on removed sink");
|
|
}
|
|
};
|
|
videoTrack.addSink(failSink);
|
|
videoTrack.removeSink(failSink);
|
|
videoSource.getCapturerObserver().onFrameCaptured(videoFrame);
|
|
}
|
|
|
|
private static void negotiate(PeerConnection offeringPC,
|
|
ObserverExpectations offeringExpectations, PeerConnection answeringPC,
|
|
ObserverExpectations answeringExpectations) {
|
|
// Create offer.
|
|
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());
|
|
|
|
// Set local description for offerer.
|
|
sdpLatch = new SdpObserverLatch();
|
|
offeringExpectations.expectSignalingChange(SignalingState.HAVE_LOCAL_OFFER);
|
|
offeringPC.setLocalDescription(sdpLatch, offerSdp);
|
|
assertTrue(sdpLatch.await());
|
|
assertNull(sdpLatch.getSdp());
|
|
|
|
// Set remote description for answerer.
|
|
sdpLatch = new SdpObserverLatch();
|
|
answeringExpectations.expectSignalingChange(SignalingState.HAVE_REMOTE_OFFER);
|
|
answeringPC.setRemoteDescription(sdpLatch, offerSdp);
|
|
assertTrue(sdpLatch.await());
|
|
assertNull(sdpLatch.getSdp());
|
|
|
|
// Create answer.
|
|
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());
|
|
|
|
// Set local description for answerer.
|
|
sdpLatch = new SdpObserverLatch();
|
|
answeringExpectations.expectSignalingChange(SignalingState.STABLE);
|
|
answeringPC.setLocalDescription(sdpLatch, answerSdp);
|
|
assertTrue(sdpLatch.await());
|
|
assertNull(sdpLatch.getSdp());
|
|
|
|
// Set remote description for offerer.
|
|
sdpLatch = new SdpObserverLatch();
|
|
offeringExpectations.expectSignalingChange(SignalingState.STABLE);
|
|
offeringPC.setRemoteDescription(sdpLatch, answerSdp);
|
|
assertTrue(sdpLatch.await());
|
|
assertNull(sdpLatch.getSdp());
|
|
}
|
|
|
|
private static void getMetrics() {
|
|
Metrics metrics = Metrics.getAndReset();
|
|
assertTrue(metrics.map.size() > 0);
|
|
// Test for example that the lifetime of a Call is recorded.
|
|
String name = "WebRTC.Call.LifetimeInSeconds";
|
|
assertTrue(metrics.map.containsKey(name));
|
|
HistogramInfo info = metrics.map.get(name);
|
|
assertTrue(info.samples.size() > 0);
|
|
}
|
|
|
|
@SuppressWarnings("deprecation") // TODO(sakal): getStats is deprecated
|
|
private static void shutdownPC(PeerConnection pc, ObserverExpectations expectations) {
|
|
if (expectations.dataChannel != null) {
|
|
expectations.dataChannel.unregisterObserver();
|
|
expectations.dataChannel.dispose();
|
|
}
|
|
|
|
// Call getStats (old implementation) before shutting down PC.
|
|
expectations.expectOldStatsCallback();
|
|
assertTrue(pc.getStats(expectations, null /* track */));
|
|
assertTrue(expectations.waitForAllExpectationsToBeSatisfied(TIMEOUT_SECONDS));
|
|
|
|
// Call the new getStats implementation as well.
|
|
expectations.expectNewStatsCallback();
|
|
pc.getStats(expectations);
|
|
assertTrue(expectations.waitForAllExpectationsToBeSatisfied(TIMEOUT_SECONDS));
|
|
|
|
expectations.expectIceConnectionChange(IceConnectionState.CLOSED);
|
|
expectations.expectConnectionChange(PeerConnectionState.CLOSED);
|
|
expectations.expectSignalingChange(SignalingState.CLOSED);
|
|
pc.close();
|
|
assertTrue(expectations.waitForAllExpectationsToBeSatisfied(TIMEOUT_SECONDS));
|
|
|
|
// Call getStats (old implementation) after calling close(). Should still
|
|
// work.
|
|
expectations.expectOldStatsCallback();
|
|
assertTrue(pc.getStats(expectations, null /* track */));
|
|
assertTrue(expectations.waitForAllExpectationsToBeSatisfied(TIMEOUT_SECONDS));
|
|
|
|
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<String> allThreads() {
|
|
TreeSet<String> threads = new TreeSet<String>();
|
|
// 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;
|
|
}
|
|
}
|