diff options
Diffstat (limited to 'testing/web-platform/tests/webrtc/RTCRtpTransceiver.https.html')
-rw-r--r-- | testing/web-platform/tests/webrtc/RTCRtpTransceiver.https.html | 2294 |
1 files changed, 2294 insertions, 0 deletions
diff --git a/testing/web-platform/tests/webrtc/RTCRtpTransceiver.https.html b/testing/web-platform/tests/webrtc/RTCRtpTransceiver.https.html new file mode 100644 index 0000000000..9a8fbedc23 --- /dev/null +++ b/testing/web-platform/tests/webrtc/RTCRtpTransceiver.https.html @@ -0,0 +1,2294 @@ +<!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); + 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 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> |