| <!doctype html> | |
| <meta charset=utf-8> | |
| <meta name="variant" content="?interop-2026"> | |
| <meta name="variant" content="?rest"> | |
| <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 rest = !new URLSearchParams(location.search).has("interop-2026"); | |
| const interop2026 = !new URLSearchParams(location.search).has("rest"); | |
| 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); | |
| 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 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 | |
| }); | |
| }; | |
| if (rest) { | |
| [ | |
| checkAddTransceiverNoTrack, | |
| checkAddTransceiverWithTrack, | |
| checkAddTransceiverWithAddTrack, | |
| checkAddTransceiverWithDirection, | |
| checkAddTransceiverWithSetRemoteOfferSending, | |
| checkAddTransceiverWithSetRemoteOfferNoSend, | |
| checkAddTransceiverBadKind, | |
| checkNoMidOffer, | |
| checkNoMidAnswer, | |
| checkSetDirection, | |
| checkSendrecvWithNoSendTrack, | |
| checkSendrecvWithTracklessStream, | |
| checkAddTransceiverNoTrackDoesntPair, | |
| checkAddTransceiverWithTrackDoesntPair, | |
| checkAddTransceiverThenReplaceTrackDoesntPair, | |
| checkAddTransceiverThenAddTrackPairs, | |
| checkAddTrackPairs, | |
| checkReplaceTrackNullDoesntPreventPairing, | |
| checkRemoveAndReadd, | |
| checkAddTrackExistingTransceiverThenRemove, | |
| checkRemoveTrackNegotiation, | |
| checkStopAfterClose, | |
| checkRollbackAndSetRemoteOfferWithDifferentType, | |
| checkStopAfterCreateOfferWithReusedMsection, | |
| checkAddIceCandidateToStoppedTransceiver, | |
| checkBundleTagRejected | |
| ].forEach(test => promise_test(test, test.name)); | |
| } | |
| if (interop2026) { | |
| [ | |
| checkCurrentDirection, | |
| checkLocalRollback, | |
| checkMsectionReuse, | |
| checkRemoteRollback, | |
| checkStop, | |
| checkStopAfterCreateAnswer, | |
| checkStopAfterCreateOffer, | |
| checkStopAfterSetLocalAnswer, | |
| checkStopAfterSetLocalOffer, | |
| checkStopAfterSetRemoteOffer, | |
| ].forEach(test => promise_test(test, test.name)); | |
| } | |
| ['audio', 'video'].forEach(kind => { | |
| const waitUntilTrackEventWithTimeout = (obj, name, t, timeout = 4000/*ms*/) => { | |
| return new Promise((resolve, reject) => { | |
| obj.addEventListener(name, resolve, {once: true}); | |
| t.step_timeout(() => reject('timeout'), timeout) | |
| }); | |
| } | |
| if (rest) { | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| t.add_cleanup(() => pc2.close()); | |
| pc1.addTransceiver(kind); | |
| pc2.ontrack = t.step_func(async e => { | |
| assert_true(e.track.muted, `track is muted in ontrack`); | |
| }); | |
| const offer = await pc1.createOffer(); | |
| await pc2.setRemoteDescription(offer); | |
| assert_true(pc2.getReceivers()[0].track.muted, `track is muted after SRD`); | |
| }, `track with ${kind} is muted after SRD`); | |
| } | |
| // audio is part of interop2026, video is not | |
| if ((kind == 'audio')? interop2026 : rest) { | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| t.add_cleanup(() => pc2.close()); | |
| let stream; | |
| if (kind === 'audio') { | |
| stream = await navigator.mediaDevices.getUserMedia({[kind]: true}); | |
| } else { | |
| stream = await getNoiseStream({[kind]: true}); | |
| } | |
| t.add_cleanup(() => stream.getTracks().forEach(t => t.stop())); | |
| pc1.addTrack(stream.getTracks()[0], stream); | |
| pc2.ontrack = t.step_func(async e => { | |
| assert_true(e.track.muted, `track is muted in ontrack`); | |
| }); | |
| exchangeIceCandidates(pc1, pc2); | |
| await exchangeOfferAnswer(pc1, pc2); | |
| await waitUntilTrackEventWithTimeout(pc2.getReceivers()[0].track, 'unmute', t); | |
| assert_false(pc2.getReceivers()[0].track.muted, `track is unmuted`); | |
| }, `track with ${kind} gets unmuted when packets flow.`); | |
| } | |
| if (rest) { | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| t.add_cleanup(() => pc2.close()); | |
| let stream; | |
| if (kind === 'audio') { | |
| stream = await navigator.mediaDevices.getUserMedia({[kind]: true}); | |
| } else { | |
| stream = await getNoiseStream({[kind]: true}); | |
| } | |
| t.add_cleanup(() => stream.getTracks().forEach(t => t.stop())); | |
| pc1.addTrack(stream.getTracks()[0], stream); | |
| pc2.ontrack = t.step_func(async e => { | |
| assert_true(e.track.muted, `track is muted in ontrack`); | |
| }); | |
| exchangeIceCandidates(pc1, pc2); | |
| await exchangeOfferAnswer(pc1, pc2); | |
| await waitUntilTrackEventWithTimeout(pc2.getReceivers()[0].track, 'unmute', t); | |
| assert_false(pc2.getReceivers()[0].track.muted, `track is unmuted`); | |
| pc1.getTransceivers()[0].direction = 'inactive'; | |
| // The mute event is queued in the success task for | |
| // pc2.setRemoteDescription(offer); checking after offer/answer finishes | |
| // might be too late and miss the event. | |
| await Promise.all([ | |
| exchangeOfferAnswer(pc1, pc2), | |
| waitUntilTrackEventWithTimeout(pc2.getReceivers()[0].track, 'mute', t)]); | |
| assert_true(pc2.getReceivers()[0].track.muted, `track is muted`); | |
| }, `track with ${kind} gets muted when setting the transceiver direction to 'inactive'`); | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| t.add_cleanup(() => pc2.close()); | |
| let stream; | |
| if (kind === 'audio') { | |
| stream = await navigator.mediaDevices.getUserMedia({[kind]: true}); | |
| } else { | |
| stream = await getNoiseStream({[kind]: true}); | |
| } | |
| t.add_cleanup(() => stream.getTracks().forEach(t => t.stop())); | |
| pc1.addTrack(stream.getTracks()[0], stream); | |
| pc2.ontrack = t.step_func(async e => { | |
| assert_true(e.track.muted, `track is muted in ontrack`); | |
| }); | |
| exchangeIceCandidates(pc1, pc2); | |
| await exchangeOfferAnswer(pc1, pc2); | |
| await waitUntilTrackEventWithTimeout(pc2.getReceivers()[0].track, 'unmute', t); | |
| assert_false(pc2.getReceivers()[0].track.muted, `track is unmuted`); | |
| pc1.getTransceivers()[0].direction = 'inactive'; | |
| // The mute event is queued in the success task for | |
| // pc2.setRemoteDescription(offer); checking after offer/answer finishes | |
| // might be too late and miss the event. | |
| await Promise.all([ | |
| exchangeOfferAnswer(pc1, pc2), | |
| waitUntilTrackEventWithTimeout(pc2.getReceivers()[0].track, 'mute', t)]); | |
| assert_true(pc2.getReceivers()[0].track.muted, `track is muted`); | |
| pc1.getTransceivers()[0].direction = 'sendrecv'; | |
| await exchangeOfferAnswer(pc1, pc2); | |
| await waitUntilTrackEventWithTimeout(pc2.getReceivers()[0].track, 'unmute', t); | |
| assert_false(pc2.getReceivers()[0].track.muted, `track is unmuted`); | |
| }, `track with ${kind} gets unmuted when setting the transceiver direction to 'sendrecv' after 'inactive'`); | |
| } | |
| // audio is part of interop2026, video is not | |
| if ((kind == 'audio')? interop2026 : rest) { | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| t.add_cleanup(() => pc2.close()); | |
| let stream; | |
| if (kind === 'audio') { | |
| stream = await navigator.mediaDevices.getUserMedia({[kind]: true}); | |
| } else { | |
| stream = await getNoiseStream({[kind]: true}); | |
| } | |
| t.add_cleanup(() => stream.getTracks().forEach(t => t.stop())); | |
| pc1.addTrack(stream.getTracks()[0], stream); | |
| pc2.ontrack = t.step_func(async e => { | |
| assert_true(e.track.muted, `track is muted in ontrack`); | |
| }); | |
| exchangeIceCandidates(pc1, pc2); | |
| await exchangeOfferAnswer(pc1, pc2); | |
| await waitUntilTrackEventWithTimeout(pc2.getReceivers()[0].track, 'unmute', t); | |
| assert_false(pc2.getReceivers()[0].track.muted, `track is unmuted`); | |
| pc1.getSenders()[0].replaceTrack(null); | |
| pc1.getTransceivers()[0].direction = 'inactive'; | |
| // The mute event is queued in the success task for | |
| // pc2.setRemoteDescription(offer); checking after offer/answer finishes | |
| // might be too late and miss the event. | |
| await Promise.all([ | |
| exchangeOfferAnswer(pc1, pc2), | |
| waitUntilTrackEventWithTimeout(pc2.getReceivers()[0].track, 'mute', t)]); | |
| assert_true(pc2.getReceivers()[0].track.muted, `track is muted`); | |
| pc1.getTransceivers()[0].direction = 'sendrecv'; | |
| await exchangeOfferAnswer(pc1, pc2); | |
| try { | |
| await waitUntilTrackEventWithTimeout(pc2.getReceivers()[0].track, 'unmute', t); | |
| assert_unreached('unmute event should not fire'); | |
| } catch (e) { | |
| assert_true(pc2.getReceivers()[0].track.muted, `track remains muted`); | |
| } | |
| }, `track with ${kind} stays muted when setting the transceiver direction to 'sendrecv' after 'inactive' but no packets flow`); | |
| } | |
| }); | |
| </script> |