blob: aba2796489603b9798832a64f08988e8507fa29a [file] [log] [blame]
<!doctype html>
<meta charset=utf-8>
<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 => {
if (e.candidate) {
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,
stopped: false
},
{
receiver: {track: {kind: "video", readyState: "live", muted: true}},
sender: {track: null},
direction: "sendrecv",
mid: null,
currentDirection: null,
stopped: false
}
]);
};
const checkAddTransceiverWithTrack = async t => {
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
const stream = await navigator.mediaDevices.getUserMedia({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,
stopped: false
},
{
receiver: {track: {kind: "video"}},
sender: {track: video},
direction: "sendrecv",
mid: null,
currentDirection: null,
stopped: false
}
]);
};
const checkAddTransceiverWithAddTrack = async t => {
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
const stream = await navigator.mediaDevices.getUserMedia({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,
stopped: false
},
{
receiver: {track: {kind: "video"}},
sender: {track: video},
direction: "sendrecv",
mid: null,
currentDirection: null,
stopped: false
}
]);
};
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,
stopped: false
},
{
receiver: {track: {kind: "video"}},
sender: {track: null},
direction: "recvonly",
mid: null,
currentDirection: null,
stopped: false
}
]);
};
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 navigator.mediaDevices.getUserMedia({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,
stopped: false
}
]);
};
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 navigator.mediaDevices.getUserMedia({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,
stopped: false
}
]);
};
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 checkMsidNoTrackId = async t => {
const pc1 = new RTCPeerConnection();
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
t.add_cleanup(() => pc2.close());
const stream = await navigator.mediaDevices.getUserMedia({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 track-id from msid
offer.sdp = offer.sdp.replace(/(a=msid:[^ \t]+).*\r\n/g, "$1\r\n");
assert_true(offer.sdp.includes(`a=msid:${stream.id}\r\n`));
await pc2.setRemoteDescription(offer);
const answer = await pc2.createAnswer();
await pc1.setRemoteDescription(answer);
await pc2.setLocalDescription(answer);
};
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 navigator.mediaDevices.getUserMedia({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:");
await pc2.setRemoteDescription(offer);
hasPropsAndUniqueMids(pc2.getTransceivers(),
[
{
receiver: {track: {kind: "audio"}},
sender: {track: null},
direction: "recvonly",
currentDirection: null,
stopped: false
}
]);
const answer = await pc2.createAnswer();
await pc2.setLocalDescription(answer);
await pc1.setRemoteDescription(answer);
};
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 navigator.mediaDevices.getUserMedia({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 navigator.mediaDevices.getUserMedia({audio: true});
t.add_cleanup(() => stopTracks(stream));
const track = stream.getAudioTracks()[0];
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 navigator.mediaDevices.getUserMedia({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 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 navigator.mediaDevices.getUserMedia({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 navigator.mediaDevices.getUserMedia({audio: true});
t.add_cleanup(() => stopTracks(stream));
const track = stream.getAudioTracks()[0];
pc2.addTrack(track, stream);
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 navigator.mediaDevices.getUserMedia({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 navigator.mediaDevices.getUserMedia({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 navigator.mediaDevices.getUserMedia({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 navigator.mediaDevices.getUserMedia({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"}]);
};
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 navigator.mediaDevices.getUserMedia({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 navigator.mediaDevices.getUserMedia({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 navigator.mediaDevices.getUserMedia({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);
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);
await pc1.setLocalDescription(offer);
answer = await pc2.createAnswer();
gotUnmuteAudio1 = gotUnmuteEvent(pc1.getTransceivers()[0].receiver.track);
gotUnmuteVideo1 = gotUnmuteEvent(pc1.getTransceivers()[1].receiver.track);
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 navigator.mediaDevices.getUserMedia({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();
await onended;
hasPropsAndUniqueMids(pc1.getTransceivers(),
[
{
sender: {track: {kind: "audio"}},
receiver: {track: {kind: "audio", readyState: "ended"}},
stopped: true,
currentDirection: null,
direction: "sendrecv"
}
]);
const transceiver = pc1.getTransceivers()[0];
checkThrows(() => transceiver.sender.setParameters(
transceiver.sender.getParameters()),
"InvalidStateError", "setParameters on stopped transceiver");
const stream2 = await navigator.mediaDevices.getUserMedia({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);
stoppedTransceiver = pc2.getTransceivers()[0];
onended = new Promise(resolve => {
stoppedTransceiver.receiver.track.onended = resolve;
});
await pc2.setRemoteDescription(offer);
await onended;
hasProps(pc2.getTransceivers(),
[
{
sender: {track: {kind: "audio"}},
receiver: {track: {kind: "audio", readyState: "ended"}},
stopped: true,
mid: null,
currentDirection: null,
direction: "sendrecv"
}
]);
// Shouldn't throw either
stoppedTransceiver.stop();
pc1.close();
pc2.close();
// Still shouldn't throw
stoppedTransceiver.stop();
};
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 navigator.mediaDevices.getUserMedia({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();
pc1.getTransceivers()[0].stop();
await pc2.setRemoteDescription(offer)
trickle(t, pc1, pc2);
await pc1.setLocalDescription(offer);
let answer = await pc2.createAnswer();
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(),
[
{
stopped: true,
}
]);
trickle(t, pc2, pc1);
await pc2.setLocalDescription(answer);
await negotiationNeeded(pc1);
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);
hasProps(pc1.getTransceivers(),
[
{
stopped: true,
mid: null
}
]);
hasProps(pc2.getTransceivers(),
[
{
stopped: true,
mid: null
}
]);
};
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 navigator.mediaDevices.getUserMedia({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 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(),
[
{
stopped: true,
}
]);
await negotiationNeeded(pc1);
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);
hasProps(pc1.getTransceivers(),
[
{
stopped: true,
mid: null
}
]);
hasProps(pc2.getTransceivers(),
[
{
stopped: true,
mid: null
}
]);
};
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 navigator.mediaDevices.getUserMedia({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 take effect in answer.
pc2.getTransceivers()[0].stop();
const answer = await pc2.createAnswer();
const trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
hasProps(trackEvents, []);
hasProps(pc1.getTransceivers(),
[
{
stopped: true,
mid: null
}
]);
await pc2.setLocalDescription(answer);
};
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 navigator.mediaDevices.getUserMedia({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(),
[
{
stopped: true,
}
]);
trickle(t, pc2, pc1);
await pc2.setLocalDescription(answer);
await negotiationNeeded(pc2);
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);
hasProps(pc1.getTransceivers(),
[
{
stopped: true,
mid: null
}
]);
hasProps(pc2.getTransceivers(),
[
{
stopped: true,
mid: null
}
]);
};
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 navigator.mediaDevices.getUserMedia({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(),
[
{
stopped: true,
}
]);
await negotiationNeeded(pc2);
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);
hasProps(pc1.getTransceivers(),
[
{
stopped: true,
mid: null
}
]);
hasProps(pc2.getTransceivers(),
[
{
stopped: true,
mid: null
}
]);
};
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 navigator.mediaDevices.getUserMedia({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 navigator.mediaDevices.getUserMedia({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,
stopped: false
}
]);
// Verify that rollback doesn't stomp things it should not
pc.getTransceivers()[0].direction = "sendonly";
const stream2 = await navigator.mediaDevices.getUserMedia({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,
stopped: false
}
]);
// 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(), [{ stopped: true }]);
};
const checkRollbackAndSetRemoteOfferWithDifferentType = async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const audioStream = await navigator.mediaDevices.getUserMedia({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 navigator.mediaDevices.getUserMedia({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,
stopped: false
}
]);
hasProps(pc2.getTransceivers(),
[
{
receiver: {track: {kind: "video"}},
sender: {track: videoTrack},
direction: "sendrecv",
mid: null,
currentDirection: null,
stopped: false
}
]);
await offerAnswer(pc2, pc1);
hasPropsAndUniqueMids(pc1.getTransceivers(),
[
{
receiver: {track: {kind: "audio"}},
sender: {track: audioTrack},
direction: "sendrecv",
mid: null,
currentDirection: null,
stopped: false
},
{
receiver: {track: {kind: "video"}},
sender: {track: null},
direction: "recvonly",
currentDirection: "recvonly",
stopped: false
}
]);
hasPropsAndUniqueMids(pc2.getTransceivers(),
[
{
receiver: {track: {kind: "video"}},
sender: {track: videoTrack},
direction: "sendrecv",
currentDirection: "sendonly",
stopped: false
}
]);
await offerAnswer(pc1, pc2);
};
const checkRemoteRollback = async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const stream = await navigator.mediaDevices.getUserMedia({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,
{
stopped: true,
mid: null,
currentDirection: null
}
);
await onended;
hasProps(removedTransceiver,
{
receiver: {track: {readyState: "ended"}},
stopped: true,
mid: null,
currentDirection: null
}
);
// 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,
stopped: false
}
]);
const mid0 = pc2.getTransceivers()[0].mid;
// Give pc2 a track with replaceTrack
const stream2 = await navigator.mediaDevices.getUserMedia({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,
stopped: false
}
]);
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,
stopped: false
}
]);
// 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,
stopped: false
}
]);
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,
stopped: false
}
]);
// 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);
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);
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",
stopped: false
}
]);
// 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"});
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: "sendrecv",
mid: null,
currentDirection: null,
stopped: true
}
]);
// stop() cannot be rolled back!
await pc2.setRemoteDescription({type: "rollback"});
hasProps(pc2.getTransceivers(),
[
{
receiver: {track: {kind: "audio"}},
sender: {track: {kind: "audio"}},
direction: "sendrecv",
mid: null,
currentDirection: null,
stopped: true
}
]);
};
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 navigator.mediaDevices.getUserMedia({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);
await pc2.setRemoteDescription(offer);
// answerer stops transceiver to reject m-section
const stoppedMid0 = pc2.getTransceivers()[0].mid;
pc2.getTransceivers()[0].stop();
let answer = await pc2.createAnswer();
await pc2.setLocalDescription(answer);
await pc1.setRemoteDescription(answer);
hasProps(pc1.getTransceivers(),
[
{
mid: null,
currentDirection: null,
stopped: true
}
]);
hasProps(pc2.getTransceivers(),
[
{
mid: null,
currentDirection: null,
stopped: true
}
]);
// Check that m-section is reused on both ends
const stream2 = await navigator.mediaDevices.getUserMedia({audio: true});
t.add_cleanup(() => stopTracks(stream2));
const track2 = stream2.getAudioTracks()[0];
pc1.addTrack(track2, stream2);
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(),
[
{
stopped: true
},
{
sender: {track: track2}
}
]);
assert_not_equals(pc1.getTransceivers()[1].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(),
[
{
stopped: true
},
{
sender: {track}
}
]);
assert_not_equals(pc2.getTransceivers()[1].mid, stoppedMid0);
await pc2.setLocalDescription(offer);
await pc1.setRemoteDescription(offer);
answer = await pc1.createAnswer();
await pc1.setLocalDescription(answer);
await pc2.setRemoteDescription(answer);
hasPropsAndUniqueMids(pc1.getTransceivers(),
[
{
mid: null
},
{
sender: {track: track2},
currentDirection: "sendrecv"
}
]);
const mid0 = pc1.getTransceivers()[1].mid;
hasProps(pc2.getTransceivers(),
[
{
mid: null
},
{
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()[1].mid;
pc1.getTransceivers()[1].stop();
const stream3 = await navigator.mediaDevices.getUserMedia({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()[2].receiver.track,
streams: [{id: stream3.id}]
}
]);
answer = await pc2.createAnswer();
await pc2.setLocalDescription(answer);
trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
hasProps(trackEvents, []);
hasPropsAndUniqueMids(pc2.getTransceivers(),
[
{
mid: null
},
{
mid: null,
stopped: true
},
{
sender: {track: null},
currentDirection: "recvonly"
}
]);
// Verify that we don't reuse the mid from the stopped transceiver
const mid1 = pc2.getTransceivers()[2].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(),
[
{},
{
stopped: true
},
{
mid: mid1,
sender: {track: track3},
currentDirection: "recvonly",
direction: "sendrecv"
}
]);
// Add _another_ track; this should reuse the disabled m-section
const stream4 = await navigator.mediaDevices.getUserMedia({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: null
},
{
mid: null
},
{
mid: mid1
},
{
sender: {track: track4},
}
]);
// Fourth transceiver should have a new mid
assert_not_equals(pc2.getTransceivers()[3].mid, stoppedMid0);
assert_not_equals(pc2.getTransceivers()[3].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 tests = [
checkAddTransceiverNoTrack,
checkAddTransceiverWithTrack,
checkAddTransceiverWithAddTrack,
checkAddTransceiverWithDirection,
checkMsidNoTrackId,
checkAddTransceiverWithSetRemoteOfferSending,
checkAddTransceiverWithSetRemoteOfferNoSend,
checkAddTransceiverBadKind,
checkNoMidOffer,
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
].forEach(test => promise_test(test, test.name));
</script>