mirror of
https://github.com/servo/servo.git
synced 2025-06-25 17:44:33 +01:00
2297 lines
72 KiB
HTML
2297 lines
72 KiB
HTML
<!doctype html>
|
|
<meta charset=utf-8>
|
|
<meta name="timeout" content="long">
|
|
<title>RTCRtpTransceiver</title>
|
|
<script src="/resources/testharness.js"></script>
|
|
<script src="/resources/testharnessreport.js"></script>
|
|
<script src="RTCPeerConnection-helper.js"></script>
|
|
<script>
|
|
'use strict';
|
|
|
|
const checkThrows = async (func, exceptionName, description) => {
|
|
try {
|
|
await func();
|
|
assert_true(false, description + " throws " + exceptionName);
|
|
} catch (e) {
|
|
assert_equals(e.name, exceptionName, description + " throws " + exceptionName);
|
|
}
|
|
};
|
|
|
|
const stopTracks = (...streams) => {
|
|
streams.forEach(stream => stream.getTracks().forEach(track => track.stop()));
|
|
};
|
|
|
|
const collectEvents = (target, name, check) => {
|
|
const events = [];
|
|
const handler = e => {
|
|
check(e);
|
|
events.push(e);
|
|
};
|
|
|
|
target.addEventListener(name, handler);
|
|
|
|
const finishCollecting = () => {
|
|
target.removeEventListener(name, handler);
|
|
return events;
|
|
};
|
|
|
|
return {finish: finishCollecting};
|
|
};
|
|
|
|
const collectAddTrackEvents = stream => {
|
|
const checkEvent = e => {
|
|
assert_true(e.track instanceof MediaStreamTrack, "Track is set on event");
|
|
assert_true(stream.getTracks().includes(e.track),
|
|
"track in addtrack event is in the stream");
|
|
};
|
|
return collectEvents(stream, "addtrack", checkEvent);
|
|
};
|
|
|
|
const collectRemoveTrackEvents = stream => {
|
|
const checkEvent = e => {
|
|
assert_true(e.track instanceof MediaStreamTrack, "Track is set on event");
|
|
assert_true(!stream.getTracks().includes(e.track),
|
|
"track in removetrack event is not in the stream");
|
|
};
|
|
return collectEvents(stream, "removetrack", checkEvent);
|
|
};
|
|
|
|
const collectTrackEvents = pc => {
|
|
const checkEvent = e => {
|
|
assert_true(e.track instanceof MediaStreamTrack, "Track is set on event");
|
|
assert_true(e.receiver instanceof RTCRtpReceiver, "Receiver is set on event");
|
|
assert_true(e.transceiver instanceof RTCRtpTransceiver, "Transceiver is set on event");
|
|
assert_true(Array.isArray(e.streams), "Streams is set on event");
|
|
e.streams.forEach(stream => {
|
|
assert_true(stream.getTracks().includes(e.track),
|
|
"Each stream in event contains the track");
|
|
});
|
|
assert_equals(e.receiver, e.transceiver.receiver,
|
|
"Receiver belongs to transceiver");
|
|
assert_equals(e.track, e.receiver.track,
|
|
"Track belongs to receiver");
|
|
};
|
|
|
|
return collectEvents(pc, "track", checkEvent);
|
|
};
|
|
|
|
const setRemoteDescriptionReturnTrackEvents = async (pc, desc) => {
|
|
const trackEventCollector = collectTrackEvents(pc);
|
|
await pc.setRemoteDescription(desc);
|
|
return trackEventCollector.finish();
|
|
};
|
|
|
|
const offerAnswer = async (offerer, answerer) => {
|
|
const offer = await offerer.createOffer();
|
|
await answerer.setRemoteDescription(offer);
|
|
await offerer.setLocalDescription(offer);
|
|
const answer = await answerer.createAnswer();
|
|
await offerer.setRemoteDescription(answer);
|
|
await answerer.setLocalDescription(answer);
|
|
};
|
|
|
|
const trickle = (t, pc1, pc2) => {
|
|
pc1.onicecandidate = t.step_func(async e => {
|
|
try {
|
|
await pc2.addIceCandidate(e.candidate);
|
|
} catch (e) {
|
|
assert_true(false, "addIceCandidate threw error: " + e.name);
|
|
}
|
|
});
|
|
};
|
|
|
|
const iceConnected = pc => {
|
|
return new Promise((resolve, reject) => {
|
|
const iceCheck = () => {
|
|
if (pc.iceConnectionState == "connected") {
|
|
assert_true(true, "ICE connected");
|
|
resolve();
|
|
}
|
|
|
|
if (pc.iceConnectionState == "failed") {
|
|
assert_true(false, "ICE failed");
|
|
reject();
|
|
}
|
|
};
|
|
|
|
iceCheck();
|
|
pc.oniceconnectionstatechange = iceCheck;
|
|
});
|
|
};
|
|
|
|
const negotiationNeeded = pc => {
|
|
return new Promise(resolve => pc.onnegotiationneeded = resolve);
|
|
};
|
|
|
|
const countEvents = (target, name) => {
|
|
const result = {count: 0};
|
|
target.addEventListener(name, e => result.count++);
|
|
return result;
|
|
};
|
|
|
|
const gotMuteEvent = async track => {
|
|
await new Promise(r => track.addEventListener("mute", r, {once: true}));
|
|
|
|
assert_true(track.muted, "track should be muted after onmute");
|
|
};
|
|
|
|
const gotUnmuteEvent = async track => {
|
|
await new Promise(r => track.addEventListener("unmute", r, {once: true}));
|
|
|
|
assert_true(!track.muted, "track should not be muted after onunmute");
|
|
};
|
|
|
|
// comparable() - produces copy of object that is JSON comparable.
|
|
// o = original object (required)
|
|
// t = template of what to examine. Useful if o is non-enumerable (optional)
|
|
|
|
const comparable = (o, t = o) => {
|
|
if (typeof o != 'object' || !o) {
|
|
return o;
|
|
}
|
|
if (Array.isArray(t) && Array.isArray(o)) {
|
|
return o.map((n, i) => comparable(n, t[i]));
|
|
}
|
|
return Object.keys(t).sort()
|
|
.reduce((r, key) => (r[key] = comparable(o[key], t[key]), r), {});
|
|
};
|
|
|
|
const stripKeyQuotes = s => s.replace(/"(\w+)":/g, "$1:");
|
|
|
|
const hasProps = (observed, expected) => {
|
|
const observable = comparable(observed, expected);
|
|
assert_equals(stripKeyQuotes(JSON.stringify(observable)),
|
|
stripKeyQuotes(JSON.stringify(comparable(expected))));
|
|
};
|
|
|
|
const hasPropsAndUniqueMids = (observed, expected) => {
|
|
hasProps(observed, expected);
|
|
|
|
const mids = [];
|
|
observed.forEach((transceiver, i) => {
|
|
if (!("mid" in expected[i])) {
|
|
assert_not_equals(transceiver.mid, null);
|
|
assert_equals(typeof transceiver.mid, "string");
|
|
}
|
|
if (transceiver.mid) {
|
|
assert_false(mids.includes(transceiver.mid), "mid must be unique");
|
|
mids.push(transceiver.mid);
|
|
}
|
|
});
|
|
};
|
|
|
|
const checkAddTransceiverNoTrack = async t => {
|
|
const pc = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc.close());
|
|
|
|
hasProps(pc.getTransceivers(), []);
|
|
|
|
pc.addTransceiver("audio");
|
|
pc.addTransceiver("video");
|
|
|
|
hasProps(pc.getTransceivers(),
|
|
[
|
|
{
|
|
receiver: {track: {kind: "audio", readyState: "live", muted: true}},
|
|
sender: {track: null},
|
|
direction: "sendrecv",
|
|
mid: null,
|
|
currentDirection: null,
|
|
},
|
|
{
|
|
receiver: {track: {kind: "video", readyState: "live", muted: true}},
|
|
sender: {track: null},
|
|
direction: "sendrecv",
|
|
mid: null,
|
|
currentDirection: null,
|
|
}
|
|
]);
|
|
};
|
|
|
|
const checkAddTransceiverWithTrack = async t => {
|
|
const pc = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc.close());
|
|
|
|
const stream = await getNoiseStream({audio: true, video: true});
|
|
t.add_cleanup(() => stopTracks(stream));
|
|
const audio = stream.getAudioTracks()[0];
|
|
const video = stream.getVideoTracks()[0];
|
|
|
|
pc.addTransceiver(audio);
|
|
pc.addTransceiver(video);
|
|
|
|
hasProps(pc.getTransceivers(),
|
|
[
|
|
{
|
|
receiver: {track: {kind: "audio"}},
|
|
sender: {track: audio},
|
|
direction: "sendrecv",
|
|
mid: null,
|
|
currentDirection: null,
|
|
},
|
|
{
|
|
receiver: {track: {kind: "video"}},
|
|
sender: {track: video},
|
|
direction: "sendrecv",
|
|
mid: null,
|
|
currentDirection: null,
|
|
}
|
|
]);
|
|
};
|
|
|
|
const checkAddTransceiverWithAddTrack = async t => {
|
|
const pc = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc.close());
|
|
|
|
const stream = await getNoiseStream({audio: true, video: true});
|
|
t.add_cleanup(() => stopTracks(stream));
|
|
const audio = stream.getAudioTracks()[0];
|
|
const video = stream.getVideoTracks()[0];
|
|
|
|
pc.addTrack(audio, stream);
|
|
pc.addTrack(video, stream);
|
|
|
|
hasProps(pc.getTransceivers(),
|
|
[
|
|
{
|
|
receiver: {track: {kind: "audio"}},
|
|
sender: {track: audio},
|
|
direction: "sendrecv",
|
|
mid: null,
|
|
currentDirection: null,
|
|
},
|
|
{
|
|
receiver: {track: {kind: "video"}},
|
|
sender: {track: video},
|
|
direction: "sendrecv",
|
|
mid: null,
|
|
currentDirection: null,
|
|
}
|
|
]);
|
|
};
|
|
|
|
const checkAddTransceiverWithDirection = async t => {
|
|
const pc = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc.close());
|
|
|
|
pc.addTransceiver("audio", {direction: "recvonly"});
|
|
pc.addTransceiver("video", {direction: "recvonly"});
|
|
|
|
hasProps(pc.getTransceivers(),
|
|
[
|
|
{
|
|
receiver: {track: {kind: "audio"}},
|
|
sender: {track: null},
|
|
direction: "recvonly",
|
|
mid: null,
|
|
currentDirection: null,
|
|
},
|
|
{
|
|
receiver: {track: {kind: "video"}},
|
|
sender: {track: null},
|
|
direction: "recvonly",
|
|
mid: null,
|
|
currentDirection: null,
|
|
}
|
|
]);
|
|
};
|
|
|
|
const checkAddTransceiverWithSetRemoteOfferSending = async t => {
|
|
const pc1 = new RTCPeerConnection();
|
|
const pc2 = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc1.close());
|
|
t.add_cleanup(() => pc2.close());
|
|
|
|
const stream = await getNoiseStream({audio: true});
|
|
t.add_cleanup(() => stopTracks(stream));
|
|
const track = stream.getAudioTracks()[0];
|
|
pc1.addTransceiver(track, {streams: [stream]});
|
|
|
|
const offer = await pc1.createOffer();
|
|
|
|
const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
|
|
hasProps(trackEvents,
|
|
[
|
|
{
|
|
track: pc2.getTransceivers()[0].receiver.track,
|
|
streams: [{id: stream.id}]
|
|
}
|
|
]);
|
|
|
|
|
|
hasPropsAndUniqueMids(pc2.getTransceivers(),
|
|
[
|
|
{
|
|
receiver: {track: {kind: "audio"}},
|
|
sender: {track: null},
|
|
direction: "recvonly",
|
|
currentDirection: null,
|
|
}
|
|
]);
|
|
};
|
|
|
|
const checkAddTransceiverWithSetRemoteOfferNoSend = async t => {
|
|
const pc1 = new RTCPeerConnection();
|
|
const pc2 = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc1.close());
|
|
t.add_cleanup(() => pc2.close());
|
|
|
|
const stream = await getNoiseStream({audio: true});
|
|
t.add_cleanup(() => stopTracks(stream));
|
|
const track = stream.getAudioTracks()[0];
|
|
pc1.addTransceiver(track);
|
|
pc1.getTransceivers()[0].direction = "recvonly";
|
|
|
|
const offer = await pc1.createOffer();
|
|
const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
|
|
hasProps(trackEvents, []);
|
|
|
|
hasPropsAndUniqueMids(pc2.getTransceivers(),
|
|
[
|
|
{
|
|
receiver: {track: {kind: "audio"}},
|
|
sender: {track: null},
|
|
// rtcweb-jsep says this is recvonly, w3c-webrtc does not...
|
|
direction: "recvonly",
|
|
currentDirection: null,
|
|
}
|
|
]);
|
|
};
|
|
|
|
const checkAddTransceiverBadKind = async t => {
|
|
const pc = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc.close());
|
|
try {
|
|
pc.addTransceiver("foo");
|
|
assert_true(false, 'addTransceiver("foo") throws');
|
|
}
|
|
catch (e) {
|
|
if (e instanceof TypeError) {
|
|
assert_true(true, 'addTransceiver("foo") throws a TypeError');
|
|
} else {
|
|
assert_true(false, 'addTransceiver("foo") throws a TypeError');
|
|
}
|
|
}
|
|
|
|
hasProps(pc.getTransceivers(), []);
|
|
};
|
|
|
|
const checkNoMidOffer = async t => {
|
|
const pc1 = new RTCPeerConnection();
|
|
const pc2 = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc1.close());
|
|
t.add_cleanup(() => pc2.close());
|
|
|
|
const stream = await getNoiseStream({audio: true});
|
|
t.add_cleanup(() => stopTracks(stream));
|
|
const track = stream.getAudioTracks()[0];
|
|
pc1.addTrack(track, stream);
|
|
|
|
const offer = await pc1.createOffer();
|
|
await pc1.setLocalDescription(offer);
|
|
|
|
// Remove mid attr
|
|
offer.sdp = offer.sdp.replace("a=mid:", "a=unknownattr:");
|
|
offer.sdp = offer.sdp.replace("a=group:", "a=unknownattr:");
|
|
await pc2.setRemoteDescription(offer);
|
|
|
|
hasPropsAndUniqueMids(pc2.getTransceivers(),
|
|
[
|
|
{
|
|
receiver: {track: {kind: "audio"}},
|
|
sender: {track: null},
|
|
direction: "recvonly",
|
|
currentDirection: null,
|
|
}
|
|
]);
|
|
|
|
const answer = await pc2.createAnswer();
|
|
await pc2.setLocalDescription(answer);
|
|
await pc1.setRemoteDescription(answer);
|
|
};
|
|
|
|
const checkNoMidAnswer = async t => {
|
|
const pc1 = new RTCPeerConnection();
|
|
const pc2 = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc1.close());
|
|
t.add_cleanup(() => pc2.close());
|
|
|
|
const stream = await getNoiseStream({audio: true});
|
|
t.add_cleanup(() => stopTracks(stream));
|
|
const track = stream.getAudioTracks()[0];
|
|
pc1.addTrack(track, stream);
|
|
|
|
const offer = await pc1.createOffer();
|
|
await pc1.setLocalDescription(offer);
|
|
await pc2.setRemoteDescription(offer);
|
|
|
|
hasPropsAndUniqueMids(pc1.getTransceivers(),
|
|
[
|
|
{
|
|
receiver: {track: {kind: "audio"}},
|
|
sender: {track: {kind: "audio"}},
|
|
direction: "sendrecv",
|
|
currentDirection: null,
|
|
}
|
|
]);
|
|
|
|
const lastMid = pc1.getTransceivers()[0].mid;
|
|
|
|
let answer = await pc2.createAnswer();
|
|
// Remove mid attr
|
|
answer.sdp = answer.sdp.replace("a=mid:", "a=unknownattr:");
|
|
// Remove group attr also
|
|
answer.sdp = answer.sdp.replace("a=group:", "a=unknownattr:");
|
|
await pc1.setRemoteDescription(answer);
|
|
|
|
hasProps(pc1.getTransceivers(),
|
|
[
|
|
{
|
|
receiver: {track: {kind: "audio"}},
|
|
sender: {track: {kind: "audio"}},
|
|
direction: "sendrecv",
|
|
currentDirection: "sendonly",
|
|
mid: lastMid
|
|
}
|
|
]);
|
|
|
|
const reoffer = await pc1.createOffer();
|
|
await pc1.setLocalDescription(reoffer);
|
|
hasProps(pc1.getTransceivers(),
|
|
[
|
|
{
|
|
receiver: {track: {kind: "audio"}},
|
|
sender: {track: {kind: "audio"}},
|
|
direction: "sendrecv",
|
|
currentDirection: "sendonly",
|
|
mid: lastMid
|
|
}
|
|
]);
|
|
};
|
|
|
|
const checkAddTransceiverNoTrackDoesntPair = async t => {
|
|
const pc1 = new RTCPeerConnection();
|
|
const pc2 = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc1.close());
|
|
t.add_cleanup(() => pc2.close());
|
|
|
|
pc1.addTransceiver("audio");
|
|
pc2.addTransceiver("audio");
|
|
|
|
const offer = await pc1.createOffer();
|
|
const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
|
|
hasProps(trackEvents,
|
|
[
|
|
{
|
|
track: pc2.getTransceivers()[1].receiver.track,
|
|
streams: []
|
|
}
|
|
]);
|
|
|
|
hasPropsAndUniqueMids(pc2.getTransceivers(),
|
|
[
|
|
{mid: null}, // no addTrack magic, doesn't auto-pair
|
|
{} // Created by SRD
|
|
]);
|
|
};
|
|
|
|
const checkAddTransceiverWithTrackDoesntPair = async t => {
|
|
const pc1 = new RTCPeerConnection();
|
|
const pc2 = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc1.close());
|
|
t.add_cleanup(() => pc2.close());
|
|
pc1.addTransceiver("audio");
|
|
|
|
const stream = await getNoiseStream({audio: true});
|
|
t.add_cleanup(() => stopTracks(stream));
|
|
const track = stream.getAudioTracks()[0];
|
|
pc2.addTransceiver(track);
|
|
|
|
const offer = await pc1.createOffer();
|
|
const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
|
|
hasProps(trackEvents,
|
|
[
|
|
{
|
|
track: pc2.getTransceivers()[1].receiver.track,
|
|
streams: []
|
|
}
|
|
]);
|
|
|
|
hasPropsAndUniqueMids(pc2.getTransceivers(),
|
|
[
|
|
{mid: null, sender: {track}},
|
|
{sender: {track: null}} // Created by SRD
|
|
]);
|
|
};
|
|
|
|
const checkAddTransceiverThenReplaceTrackDoesntPair = async t => {
|
|
const pc1 = new RTCPeerConnection();
|
|
const pc2 = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc1.close());
|
|
t.add_cleanup(() => pc2.close());
|
|
pc1.addTransceiver("audio");
|
|
pc2.addTransceiver("audio");
|
|
|
|
const stream = await getNoiseStream({audio: true});
|
|
t.add_cleanup(() => stopTracks(stream));
|
|
const track = stream.getAudioTracks()[0];
|
|
await pc2.getTransceivers()[0].sender.replaceTrack(track);
|
|
|
|
const offer = await pc1.createOffer();
|
|
const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
|
|
hasProps(trackEvents,
|
|
[
|
|
{
|
|
track: pc2.getTransceivers()[1].receiver.track,
|
|
streams: []
|
|
}
|
|
]);
|
|
|
|
hasPropsAndUniqueMids(pc2.getTransceivers(),
|
|
[
|
|
{mid: null, sender: {track}},
|
|
{sender: {track: null}} // Created by SRD
|
|
]);
|
|
};
|
|
|
|
const checkAddTransceiverThenAddTrackPairs = async t => {
|
|
const pc1 = new RTCPeerConnection();
|
|
const pc2 = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc1.close());
|
|
t.add_cleanup(() => pc2.close());
|
|
pc1.addTransceiver("audio");
|
|
pc2.addTransceiver("audio");
|
|
|
|
const stream = await getNoiseStream({audio: true});
|
|
t.add_cleanup(() => stopTracks(stream));
|
|
const track = stream.getAudioTracks()[0];
|
|
pc2.addTrack(track, stream);
|
|
|
|
const offer = await pc1.createOffer();
|
|
const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
|
|
hasProps(trackEvents,
|
|
[
|
|
{
|
|
track: pc2.getTransceivers()[0].receiver.track,
|
|
streams: []
|
|
}
|
|
]);
|
|
|
|
// addTransceiver-transceivers cannot attach to a remote offers, so a second
|
|
// transceiver is created and associated whilst the first transceiver
|
|
// remains unassociated.
|
|
assert_equals(pc2.getTransceivers()[0].mid, null);
|
|
assert_not_equals(pc2.getTransceivers()[1].mid, null);
|
|
};
|
|
|
|
const checkAddTrackPairs = async t => {
|
|
const pc1 = new RTCPeerConnection();
|
|
const pc2 = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc1.close());
|
|
t.add_cleanup(() => pc2.close());
|
|
pc1.addTransceiver("audio");
|
|
|
|
const stream = await getNoiseStream({audio: true});
|
|
t.add_cleanup(() => stopTracks(stream));
|
|
const track = stream.getAudioTracks()[0];
|
|
pc2.addTrack(track, stream);
|
|
|
|
const offer = await pc1.createOffer();
|
|
const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
|
|
hasProps(trackEvents,
|
|
[
|
|
{
|
|
track: pc2.getTransceivers()[0].receiver.track,
|
|
streams: []
|
|
}
|
|
]);
|
|
|
|
hasPropsAndUniqueMids(pc2.getTransceivers(),
|
|
[
|
|
{sender: {track}}
|
|
]);
|
|
};
|
|
|
|
const checkReplaceTrackNullDoesntPreventPairing = async t => {
|
|
const pc1 = new RTCPeerConnection();
|
|
const pc2 = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc1.close());
|
|
t.add_cleanup(() => pc2.close());
|
|
pc1.addTransceiver("audio");
|
|
|
|
const stream = await getNoiseStream({audio: true});
|
|
t.add_cleanup(() => stopTracks(stream));
|
|
const track = stream.getAudioTracks()[0];
|
|
pc2.addTrack(track, stream);
|
|
await pc2.getTransceivers()[0].sender.replaceTrack(null);
|
|
|
|
const offer = await pc1.createOffer();
|
|
const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
|
|
hasProps(trackEvents,
|
|
[
|
|
{
|
|
track: pc2.getTransceivers()[0].receiver.track,
|
|
streams: []
|
|
}
|
|
]);
|
|
|
|
hasPropsAndUniqueMids(pc2.getTransceivers(),
|
|
[
|
|
{sender: {track: null}}
|
|
]);
|
|
};
|
|
|
|
const checkRemoveAndReadd = async t => {
|
|
const pc1 = new RTCPeerConnection();
|
|
const pc2 = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc1.close());
|
|
t.add_cleanup(() => pc2.close());
|
|
const stream = await getNoiseStream({audio: true});
|
|
t.add_cleanup(() => stopTracks(stream));
|
|
const track = stream.getAudioTracks()[0];
|
|
pc1.addTrack(track, stream);
|
|
|
|
await offerAnswer(pc1, pc2);
|
|
|
|
pc1.removeTrack(pc1.getSenders()[0]);
|
|
pc1.addTrack(track, stream);
|
|
|
|
hasProps(pc1.getTransceivers(),
|
|
[
|
|
{
|
|
sender: {track: null},
|
|
direction: "recvonly"
|
|
},
|
|
{
|
|
sender: {track},
|
|
direction: "sendrecv"
|
|
}
|
|
]);
|
|
|
|
// pc1 is offerer
|
|
await offerAnswer(pc1, pc2);
|
|
|
|
hasProps(pc2.getTransceivers(),
|
|
[
|
|
{currentDirection: "inactive"},
|
|
{currentDirection: "recvonly"}
|
|
]);
|
|
|
|
pc1.removeTrack(pc1.getSenders()[1]);
|
|
pc1.addTrack(track, stream);
|
|
|
|
hasProps(pc1.getTransceivers(),
|
|
[
|
|
{
|
|
sender: {track: null},
|
|
direction: "recvonly"
|
|
},
|
|
{
|
|
sender: {track: null},
|
|
direction: "recvonly"
|
|
},
|
|
{
|
|
sender: {track},
|
|
direction: "sendrecv"
|
|
}
|
|
]);
|
|
|
|
// pc1 is answerer. We need to create a new transceiver so pc1 will have
|
|
// something to attach the re-added track to
|
|
pc2.addTransceiver("audio");
|
|
|
|
await offerAnswer(pc2, pc1);
|
|
|
|
hasProps(pc2.getTransceivers(),
|
|
[
|
|
{currentDirection: "inactive"},
|
|
{currentDirection: "inactive"},
|
|
{currentDirection: "sendrecv"}
|
|
]);
|
|
};
|
|
|
|
const checkAddTrackExistingTransceiverThenRemove = async t => {
|
|
const pc = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc.close());
|
|
pc.addTransceiver("audio");
|
|
const stream = await getNoiseStream({audio: true});
|
|
const audio = stream.getAudioTracks()[0];
|
|
let sender = pc.addTrack(audio, stream);
|
|
pc.removeTrack(sender);
|
|
|
|
// Cause transceiver to be associated
|
|
await pc.setLocalDescription(await pc.createOffer());
|
|
|
|
// Make sure add/remove works still
|
|
sender = pc.addTrack(audio, stream);
|
|
pc.removeTrack(sender);
|
|
|
|
stopTracks(stream);
|
|
};
|
|
|
|
const checkRemoveTrackNegotiation = async t => {
|
|
const pc1 = new RTCPeerConnection();
|
|
const pc2 = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc1.close());
|
|
t.add_cleanup(() => pc2.close());
|
|
const stream = await getNoiseStream({audio: true, video: true});
|
|
t.add_cleanup(() => stopTracks(stream));
|
|
const audio = stream.getAudioTracks()[0];
|
|
pc1.addTrack(audio, stream);
|
|
const video = stream.getVideoTracks()[0];
|
|
pc1.addTrack(video, stream);
|
|
// We want both a sendrecv and sendonly transceiver to test that the
|
|
// appropriate direction changes happen.
|
|
pc1.getTransceivers()[1].direction = "sendonly";
|
|
|
|
let offer = await pc1.createOffer();
|
|
|
|
// Get a reference to the stream
|
|
let trackEventCollector = collectTrackEvents(pc2);
|
|
await pc2.setRemoteDescription(offer);
|
|
let pc2TrackEvents = trackEventCollector.finish();
|
|
hasProps(pc2TrackEvents,
|
|
[
|
|
{streams: [{id: stream.id}]},
|
|
{streams: [{id: stream.id}]}
|
|
]);
|
|
const receiveStream = pc2TrackEvents[0].streams[0];
|
|
|
|
// Verify that rollback causes onremovetrack to fire for the added tracks
|
|
let removetrackEventCollector = collectRemoveTrackEvents(receiveStream);
|
|
await pc2.setRemoteDescription({type: "rollback"});
|
|
let removedtracks = removetrackEventCollector.finish().map(e => e.track);
|
|
assert_equals(removedtracks.length, 2,
|
|
"Rollback should have removed two tracks");
|
|
assert_true(removedtracks.includes(pc2TrackEvents[0].track),
|
|
"First track should be removed");
|
|
assert_true(removedtracks.includes(pc2TrackEvents[1].track),
|
|
"Second track should be removed");
|
|
|
|
offer = await pc1.createOffer();
|
|
|
|
let addtrackEventCollector = collectAddTrackEvents(receiveStream);
|
|
trackEventCollector = collectTrackEvents(pc2);
|
|
await pc2.setRemoteDescription(offer);
|
|
pc2TrackEvents = trackEventCollector.finish();
|
|
let addedtracks = addtrackEventCollector.finish().map(e => e.track);
|
|
assert_equals(addedtracks.length, 2,
|
|
"pc2.setRemoteDescription(offer) should've added 2 tracks to receive stream");
|
|
assert_true(addedtracks.includes(pc2TrackEvents[0].track),
|
|
"First track should be added");
|
|
assert_true(addedtracks.includes(pc2TrackEvents[1].track),
|
|
"Second track should be added");
|
|
|
|
await pc1.setLocalDescription(offer);
|
|
let answer = await pc2.createAnswer();
|
|
await pc1.setRemoteDescription(answer);
|
|
await pc2.setLocalDescription(answer);
|
|
pc1.removeTrack(pc1.getSenders()[0]);
|
|
|
|
hasProps(pc1.getSenders(),
|
|
[
|
|
{track: null},
|
|
{track: video}
|
|
]);
|
|
|
|
hasProps(pc1.getTransceivers(),
|
|
[
|
|
{
|
|
sender: {track: null},
|
|
direction: "recvonly"
|
|
},
|
|
{
|
|
sender: {track: video},
|
|
direction: "sendonly"
|
|
}
|
|
]);
|
|
|
|
await negotiationNeeded(pc1);
|
|
|
|
pc1.removeTrack(pc1.getSenders()[1]);
|
|
|
|
hasProps(pc1.getSenders(),
|
|
[
|
|
{track: null},
|
|
{track: null}
|
|
]);
|
|
|
|
hasProps(pc1.getTransceivers(),
|
|
[
|
|
{
|
|
sender: {track: null},
|
|
direction: "recvonly"
|
|
},
|
|
{
|
|
sender: {track: null},
|
|
direction: "inactive"
|
|
}
|
|
]);
|
|
|
|
// pc1 as offerer
|
|
offer = await pc1.createOffer();
|
|
|
|
removetrackEventCollector = collectRemoveTrackEvents(receiveStream);
|
|
await pc2.setRemoteDescription(offer);
|
|
removedtracks = removetrackEventCollector.finish().map(e => e.track);
|
|
assert_equals(removedtracks.length, 2, "Should have two removed tracks");
|
|
assert_true(removedtracks.includes(pc2TrackEvents[0].track),
|
|
"First track should be removed");
|
|
assert_true(removedtracks.includes(pc2TrackEvents[1].track),
|
|
"Second track should be removed");
|
|
|
|
addtrackEventCollector = collectAddTrackEvents(receiveStream);
|
|
await pc2.setRemoteDescription({type: "rollback"});
|
|
addedtracks = addtrackEventCollector.finish().map(e => e.track);
|
|
assert_equals(addedtracks.length, 2, "Rollback should have added two tracks");
|
|
|
|
// pc2 as offerer
|
|
offer = await pc2.createOffer();
|
|
await pc2.setLocalDescription(offer);
|
|
await pc1.setRemoteDescription(offer);
|
|
answer = await pc1.createAnswer();
|
|
await pc1.setLocalDescription(answer);
|
|
|
|
removetrackEventCollector = collectRemoveTrackEvents(receiveStream);
|
|
await pc2.setRemoteDescription(answer);
|
|
removedtracks = removetrackEventCollector.finish().map(e => e.track);
|
|
assert_equals(removedtracks.length, 2, "Should have two removed tracks");
|
|
|
|
hasProps(pc2.getTransceivers(),
|
|
[
|
|
{
|
|
currentDirection: "inactive"
|
|
},
|
|
{
|
|
currentDirection: "inactive"
|
|
}
|
|
]);
|
|
};
|
|
|
|
const checkSetDirection = async t => {
|
|
const pc = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc.close());
|
|
pc.addTransceiver("audio");
|
|
|
|
pc.getTransceivers()[0].direction = "sendonly";
|
|
hasProps(pc.getTransceivers(),[{direction: "sendonly"}]);
|
|
pc.getTransceivers()[0].direction = "recvonly";
|
|
hasProps(pc.getTransceivers(),[{direction: "recvonly"}]);
|
|
pc.getTransceivers()[0].direction = "inactive";
|
|
hasProps(pc.getTransceivers(),[{direction: "inactive"}]);
|
|
pc.getTransceivers()[0].direction = "sendrecv";
|
|
hasProps(pc.getTransceivers(),[{direction: "sendrecv"}]);
|
|
};
|
|
|
|
const checkCurrentDirection = async t => {
|
|
const pc1 = new RTCPeerConnection();
|
|
const pc2 = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc1.close());
|
|
t.add_cleanup(() => pc2.close());
|
|
|
|
const stream = await getNoiseStream({audio: true});
|
|
t.add_cleanup(() => stopTracks(stream));
|
|
const track = stream.getAudioTracks()[0];
|
|
pc1.addTrack(track, stream);
|
|
pc2.addTrack(track, stream);
|
|
hasProps(pc1.getTransceivers(), [{currentDirection: null}]);
|
|
|
|
let offer = await pc1.createOffer();
|
|
hasProps(pc1.getTransceivers(), [{currentDirection: null}]);
|
|
|
|
await pc1.setLocalDescription(offer);
|
|
hasProps(pc1.getTransceivers(), [{currentDirection: null}]);
|
|
|
|
let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
|
|
hasProps(trackEvents,
|
|
[
|
|
{
|
|
track: pc2.getTransceivers()[0].receiver.track,
|
|
streams: [{id: stream.id}]
|
|
}
|
|
]);
|
|
|
|
hasProps(pc2.getTransceivers(), [{currentDirection: null}]);
|
|
|
|
let answer = await pc2.createAnswer();
|
|
hasProps(pc2.getTransceivers(), [{currentDirection: null}]);
|
|
|
|
await pc2.setLocalDescription(answer);
|
|
hasProps(pc2.getTransceivers(), [{currentDirection: "sendrecv"}]);
|
|
|
|
trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
|
|
hasProps(trackEvents,
|
|
[
|
|
{
|
|
track: pc1.getTransceivers()[0].receiver.track,
|
|
streams: [{id: stream.id}]
|
|
}
|
|
]);
|
|
|
|
hasProps(pc1.getTransceivers(), [{currentDirection: "sendrecv"}]);
|
|
|
|
pc2.getTransceivers()[0].direction = "sendonly";
|
|
|
|
offer = await pc2.createOffer();
|
|
hasProps(pc2.getTransceivers(), [{currentDirection: "sendrecv"}]);
|
|
|
|
await pc2.setLocalDescription(offer);
|
|
hasProps(pc2.getTransceivers(), [{currentDirection: "sendrecv"}]);
|
|
|
|
trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, offer);
|
|
hasProps(trackEvents, []);
|
|
|
|
hasProps(pc1.getTransceivers(), [{currentDirection: "sendrecv"}]);
|
|
|
|
answer = await pc1.createAnswer();
|
|
hasProps(pc1.getTransceivers(), [{currentDirection: "sendrecv"}]);
|
|
|
|
await pc1.setLocalDescription(answer);
|
|
hasProps(pc1.getTransceivers(), [{currentDirection: "recvonly"}]);
|
|
|
|
trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, answer);
|
|
hasProps(trackEvents, []);
|
|
|
|
hasProps(pc2.getTransceivers(), [{currentDirection: "sendonly"}]);
|
|
|
|
pc2.getTransceivers()[0].direction = "sendrecv";
|
|
|
|
offer = await pc2.createOffer();
|
|
hasProps(pc2.getTransceivers(), [{currentDirection: "sendonly"}]);
|
|
|
|
await pc2.setLocalDescription(offer);
|
|
hasProps(pc2.getTransceivers(), [{currentDirection: "sendonly"}]);
|
|
|
|
trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, offer);
|
|
hasProps(trackEvents, []);
|
|
|
|
hasProps(pc1.getTransceivers(), [{currentDirection: "recvonly"}]);
|
|
|
|
answer = await pc1.createAnswer();
|
|
hasProps(pc1.getTransceivers(), [{currentDirection: "recvonly"}]);
|
|
|
|
await pc1.setLocalDescription(answer);
|
|
hasProps(pc1.getTransceivers(), [{currentDirection: "sendrecv"}]);
|
|
|
|
trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, answer);
|
|
hasProps(trackEvents,
|
|
[
|
|
{
|
|
track: pc2.getTransceivers()[0].receiver.track,
|
|
streams: [{id: stream.id}]
|
|
}
|
|
]);
|
|
|
|
hasProps(pc2.getTransceivers(), [{currentDirection: "sendrecv"}]);
|
|
|
|
pc2.close();
|
|
hasProps(pc2.getTransceivers(), [{currentDirection: "stopped"}]);
|
|
};
|
|
|
|
const checkSendrecvWithNoSendTrack = async t => {
|
|
const pc1 = new RTCPeerConnection();
|
|
const pc2 = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc1.close());
|
|
t.add_cleanup(() => pc2.close());
|
|
|
|
const stream = await getNoiseStream({audio: true});
|
|
t.add_cleanup(() => stopTracks(stream));
|
|
const track = stream.getAudioTracks()[0];
|
|
pc1.addTransceiver("audio");
|
|
pc1.getTransceivers()[0].direction = "sendrecv";
|
|
pc2.addTrack(track, stream);
|
|
|
|
const offer = await pc1.createOffer();
|
|
|
|
let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
|
|
hasProps(trackEvents,
|
|
[
|
|
{
|
|
track: pc2.getTransceivers()[0].receiver.track,
|
|
streams: []
|
|
}
|
|
]);
|
|
|
|
trickle(t, pc1, pc2);
|
|
await pc1.setLocalDescription(offer);
|
|
|
|
const answer = await pc2.createAnswer();
|
|
trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
|
|
// Spec language doesn't say anything about checking whether the transceiver
|
|
// is stopped here.
|
|
hasProps(trackEvents,
|
|
[
|
|
{
|
|
track: pc1.getTransceivers()[0].receiver.track,
|
|
streams: [{id: stream.id}]
|
|
}
|
|
]);
|
|
|
|
trickle(t, pc2, pc1);
|
|
await pc2.setLocalDescription(answer);
|
|
|
|
await iceConnected(pc1);
|
|
await iceConnected(pc2);
|
|
};
|
|
|
|
const checkSendrecvWithTracklessStream = async t => {
|
|
const pc1 = new RTCPeerConnection();
|
|
const pc2 = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc1.close());
|
|
t.add_cleanup(() => pc2.close());
|
|
|
|
const stream = new MediaStream();
|
|
pc1.addTransceiver("audio", {streams: [stream]});
|
|
|
|
const offer = await pc1.createOffer();
|
|
|
|
const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
|
|
hasProps(trackEvents,
|
|
[
|
|
{
|
|
track: pc2.getTransceivers()[0].receiver.track,
|
|
streams: [{id: stream.id}]
|
|
}
|
|
]);
|
|
};
|
|
|
|
const checkMute = async t => {
|
|
const pc1 = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc1.close());
|
|
const stream1 = await getNoiseStream({audio: true, video: true});
|
|
t.add_cleanup(() => stopTracks(stream1));
|
|
const audio1 = stream1.getAudioTracks()[0];
|
|
pc1.addTrack(audio1, stream1);
|
|
const countMuteAudio1 = countEvents(pc1.getTransceivers()[0].receiver.track, "mute");
|
|
const countUnmuteAudio1 = countEvents(pc1.getTransceivers()[0].receiver.track, "unmute");
|
|
|
|
const video1 = stream1.getVideoTracks()[0];
|
|
pc1.addTrack(video1, stream1);
|
|
const countMuteVideo1 = countEvents(pc1.getTransceivers()[1].receiver.track, "mute");
|
|
const countUnmuteVideo1 = countEvents(pc1.getTransceivers()[1].receiver.track, "unmute");
|
|
|
|
const pc2 = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc2.close());
|
|
const stream2 = await getNoiseStream({audio: true, video: true});
|
|
t.add_cleanup(() => stopTracks(stream2));
|
|
const audio2 = stream2.getAudioTracks()[0];
|
|
pc2.addTrack(audio2, stream2);
|
|
const countMuteAudio2 = countEvents(pc2.getTransceivers()[0].receiver.track, "mute");
|
|
const countUnmuteAudio2 = countEvents(pc2.getTransceivers()[0].receiver.track, "unmute");
|
|
|
|
const video2 = stream2.getVideoTracks()[0];
|
|
pc2.addTrack(video2, stream2);
|
|
const countMuteVideo2 = countEvents(pc2.getTransceivers()[1].receiver.track, "mute");
|
|
const countUnmuteVideo2 = countEvents(pc2.getTransceivers()[1].receiver.track, "unmute");
|
|
|
|
|
|
// Check that receive tracks start muted
|
|
hasProps(pc1.getTransceivers(),
|
|
[
|
|
{receiver: {track: {kind: "audio", muted: true}}},
|
|
{receiver: {track: {kind: "video", muted: true}}}
|
|
]);
|
|
|
|
hasProps(pc1.getTransceivers(),
|
|
[
|
|
{receiver: {track: {kind: "audio", muted: true}}},
|
|
{receiver: {track: {kind: "video", muted: true}}}
|
|
]);
|
|
|
|
let offer = await pc1.createOffer();
|
|
await pc2.setRemoteDescription(offer);
|
|
trickle(t, pc1, pc2);
|
|
await pc1.setLocalDescription(offer);
|
|
let answer = await pc2.createAnswer();
|
|
await pc1.setRemoteDescription(answer);
|
|
trickle(t, pc2, pc1);
|
|
await pc2.setLocalDescription(answer);
|
|
|
|
let gotUnmuteAudio1 = gotUnmuteEvent(pc1.getTransceivers()[0].receiver.track);
|
|
let gotUnmuteVideo1 = gotUnmuteEvent(pc1.getTransceivers()[1].receiver.track);
|
|
|
|
let gotUnmuteAudio2 = gotUnmuteEvent(pc2.getTransceivers()[0].receiver.track);
|
|
let gotUnmuteVideo2 = gotUnmuteEvent(pc2.getTransceivers()[1].receiver.track);
|
|
// Jump out before waiting if a track is unmuted before RTP starts flowing.
|
|
assert_true(pc1.getTransceivers()[0].receiver.track.muted);
|
|
assert_true(pc1.getTransceivers()[1].receiver.track.muted);
|
|
assert_true(pc2.getTransceivers()[0].receiver.track.muted);
|
|
assert_true(pc2.getTransceivers()[1].receiver.track.muted);
|
|
|
|
await iceConnected(pc1);
|
|
await iceConnected(pc2);
|
|
|
|
|
|
// Check that receive tracks are unmuted when RTP starts flowing
|
|
await gotUnmuteAudio1;
|
|
await gotUnmuteVideo1;
|
|
await gotUnmuteAudio2;
|
|
await gotUnmuteVideo2;
|
|
|
|
// Check whether disabling recv locally causes onmute
|
|
pc1.getTransceivers()[0].direction = "sendonly";
|
|
pc1.getTransceivers()[1].direction = "sendonly";
|
|
offer = await pc1.createOffer();
|
|
await pc2.setRemoteDescription(offer);
|
|
await pc1.setLocalDescription(offer);
|
|
answer = await pc2.createAnswer();
|
|
const gotMuteAudio1 = gotMuteEvent(pc1.getTransceivers()[0].receiver.track);
|
|
const gotMuteVideo1 = gotMuteEvent(pc1.getTransceivers()[1].receiver.track);
|
|
await pc1.setRemoteDescription(answer);
|
|
await pc2.setLocalDescription(answer);
|
|
await gotMuteAudio1;
|
|
await gotMuteVideo1;
|
|
|
|
// Check whether disabling on remote causes onmute
|
|
pc1.getTransceivers()[0].direction = "inactive";
|
|
pc1.getTransceivers()[1].direction = "inactive";
|
|
offer = await pc1.createOffer();
|
|
const gotMuteAudio2 = gotMuteEvent(pc2.getTransceivers()[0].receiver.track);
|
|
const gotMuteVideo2 = gotMuteEvent(pc2.getTransceivers()[1].receiver.track);
|
|
await pc2.setRemoteDescription(offer);
|
|
await gotMuteAudio2;
|
|
await gotMuteVideo2;
|
|
await pc1.setLocalDescription(offer);
|
|
answer = await pc2.createAnswer();
|
|
await pc1.setRemoteDescription(answer);
|
|
await pc2.setLocalDescription(answer);
|
|
|
|
// Check whether onunmute fires when we turn everything on again
|
|
pc1.getTransceivers()[0].direction = "sendrecv";
|
|
pc1.getTransceivers()[1].direction = "sendrecv";
|
|
offer = await pc1.createOffer();
|
|
await pc2.setRemoteDescription(offer);
|
|
// Set these up before sLD, since that sets [[Receptive]] to true, which
|
|
// could allow an unmute to occur from a packet that was sent before we
|
|
// negotiated inactive!
|
|
gotUnmuteAudio1 = gotUnmuteEvent(pc1.getTransceivers()[0].receiver.track);
|
|
gotUnmuteVideo1 = gotUnmuteEvent(pc1.getTransceivers()[1].receiver.track);
|
|
await pc1.setLocalDescription(offer);
|
|
answer = await pc2.createAnswer();
|
|
gotUnmuteAudio2 = gotUnmuteEvent(pc2.getTransceivers()[0].receiver.track);
|
|
gotUnmuteVideo2 = gotUnmuteEvent(pc2.getTransceivers()[1].receiver.track);
|
|
await pc1.setRemoteDescription(answer);
|
|
await pc2.setLocalDescription(answer);
|
|
await gotUnmuteAudio1;
|
|
await gotUnmuteVideo1;
|
|
await gotUnmuteAudio2;
|
|
await gotUnmuteVideo2;
|
|
|
|
// Wait a little, just in case some stray events fire
|
|
await new Promise(r => t.step_timeout(r, 100));
|
|
|
|
assert_equals(1, countMuteAudio1.count, "Got 1 mute event for pc1's audio track");
|
|
assert_equals(1, countMuteVideo1.count, "Got 1 mute event for pc1's video track");
|
|
assert_equals(1, countMuteAudio2.count, "Got 1 mute event for pc2's audio track");
|
|
assert_equals(1, countMuteVideo2.count, "Got 1 mute event for pc2's video track");
|
|
assert_equals(2, countUnmuteAudio1.count, "Got 2 unmute events for pc1's audio track");
|
|
assert_equals(2, countUnmuteVideo1.count, "Got 2 unmute events for pc1's video track");
|
|
assert_equals(2, countUnmuteAudio2.count, "Got 2 unmute events for pc2's audio track");
|
|
assert_equals(2, countUnmuteVideo2.count, "Got 2 unmute events for pc2's video track");
|
|
};
|
|
|
|
const checkStop = async t => {
|
|
const pc1 = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc1.close());
|
|
const stream = await getNoiseStream({audio: true});
|
|
t.add_cleanup(() => stopTracks(stream));
|
|
const track = stream.getAudioTracks()[0];
|
|
pc1.addTrack(track, stream);
|
|
|
|
let offer = await pc1.createOffer();
|
|
await pc1.setLocalDescription(offer);
|
|
|
|
const pc2 = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc2.close());
|
|
await pc2.setRemoteDescription(offer);
|
|
|
|
pc2.addTrack(track, stream);
|
|
|
|
const answer = await pc2.createAnswer();
|
|
await pc2.setLocalDescription(answer);
|
|
await pc1.setRemoteDescription(answer);
|
|
|
|
let stoppedTransceiver = pc1.getTransceivers()[0];
|
|
let onended = new Promise(resolve => {
|
|
stoppedTransceiver.receiver.track.onended = resolve;
|
|
});
|
|
stoppedTransceiver.stop();
|
|
assert_equals(pc1.getReceivers().length, 1, 'getReceivers exposes a receiver of a stopped transceiver before negotiation');
|
|
assert_equals(pc1.getSenders().length, 1, 'getSenders exposes a sender of a stopped transceiver before negotiation');
|
|
await onended;
|
|
// The transceiver has [[stopping]] = true, [[stopped]] = false
|
|
hasPropsAndUniqueMids(pc1.getTransceivers(),
|
|
[
|
|
{
|
|
sender: {track: {kind: "audio"}},
|
|
receiver: {track: {kind: "audio", readyState: "ended"}},
|
|
currentDirection: "sendrecv",
|
|
direction: "stopped"
|
|
}
|
|
]);
|
|
|
|
const transceiver = pc1.getTransceivers()[0];
|
|
|
|
checkThrows(() => transceiver.sender.setParameters(
|
|
transceiver.sender.getParameters()),
|
|
"InvalidStateError", "setParameters on stopped transceiver");
|
|
|
|
const stream2 = await getNoiseStream({audio: true});
|
|
const track2 = stream.getAudioTracks()[0];
|
|
checkThrows(() => transceiver.sender.replaceTrack(track2),
|
|
"InvalidStateError", "replaceTrack on stopped transceiver");
|
|
|
|
checkThrows(() => transceiver.direction = "sendrecv",
|
|
"InvalidStateError", "set direction on stopped transceiver");
|
|
|
|
checkThrows(() => transceiver.sender.dtmf.insertDTMF("111"),
|
|
"InvalidStateError", "insertDTMF on stopped transceiver");
|
|
|
|
// Shouldn't throw
|
|
stoppedTransceiver.stop();
|
|
|
|
offer = await pc1.createOffer();
|
|
await pc1.setLocalDescription(offer);
|
|
|
|
const stoppedCalleeTransceiver = pc2.getTransceivers()[0];
|
|
onended = new Promise(resolve => {
|
|
stoppedCalleeTransceiver.receiver.track.onended = resolve;
|
|
});
|
|
|
|
await pc2.setRemoteDescription(offer);
|
|
|
|
await onended;
|
|
// pc2's transceiver was stopped remotely.
|
|
// The track ends when setRemeoteDescription(offer) is set.
|
|
hasProps(pc2.getTransceivers(),
|
|
[
|
|
{
|
|
sender: {track: {kind: "audio"}},
|
|
receiver: {track: {kind: "audio", readyState: "ended"}},
|
|
currentDirection: "stopped",
|
|
direction: "stopped"
|
|
}
|
|
]);
|
|
// After setLocalDescription(answer), the transceiver has
|
|
// [[stopping]] = true, [[stopped]] = true, and is removed from pc2.
|
|
const stoppingAnswer = await pc2.createAnswer();
|
|
await pc2.setLocalDescription(stoppingAnswer);
|
|
assert_equals(pc2.getTransceivers().length, 0);
|
|
assert_equals(pc2.getReceivers().length, 0, 'getReceivers does not expose a receiver of a stopped transceiver after negotiation');
|
|
assert_equals(pc2.getSenders().length, 0, 'getSenders does not expose a sender of a stopped transceiver after negotiation');
|
|
|
|
// Shouldn't throw either
|
|
stoppedTransceiver.stop();
|
|
await pc1.setRemoteDescription(stoppingAnswer);
|
|
assert_equals(pc1.getReceivers().length, 0, 'getReceivers does not expose a receiver of a stopped transceiver after negotiation');
|
|
assert_equals(pc1.getSenders().length, 0, 'getSenders does not expose a sender of a stopped transceiver after negotiation');
|
|
|
|
pc1.close();
|
|
pc2.close();
|
|
|
|
// Spec says the closed check comes before the stopped check, so this
|
|
// should throw now.
|
|
checkThrows(() => stoppedTransceiver.stop(),
|
|
"InvalidStateError", "RTCRtpTransceiver.stop() with closed PC");
|
|
};
|
|
|
|
const checkStopAfterCreateOffer = async t => {
|
|
const pc1 = new RTCPeerConnection();
|
|
const pc2 = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc1.close());
|
|
t.add_cleanup(() => pc2.close());
|
|
|
|
const stream = await getNoiseStream({audio: true});
|
|
t.add_cleanup(() => stopTracks(stream));
|
|
const track = stream.getAudioTracks()[0];
|
|
pc1.addTrack(track, stream);
|
|
pc2.addTrack(track, stream);
|
|
|
|
let offer = await pc1.createOffer();
|
|
|
|
const transceiverThatWasStopped = pc1.getTransceivers()[0];
|
|
transceiverThatWasStopped.stop();
|
|
await pc2.setRemoteDescription(offer)
|
|
trickle(t, pc1, pc2);
|
|
await pc1.setLocalDescription(offer);
|
|
|
|
let answer = await pc2.createAnswer();
|
|
const negotiationNeededAwaiter = negotiationNeeded(pc1);
|
|
const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
|
|
// Spec language doesn't say anything about checking whether the transceiver
|
|
// is stopped here.
|
|
hasProps(trackEvents,
|
|
[
|
|
{
|
|
track: pc1.getTransceivers()[0].receiver.track,
|
|
streams: [{id: stream.id}]
|
|
}
|
|
]);
|
|
|
|
assert_equals(transceiverThatWasStopped, pc1.getTransceivers()[0]);
|
|
// The transceiver should still be [[stopping]]=true, [[stopped]]=false.
|
|
hasPropsAndUniqueMids(pc1.getTransceivers(),
|
|
[
|
|
{
|
|
currentDirection: "sendrecv",
|
|
direction: "stopped"
|
|
}
|
|
]);
|
|
|
|
await negotiationNeededAwaiter;
|
|
|
|
trickle(t, pc2, pc1);
|
|
|
|
await pc2.setLocalDescription(answer);
|
|
|
|
await iceConnected(pc1);
|
|
await iceConnected(pc2);
|
|
|
|
offer = await pc1.createOffer();
|
|
await pc1.setLocalDescription(offer);
|
|
await pc2.setRemoteDescription(offer);
|
|
answer = await pc2.createAnswer();
|
|
await pc2.setLocalDescription(answer);
|
|
await pc1.setRemoteDescription(answer);
|
|
assert_equals(pc1.getTransceivers().length, 0);
|
|
assert_equals(pc2.getTransceivers().length, 0);
|
|
};
|
|
|
|
const checkStopAfterSetLocalOffer = async t => {
|
|
const pc1 = new RTCPeerConnection();
|
|
const pc2 = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc1.close());
|
|
t.add_cleanup(() => pc2.close());
|
|
|
|
const stream = await getNoiseStream({audio: true});
|
|
t.add_cleanup(() => stopTracks(stream));
|
|
const track = stream.getAudioTracks()[0];
|
|
pc1.addTrack(track, stream);
|
|
pc2.addTrack(track, stream);
|
|
|
|
let offer = await pc1.createOffer();
|
|
|
|
await pc2.setRemoteDescription(offer)
|
|
trickle(t, pc1, pc2);
|
|
await pc1.setLocalDescription(offer);
|
|
|
|
pc1.getTransceivers()[0].stop();
|
|
|
|
let answer = await pc2.createAnswer();
|
|
const negotiationNeededAwaiter = negotiationNeeded(pc1);
|
|
const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
|
|
// Spec language doesn't say anything about checking whether the transceiver
|
|
// is stopped here.
|
|
hasProps(trackEvents,
|
|
[
|
|
{
|
|
track: pc1.getTransceivers()[0].receiver.track,
|
|
streams: [{id: stream.id}]
|
|
}
|
|
]);
|
|
|
|
hasPropsAndUniqueMids(pc1.getTransceivers(),
|
|
[
|
|
{
|
|
direction: "stopped",
|
|
currentDirection: "sendrecv"
|
|
}
|
|
]);
|
|
await negotiationNeededAwaiter;
|
|
|
|
trickle(t, pc2, pc1);
|
|
await pc2.setLocalDescription(answer);
|
|
|
|
await iceConnected(pc1);
|
|
await iceConnected(pc2);
|
|
|
|
offer = await pc1.createOffer();
|
|
await pc1.setLocalDescription(offer);
|
|
await pc2.setRemoteDescription(offer);
|
|
answer = await pc2.createAnswer();
|
|
await pc2.setLocalDescription(answer);
|
|
await pc1.setRemoteDescription(answer);
|
|
|
|
assert_equals(pc1.getTransceivers().length, 0);
|
|
assert_equals(pc2.getTransceivers().length, 0);
|
|
};
|
|
|
|
const checkStopAfterSetRemoteOffer = async t => {
|
|
const pc1 = new RTCPeerConnection();
|
|
const pc2 = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc1.close());
|
|
t.add_cleanup(() => pc2.close());
|
|
|
|
const stream = await getNoiseStream({audio: true});
|
|
t.add_cleanup(() => stopTracks(stream));
|
|
const track = stream.getAudioTracks()[0];
|
|
pc1.addTrack(track, stream);
|
|
pc2.addTrack(track, stream);
|
|
|
|
const offer = await pc1.createOffer();
|
|
|
|
await pc2.setRemoteDescription(offer)
|
|
await pc1.setLocalDescription(offer);
|
|
|
|
// Stop on _answerer_ side now. Should not stop transceiver in answer,
|
|
// but cause firing of negotiationNeeded at pc2, and disabling
|
|
// of the transceiver with direction = inactive in answer.
|
|
pc2.getTransceivers()[0].stop();
|
|
assert_equals(pc2.getTransceivers()[0].direction, 'stopped');
|
|
|
|
const answer = await pc2.createAnswer();
|
|
const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
|
|
hasProps(trackEvents, []);
|
|
|
|
hasProps(pc2.getTransceivers(),
|
|
[
|
|
{
|
|
direction: "stopped",
|
|
currentDirection: null,
|
|
}
|
|
]);
|
|
|
|
const negotiationNeededAwaiter = negotiationNeeded(pc2);
|
|
await pc2.setLocalDescription(answer);
|
|
hasProps(pc2.getTransceivers(),
|
|
[
|
|
{
|
|
direction: "stopped",
|
|
currentDirection: "inactive",
|
|
}
|
|
]);
|
|
|
|
await negotiationNeededAwaiter;
|
|
};
|
|
|
|
const checkStopAfterCreateAnswer = async t => {
|
|
const pc1 = new RTCPeerConnection();
|
|
const pc2 = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc1.close());
|
|
t.add_cleanup(() => pc2.close());
|
|
|
|
const stream = await getNoiseStream({audio: true});
|
|
t.add_cleanup(() => stopTracks(stream));
|
|
const track = stream.getAudioTracks()[0];
|
|
pc1.addTrack(track, stream);
|
|
pc2.addTrack(track, stream);
|
|
|
|
let offer = await pc1.createOffer();
|
|
|
|
await pc2.setRemoteDescription(offer)
|
|
trickle(t, pc1, pc2);
|
|
await pc1.setLocalDescription(offer);
|
|
|
|
let answer = await pc2.createAnswer();
|
|
|
|
// Too late for this to go in the answer. ICE should succeed.
|
|
pc2.getTransceivers()[0].stop();
|
|
|
|
const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
|
|
hasProps(trackEvents,
|
|
[
|
|
{
|
|
track: pc1.getTransceivers()[0].receiver.track,
|
|
streams: [{id: stream.id}]
|
|
}
|
|
]);
|
|
|
|
hasPropsAndUniqueMids(pc2.getTransceivers(),
|
|
[
|
|
{
|
|
direction: "stopped",
|
|
currentDirection: null,
|
|
}
|
|
]);
|
|
|
|
trickle(t, pc2, pc1);
|
|
// The negotiationneeded event is fired during processing of
|
|
// setLocalDescription()
|
|
const negotiationNeededAwaiter = negotiationNeeded(pc2);
|
|
await pc2.setLocalDescription(answer);
|
|
hasPropsAndUniqueMids(pc2.getTransceivers(),
|
|
[
|
|
{
|
|
direction: "stopped",
|
|
currentDirection: "sendrecv",
|
|
}
|
|
]);
|
|
|
|
await negotiationNeededAwaiter;
|
|
await iceConnected(pc1);
|
|
await iceConnected(pc2);
|
|
|
|
offer = await pc1.createOffer();
|
|
await pc1.setLocalDescription(offer);
|
|
await pc2.setRemoteDescription(offer);
|
|
answer = await pc2.createAnswer();
|
|
await pc2.setLocalDescription(answer);
|
|
await pc1.setRemoteDescription(answer);
|
|
|
|
// Since this offer/answer exchange was initiated from pc1,
|
|
// pc2 still doesn't get to say that it has a stopped transceiver,
|
|
// but does get to set it to inactive.
|
|
hasProps(pc1.getTransceivers(),
|
|
[
|
|
{
|
|
direction: "sendrecv",
|
|
currentDirection: "inactive",
|
|
}
|
|
]);
|
|
|
|
hasProps(pc2.getTransceivers(),
|
|
[
|
|
{
|
|
direction: "stopped",
|
|
currentDirection: "inactive",
|
|
}
|
|
]);
|
|
};
|
|
|
|
const checkStopAfterSetLocalAnswer = async t => {
|
|
const pc1 = new RTCPeerConnection();
|
|
const pc2 = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc1.close());
|
|
t.add_cleanup(() => pc2.close());
|
|
|
|
const stream = await getNoiseStream({audio: true});
|
|
t.add_cleanup(() => stopTracks(stream));
|
|
const track = stream.getAudioTracks()[0];
|
|
pc1.addTrack(track, stream);
|
|
pc2.addTrack(track, stream);
|
|
|
|
let offer = await pc1.createOffer();
|
|
|
|
await pc2.setRemoteDescription(offer)
|
|
trickle(t, pc1, pc2);
|
|
await pc1.setLocalDescription(offer);
|
|
|
|
let answer = await pc2.createAnswer();
|
|
|
|
const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
|
|
hasProps(trackEvents,
|
|
[
|
|
{
|
|
track: pc1.getTransceivers()[0].receiver.track,
|
|
streams: [{id: stream.id}]
|
|
}
|
|
]);
|
|
|
|
trickle(t, pc2, pc1);
|
|
await pc2.setLocalDescription(answer);
|
|
|
|
// ICE should succeed.
|
|
pc2.getTransceivers()[0].stop();
|
|
|
|
hasPropsAndUniqueMids(pc2.getTransceivers(),
|
|
[
|
|
{
|
|
direction: "stopped",
|
|
currentDirection: "sendrecv",
|
|
}
|
|
]);
|
|
|
|
await negotiationNeeded(pc2);
|
|
await iceConnected(pc1);
|
|
await iceConnected(pc2);
|
|
|
|
// Initiate an offer/answer exchange from pc2 in order
|
|
// to negotiate the stopped transceiver.
|
|
offer = await pc2.createOffer();
|
|
await pc2.setLocalDescription(offer);
|
|
await pc1.setRemoteDescription(offer);
|
|
answer = await pc1.createAnswer();
|
|
await pc1.setLocalDescription(answer);
|
|
await pc2.setRemoteDescription(answer);
|
|
|
|
assert_equals(pc1.getTransceivers().length, 0);
|
|
assert_equals(pc2.getTransceivers().length, 0);
|
|
};
|
|
|
|
const checkStopAfterClose = async t => {
|
|
const pc1 = new RTCPeerConnection();
|
|
const pc2 = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc1.close());
|
|
t.add_cleanup(() => pc2.close());
|
|
|
|
const stream = await getNoiseStream({audio: true});
|
|
t.add_cleanup(() => stopTracks(stream));
|
|
const track = stream.getAudioTracks()[0];
|
|
pc1.addTrack(track, stream);
|
|
pc2.addTrack(track, stream);
|
|
|
|
const offer = await pc1.createOffer();
|
|
await pc2.setRemoteDescription(offer)
|
|
await pc1.setLocalDescription(offer);
|
|
const answer = await pc2.createAnswer();
|
|
await pc2.setLocalDescription(answer);
|
|
await pc1.setRemoteDescription(answer);
|
|
|
|
pc1.close();
|
|
await checkThrows(() => pc1.getTransceivers()[0].stop(),
|
|
"InvalidStateError",
|
|
"Stopping a transceiver on a closed PC should throw.");
|
|
};
|
|
|
|
const checkLocalRollback = async t => {
|
|
const pc = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc.close());
|
|
|
|
const stream = await getNoiseStream({audio: true});
|
|
t.add_cleanup(() => stopTracks(stream));
|
|
const track = stream.getAudioTracks()[0];
|
|
pc.addTrack(track, stream);
|
|
|
|
let offer = await pc.createOffer();
|
|
await pc.setLocalDescription(offer);
|
|
|
|
hasPropsAndUniqueMids(pc.getTransceivers(),
|
|
[
|
|
{
|
|
receiver: {track: {kind: "audio"}},
|
|
sender: {track},
|
|
direction: "sendrecv",
|
|
currentDirection: null,
|
|
}
|
|
]);
|
|
|
|
// Verify that rollback doesn't stomp things it should not
|
|
pc.getTransceivers()[0].direction = "sendonly";
|
|
const stream2 = await getNoiseStream({audio: true});
|
|
const track2 = stream2.getAudioTracks()[0];
|
|
await pc.getTransceivers()[0].sender.replaceTrack(track2);
|
|
|
|
await pc.setLocalDescription({type: "rollback"});
|
|
|
|
hasProps(pc.getTransceivers(),
|
|
[
|
|
{
|
|
receiver: {track: {kind: "audio"}},
|
|
sender: {track: track2},
|
|
direction: "sendonly",
|
|
mid: null,
|
|
currentDirection: null,
|
|
}
|
|
]);
|
|
|
|
// Make sure stop() isn't rolled back either.
|
|
offer = await pc.createOffer();
|
|
await pc.setLocalDescription(offer);
|
|
pc.getTransceivers()[0].stop();
|
|
await pc.setLocalDescription({type: "rollback"});
|
|
|
|
hasProps(pc.getTransceivers(), [
|
|
{
|
|
direction: "stopped",
|
|
}
|
|
]);
|
|
};
|
|
|
|
const checkRollbackAndSetRemoteOfferWithDifferentType = async t => {
|
|
const pc1 = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc1.close());
|
|
|
|
const audioStream = await getNoiseStream({audio: true});
|
|
t.add_cleanup(() => stopTracks(audioStream));
|
|
const audioTrack = audioStream.getAudioTracks()[0];
|
|
pc1.addTrack(audioTrack, audioStream);
|
|
|
|
const pc2 = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc2.close());
|
|
|
|
const videoStream = await getNoiseStream({video: true});
|
|
t.add_cleanup(() => stopTracks(videoStream));
|
|
const videoTrack = videoStream.getVideoTracks()[0];
|
|
pc2.addTrack(videoTrack, videoStream);
|
|
|
|
await pc1.setLocalDescription(await pc1.createOffer());
|
|
await pc1.setLocalDescription({type: "rollback"});
|
|
|
|
hasProps(pc1.getTransceivers(),
|
|
[
|
|
{
|
|
receiver: {track: {kind: "audio"}},
|
|
sender: {track: audioTrack},
|
|
direction: "sendrecv",
|
|
mid: null,
|
|
currentDirection: null,
|
|
}
|
|
]);
|
|
|
|
hasProps(pc2.getTransceivers(),
|
|
[
|
|
{
|
|
receiver: {track: {kind: "video"}},
|
|
sender: {track: videoTrack},
|
|
direction: "sendrecv",
|
|
mid: null,
|
|
currentDirection: null,
|
|
}
|
|
]);
|
|
|
|
await offerAnswer(pc2, pc1);
|
|
|
|
hasPropsAndUniqueMids(pc1.getTransceivers(),
|
|
[
|
|
{
|
|
receiver: {track: {kind: "audio"}},
|
|
sender: {track: audioTrack},
|
|
direction: "sendrecv",
|
|
mid: null,
|
|
currentDirection: null,
|
|
},
|
|
{
|
|
receiver: {track: {kind: "video"}},
|
|
sender: {track: null},
|
|
direction: "recvonly",
|
|
currentDirection: "recvonly",
|
|
}
|
|
]);
|
|
|
|
hasPropsAndUniqueMids(pc2.getTransceivers(),
|
|
[
|
|
{
|
|
receiver: {track: {kind: "video"}},
|
|
sender: {track: videoTrack},
|
|
direction: "sendrecv",
|
|
currentDirection: "sendonly",
|
|
}
|
|
]);
|
|
|
|
await offerAnswer(pc1, pc2);
|
|
};
|
|
|
|
const checkRemoteRollback = async t => {
|
|
const pc1 = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc1.close());
|
|
|
|
const stream = await getNoiseStream({audio: true});
|
|
t.add_cleanup(() => stopTracks(stream));
|
|
const track = stream.getAudioTracks()[0];
|
|
pc1.addTrack(track, stream);
|
|
|
|
let offer = await pc1.createOffer();
|
|
|
|
const pc2 = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc2.close());
|
|
await pc2.setRemoteDescription(offer);
|
|
|
|
const removedTransceiver = pc2.getTransceivers()[0];
|
|
|
|
const onended = new Promise(resolve => {
|
|
removedTransceiver.receiver.track.onended = resolve;
|
|
});
|
|
|
|
await pc2.setRemoteDescription({type: "rollback"});
|
|
|
|
// Transceiver should be _gone_
|
|
hasProps(pc2.getTransceivers(), []);
|
|
|
|
hasProps(removedTransceiver,
|
|
{
|
|
mid: null,
|
|
currentDirection: "stopped"
|
|
}
|
|
);
|
|
|
|
await onended;
|
|
|
|
hasProps(removedTransceiver,
|
|
{
|
|
receiver: {track: {readyState: "ended"}},
|
|
mid: null,
|
|
currentDirection: "stopped"
|
|
}
|
|
);
|
|
|
|
// Setting the same offer again should do the same thing as before
|
|
await pc2.setRemoteDescription(offer);
|
|
hasPropsAndUniqueMids(pc2.getTransceivers(),
|
|
[
|
|
{
|
|
receiver: {track: {kind: "audio"}},
|
|
sender: {track: null},
|
|
direction: "recvonly",
|
|
currentDirection: null,
|
|
}
|
|
]);
|
|
|
|
const mid0 = pc2.getTransceivers()[0].mid;
|
|
|
|
// Give pc2 a track with replaceTrack
|
|
const stream2 = await getNoiseStream({audio: true});
|
|
t.add_cleanup(() => stopTracks(stream2));
|
|
const track2 = stream2.getAudioTracks()[0];
|
|
await pc2.getTransceivers()[0].sender.replaceTrack(track2);
|
|
pc2.getTransceivers()[0].direction = "sendrecv";
|
|
hasProps(pc2.getTransceivers(),
|
|
[
|
|
{
|
|
receiver: {track: {kind: "audio"}},
|
|
sender: {track: track2},
|
|
direction: "sendrecv",
|
|
mid: mid0,
|
|
currentDirection: null,
|
|
}
|
|
]);
|
|
|
|
await pc2.setRemoteDescription({type: "rollback"});
|
|
|
|
// Transceiver should be _gone_, again. replaceTrack doesn't prevent this,
|
|
// nor does setting direction.
|
|
hasProps(pc2.getTransceivers(), []);
|
|
|
|
// Setting the same offer for a _third_ time should do the same thing
|
|
await pc2.setRemoteDescription(offer);
|
|
hasProps(pc2.getTransceivers(),
|
|
[
|
|
{
|
|
receiver: {track: {kind: "audio"}},
|
|
sender: {track: null},
|
|
direction: "recvonly",
|
|
mid: mid0,
|
|
currentDirection: null,
|
|
}
|
|
]);
|
|
|
|
// We should be able to add the same track again
|
|
pc2.addTrack(track2, stream2);
|
|
hasProps(pc2.getTransceivers(),
|
|
[
|
|
{
|
|
receiver: {track: {kind: "audio"}},
|
|
sender: {track: track2},
|
|
direction: "sendrecv",
|
|
mid: mid0,
|
|
currentDirection: null,
|
|
}
|
|
]);
|
|
|
|
await pc2.setRemoteDescription({type: "rollback"});
|
|
// Transceiver should _not_ be gone this time, because addTrack touched it.
|
|
hasProps(pc2.getTransceivers(),
|
|
[
|
|
{
|
|
receiver: {track: {kind: "audio"}},
|
|
sender: {track: track2},
|
|
direction: "sendrecv",
|
|
mid: null,
|
|
currentDirection: null,
|
|
}
|
|
]);
|
|
|
|
// Complete negotiation so we can test interactions with transceiver.stop()
|
|
await pc1.setLocalDescription(offer);
|
|
|
|
// After all this SRD/rollback, we should still get the track event
|
|
let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
|
|
|
|
assert_equals(trackEvents.length, 1);
|
|
hasProps(trackEvents,
|
|
[
|
|
{
|
|
track: pc2.getTransceivers()[0].receiver.track,
|
|
streams: [{id: stream.id}]
|
|
}
|
|
]);
|
|
|
|
const answer = await pc2.createAnswer();
|
|
await pc2.setLocalDescription(answer);
|
|
|
|
// Make sure all this rollback hasn't messed up the signaling
|
|
trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
|
|
assert_equals(trackEvents.length, 1);
|
|
hasProps(trackEvents,
|
|
[
|
|
{
|
|
track: pc1.getTransceivers()[0].receiver.track,
|
|
streams: [{id: stream2.id}]
|
|
}
|
|
]);
|
|
hasProps(pc1.getTransceivers(),
|
|
[
|
|
{
|
|
receiver: {track: {kind: "audio"}},
|
|
sender: {track},
|
|
direction: "sendrecv",
|
|
mid: mid0,
|
|
currentDirection: "sendrecv",
|
|
}
|
|
]);
|
|
|
|
// Don't bother waiting for ICE and such
|
|
|
|
// Check to see whether rolling back a remote track removal works
|
|
pc1.getTransceivers()[0].direction = "recvonly";
|
|
offer = await pc1.createOffer();
|
|
|
|
trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
|
|
hasProps(trackEvents, []);
|
|
|
|
trackEvents =
|
|
await setRemoteDescriptionReturnTrackEvents(pc2, {type: "rollback"});
|
|
|
|
assert_equals(trackEvents.length, 1, 'track event from remote rollback');
|
|
hasProps(trackEvents,
|
|
[
|
|
{
|
|
track: pc2.getTransceivers()[0].receiver.track,
|
|
streams: [{id: stream.id}]
|
|
}
|
|
]);
|
|
|
|
// Check to see that stop() cannot be rolled back
|
|
pc1.getTransceivers()[0].stop();
|
|
offer = await pc1.createOffer();
|
|
|
|
await pc2.setRemoteDescription(offer);
|
|
hasProps(pc2.getTransceivers(),
|
|
[
|
|
{
|
|
receiver: {track: {kind: "audio"}},
|
|
sender: {track: track2},
|
|
direction: "stopped",
|
|
mid: mid0,
|
|
currentDirection: "stopped",
|
|
}
|
|
]);
|
|
|
|
// stop() cannot be rolled back!
|
|
// Transceiver should have [[stopping]]=true, [[stopped]]=false.
|
|
await pc2.setRemoteDescription({type: "rollback"});
|
|
hasProps(pc2.getTransceivers(),
|
|
[
|
|
{
|
|
receiver: {track: {kind: "audio"}},
|
|
sender: {track: {kind: "audio"}},
|
|
direction: "stopped",
|
|
mid: mid0,
|
|
currentDirection: "stopped",
|
|
}
|
|
]);
|
|
};
|
|
|
|
const checkBundleTagRejected = async t => {
|
|
const pc1 = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc1.close());
|
|
const pc2 = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc2.close());
|
|
|
|
const stream1 = await getNoiseStream({audio: true});
|
|
t.add_cleanup(() => stopTracks(stream1));
|
|
const track1 = stream1.getAudioTracks()[0];
|
|
const stream2 = await getNoiseStream({audio: true});
|
|
t.add_cleanup(() => stopTracks(stream2));
|
|
const track2 = stream2.getAudioTracks()[0];
|
|
|
|
pc1.addTrack(track1, stream1);
|
|
pc1.addTrack(track2, stream2);
|
|
|
|
await offerAnswer(pc1, pc2);
|
|
|
|
pc2.getTransceivers()[0].stop();
|
|
|
|
await offerAnswer(pc1, pc2);
|
|
await offerAnswer(pc2, pc1);
|
|
};
|
|
|
|
const checkMsectionReuse = async t => {
|
|
// Use max-compat to make it easier to check for disabled m-sections
|
|
const pc1 = new RTCPeerConnection({ bundlePolicy: "max-compat" });
|
|
const pc2 = new RTCPeerConnection({ bundlePolicy: "max-compat" });
|
|
t.add_cleanup(() => pc1.close());
|
|
t.add_cleanup(() => pc2.close());
|
|
|
|
const stream = await getNoiseStream({audio: true});
|
|
t.add_cleanup(() => stopTracks(stream));
|
|
const track = stream.getAudioTracks()[0];
|
|
pc1.addTrack(track, stream);
|
|
const [pc1Transceiver] = pc1.getTransceivers();
|
|
|
|
await pc1.setLocalDescription();
|
|
await pc2.setRemoteDescription(pc1.localDescription);
|
|
|
|
// Answerer stops transceiver. The m-section is not immediately rejected
|
|
// (a follow-up O/A exchange is needed) but it should become inactive in
|
|
// the meantime.
|
|
const stoppedMid0 = pc2.getTransceivers()[0].mid;
|
|
const [pc2Transceiver] = pc2.getTransceivers();
|
|
pc2Transceiver.stop();
|
|
assert_equals(pc2.getTransceivers()[0].direction, "stopped");
|
|
assert_not_equals(pc2.getTransceivers()[0].currentDirection, "stopped");
|
|
|
|
await pc2.setLocalDescription();
|
|
await pc1.setRemoteDescription(pc2.localDescription);
|
|
|
|
// Still not stopped - but inactive is reflected!
|
|
assert_equals(pc1Transceiver.mid, stoppedMid0);
|
|
assert_equals(pc1Transceiver.direction, "sendrecv");
|
|
assert_equals(pc1Transceiver.currentDirection, "inactive");
|
|
assert_equals(pc2Transceiver.mid, stoppedMid0);
|
|
assert_equals(pc2Transceiver.direction, "stopped");
|
|
assert_equals(pc2Transceiver.currentDirection, "inactive");
|
|
|
|
// Now do the follow-up O/A exchange pc2 -> pc1.
|
|
await pc2.setLocalDescription();
|
|
await pc1.setRemoteDescription(pc2.localDescription);
|
|
await pc1.setLocalDescription();
|
|
await pc2.setRemoteDescription(pc1.localDescription);
|
|
|
|
// Now they're stopped, and have been removed from the PCs.
|
|
assert_equals(pc1.getTransceivers().length, 0);
|
|
assert_equals(pc2.getTransceivers().length, 0);
|
|
assert_equals(pc1Transceiver.mid, null);
|
|
assert_equals(pc1Transceiver.direction, "stopped");
|
|
assert_equals(pc1Transceiver.currentDirection, "stopped");
|
|
assert_equals(pc2Transceiver.mid, null);
|
|
assert_equals(pc2Transceiver.direction, "stopped");
|
|
assert_equals(pc2Transceiver.currentDirection, "stopped");
|
|
|
|
// Check that m-section is reused on both ends
|
|
const stream2 = await getNoiseStream({audio: true});
|
|
t.add_cleanup(() => stopTracks(stream2));
|
|
const track2 = stream2.getAudioTracks()[0];
|
|
|
|
pc1.addTrack(track2, stream2);
|
|
let offer = await pc1.createOffer();
|
|
assert_equals(offer.sdp.match(/m=/g).length, 1,
|
|
"Exactly one m-line in offer, because it was reused");
|
|
hasProps(pc1.getTransceivers(),
|
|
[
|
|
{
|
|
sender: {track: track2}
|
|
}
|
|
]);
|
|
|
|
assert_not_equals(pc1.getTransceivers()[0].mid, stoppedMid0);
|
|
|
|
pc2.addTrack(track, stream);
|
|
offer = await pc2.createOffer();
|
|
assert_equals(offer.sdp.match(/m=/g).length, 1,
|
|
"Exactly one m-line in offer, because it was reused");
|
|
hasProps(pc2.getTransceivers(),
|
|
[
|
|
{
|
|
sender: {track}
|
|
}
|
|
]);
|
|
|
|
assert_not_equals(pc2.getTransceivers()[0].mid, stoppedMid0);
|
|
|
|
await pc2.setLocalDescription(offer);
|
|
await pc1.setRemoteDescription(offer);
|
|
let answer = await pc1.createAnswer();
|
|
await pc1.setLocalDescription(answer);
|
|
await pc2.setRemoteDescription(answer);
|
|
hasPropsAndUniqueMids(pc1.getTransceivers(),
|
|
[
|
|
{
|
|
sender: {track: track2},
|
|
currentDirection: "sendrecv"
|
|
}
|
|
]);
|
|
|
|
const mid0 = pc1.getTransceivers()[0].mid;
|
|
|
|
hasProps(pc2.getTransceivers(),
|
|
[
|
|
{
|
|
sender: {track},
|
|
currentDirection: "sendrecv",
|
|
mid: mid0
|
|
}
|
|
]);
|
|
|
|
// stop the transceiver, and add a track. Verify that we don't reuse
|
|
// prematurely in our offer. (There should be one rejected m-section, and a
|
|
// new one for the new track)
|
|
const stoppedMid1 = pc1.getTransceivers()[0].mid;
|
|
pc1.getTransceivers()[0].stop();
|
|
const stream3 = await getNoiseStream({audio: true});
|
|
t.add_cleanup(() => stopTracks(stream3));
|
|
const track3 = stream3.getAudioTracks()[0];
|
|
pc1.addTrack(track3, stream3);
|
|
offer = await pc1.createOffer();
|
|
assert_equals(offer.sdp.match(/m=/g).length, 2,
|
|
"Exactly 2 m-lines in offer, because it is too early to reuse");
|
|
assert_equals(offer.sdp.match(/m=audio 0 /g).length, 1,
|
|
"One m-line is rejected");
|
|
|
|
await pc1.setLocalDescription(offer);
|
|
|
|
let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
|
|
hasProps(trackEvents,
|
|
[
|
|
{
|
|
track: pc2.getTransceivers()[1].receiver.track,
|
|
streams: [{id: stream3.id}]
|
|
}
|
|
]);
|
|
|
|
answer = await pc2.createAnswer();
|
|
await pc2.setLocalDescription(answer);
|
|
|
|
trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
|
|
hasProps(trackEvents, []);
|
|
|
|
hasPropsAndUniqueMids(pc2.getTransceivers(),
|
|
[
|
|
{
|
|
sender: {track: null},
|
|
currentDirection: "recvonly"
|
|
}
|
|
]);
|
|
|
|
// Verify that we don't reuse the mid from the stopped transceiver
|
|
const mid1 = pc2.getTransceivers()[0].mid;
|
|
assert_not_equals(mid1, stoppedMid1);
|
|
|
|
pc2.addTrack(track3, stream3);
|
|
// There are two ways to handle this new track; reuse the recvonly
|
|
// transceiver created above, or create a new transceiver and reuse the
|
|
// disabled m-section. We're supposed to do the former.
|
|
offer = await pc2.createOffer();
|
|
assert_equals(offer.sdp.match(/m=/g).length, 2, "Exactly 2 m-lines in offer");
|
|
assert_equals(offer.sdp.match(/m=audio 0 /g).length, 1,
|
|
"One m-line is rejected, because the other was used");
|
|
|
|
hasProps(pc2.getTransceivers(),
|
|
[
|
|
{
|
|
mid: mid1,
|
|
sender: {track: track3},
|
|
currentDirection: "recvonly",
|
|
direction: "sendrecv"
|
|
}
|
|
]);
|
|
|
|
// Add _another_ track; this should reuse the disabled m-section
|
|
const stream4 = await getNoiseStream({audio: true});
|
|
t.add_cleanup(() => stopTracks(stream4));
|
|
const track4 = stream4.getAudioTracks()[0];
|
|
pc2.addTrack(track4, stream4);
|
|
offer = await pc2.createOffer();
|
|
await pc2.setLocalDescription(offer);
|
|
hasPropsAndUniqueMids(pc2.getTransceivers(),
|
|
[
|
|
{
|
|
mid: mid1
|
|
},
|
|
{
|
|
sender: {track: track4},
|
|
}
|
|
]);
|
|
|
|
// Fourth transceiver should have a new mid
|
|
assert_not_equals(pc2.getTransceivers()[1].mid, stoppedMid0);
|
|
assert_not_equals(pc2.getTransceivers()[1].mid, stoppedMid1);
|
|
|
|
assert_equals(offer.sdp.match(/m=/g).length, 2,
|
|
"Exactly 2 m-lines in offer, because m-section was reused");
|
|
assert_equals(offer.sdp.match(/m=audio 0 /g), null,
|
|
"No rejected m-line, because it was reused");
|
|
};
|
|
|
|
const checkStopAfterCreateOfferWithReusedMsection = async t => {
|
|
const pc1 = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc1.close());
|
|
const pc2 = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc2.close());
|
|
|
|
const stream = await getNoiseStream({audio: true, video: true});
|
|
t.add_cleanup(() => stopTracks(stream));
|
|
const audio = stream.getAudioTracks()[0];
|
|
const video = stream.getVideoTracks()[0];
|
|
|
|
pc1.addTrack(audio, stream);
|
|
pc1.addTrack(video, stream);
|
|
|
|
await offerAnswer(pc1, pc2);
|
|
pc1.getTransceivers()[1].stop();
|
|
await offerAnswer(pc1, pc2);
|
|
|
|
// Second (video) m-section has been negotiated disabled.
|
|
const transceiver = pc1.addTransceiver("video");
|
|
const offer = await pc1.createOffer();
|
|
transceiver.stop();
|
|
await pc1.setLocalDescription(offer);
|
|
await pc2.setRemoteDescription(offer);
|
|
|
|
const answer = await pc2.createAnswer();
|
|
await pc2.setLocalDescription(answer);
|
|
await pc1.setRemoteDescription(answer);
|
|
};
|
|
|
|
const checkAddIceCandidateToStoppedTransceiver = async t => {
|
|
const pc1 = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc1.close());
|
|
const pc2 = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc2.close());
|
|
|
|
const stream = await getNoiseStream({audio: true, video: true});
|
|
t.add_cleanup(() => stopTracks(stream));
|
|
const audio = stream.getAudioTracks()[0];
|
|
const video = stream.getVideoTracks()[0];
|
|
|
|
pc1.addTrack(audio, stream);
|
|
pc1.addTrack(video, stream);
|
|
|
|
pc2.addTrack(audio, stream);
|
|
pc2.addTrack(video, stream);
|
|
|
|
await pc1.setLocalDescription(await pc1.createOffer());
|
|
pc1.getTransceivers()[1].stop();
|
|
pc1.setLocalDescription({type: "rollback"});
|
|
|
|
const offer = await pc2.createOffer();
|
|
await pc2.setLocalDescription(offer);
|
|
await pc1.setRemoteDescription(offer);
|
|
|
|
await pc1.addIceCandidate(
|
|
{
|
|
candidate: "candidate:0 1 UDP 2122252543 192.168.1.112 64261 typ host",
|
|
sdpMid: pc2.getTransceivers()[1].mid
|
|
});
|
|
};
|
|
|
|
const tests = [
|
|
checkAddTransceiverNoTrack,
|
|
checkAddTransceiverWithTrack,
|
|
checkAddTransceiverWithAddTrack,
|
|
checkAddTransceiverWithDirection,
|
|
checkAddTransceiverWithSetRemoteOfferSending,
|
|
checkAddTransceiverWithSetRemoteOfferNoSend,
|
|
checkAddTransceiverBadKind,
|
|
checkNoMidOffer,
|
|
checkNoMidAnswer,
|
|
checkSetDirection,
|
|
checkCurrentDirection,
|
|
checkSendrecvWithNoSendTrack,
|
|
checkSendrecvWithTracklessStream,
|
|
checkAddTransceiverNoTrackDoesntPair,
|
|
checkAddTransceiverWithTrackDoesntPair,
|
|
checkAddTransceiverThenReplaceTrackDoesntPair,
|
|
checkAddTransceiverThenAddTrackPairs,
|
|
checkAddTrackPairs,
|
|
checkReplaceTrackNullDoesntPreventPairing,
|
|
checkRemoveAndReadd,
|
|
checkAddTrackExistingTransceiverThenRemove,
|
|
checkRemoveTrackNegotiation,
|
|
checkMute,
|
|
checkStop,
|
|
checkStopAfterCreateOffer,
|
|
checkStopAfterSetLocalOffer,
|
|
checkStopAfterSetRemoteOffer,
|
|
checkStopAfterCreateAnswer,
|
|
checkStopAfterSetLocalAnswer,
|
|
checkStopAfterClose,
|
|
checkLocalRollback,
|
|
checkRollbackAndSetRemoteOfferWithDifferentType,
|
|
checkRemoteRollback,
|
|
checkMsectionReuse,
|
|
checkStopAfterCreateOfferWithReusedMsection,
|
|
checkAddIceCandidateToStoppedTransceiver,
|
|
checkBundleTagRejected
|
|
].forEach(test => promise_test(test, test.name));
|
|
|
|
</script>
|