<!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>