'use strict'; /* Helper functions to munge SDP and split the sending track into * separate tracks on the receiving end. This can be done in a number * of ways, the one used here uses the fact that the MID and RID header * extensions which are used for packet routing share the same wire * format. The receiver interprets the rids from the sender as mids * which allows receiving the different spatial resolutions on separate * m-lines and tracks. */ const extensionsToFilter = [ 'urn:ietf:params:rtp-hdrext:sdes:mid', 'urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id', 'urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id', ]; function swapRidAndMidExtensionsInSimulcastOffer(offer, rids) { const sections = SDPUtils.splitSections(offer.sdp); const dtls = SDPUtils.getDtlsParameters(sections[1], sections[0]); const ice = SDPUtils.getIceParameters(sections[1], sections[0]); const rtpParameters = SDPUtils.parseRtpParameters(sections[1]); // The gist of this hack is that rid and mid have the same wire format. const rid = rtpParameters.headerExtensions.find(ext => ext.uri === 'urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id'); rtpParameters.headerExtensions = rtpParameters.headerExtensions.filter(ext => { return !extensionsToFilter.includes(ext.uri); }); // This tells the other side that the RID packets are actually mids. rtpParameters.headerExtensions.push({id: rid.id, uri: 'urn:ietf:params:rtp-hdrext:sdes:mid', direction: 'sendrecv'}); // Filter rtx as we have no way to (re)interpret rrid. // Not doing this makes probing use RTX, it's not understood and ramp-up is slower. rtpParameters.codecs = rtpParameters.codecs.filter(c => c.name.toUpperCase() !== 'RTX'); let sdp = SDPUtils.writeSessionBoilerplate() + SDPUtils.writeDtlsParameters(dtls, 'actpass') + SDPUtils.writeIceParameters(ice) + 'a=group:BUNDLE ' + rids.join(' ') + '\r\n'; const baseRtpDescription = SDPUtils.writeRtpDescription('video', rtpParameters); rids.forEach(rid => { sdp += baseRtpDescription + 'a=mid:' + rid + '\r\n' + 'a=msid:rid-' + rid + ' rid-' + rid + '\r\n'; }); return sdp; } function swapRidAndMidExtensionsInSimulcastAnswer(answer, localDescription, rids) { const sections = SDPUtils.splitSections(answer.sdp); const dtls = SDPUtils.getDtlsParameters(sections[1], sections[0]); const ice = SDPUtils.getIceParameters(sections[1], sections[0]); const rtpParameters = SDPUtils.parseRtpParameters(sections[1]); rtpParameters.headerExtensions = rtpParameters.headerExtensions.filter(ext => { return !extensionsToFilter.includes(ext.uri); }); const localMid = SDPUtils.getMid(SDPUtils.splitSections(localDescription.sdp)[1]); let sdp = SDPUtils.writeSessionBoilerplate() + SDPUtils.writeDtlsParameters(dtls, 'active') + SDPUtils.writeIceParameters(ice) + 'a=group:BUNDLE ' + localMid + '\r\n'; sdp += SDPUtils.writeRtpDescription('video', rtpParameters); sdp += 'a=mid:' + localMid + '\r\n'; rids.forEach(rid => { sdp += 'a=rid:' + rid + ' recv\r\n'; }); sdp += 'a=simulcast:recv ' + rids.join(';') + '\r\n'; // Re-add headerextensions we filtered. const headerExtensions = SDPUtils.parseRtpParameters(SDPUtils.splitSections(localDescription.sdp)[1]).headerExtensions; headerExtensions.forEach(ext => { if (extensionsToFilter.includes(ext.uri)) { sdp += 'a=extmap:' + ext.id + ' ' + ext.uri + '\r\n'; } }); return sdp; } async function negotiateSimulcastAndWaitForVideo(t, rids, pc1, pc2, codec) { exchangeIceCandidates(pc1, pc2); const metadataToBeLoaded = []; pc2.ontrack = (e) => { const stream = e.streams[0]; const v = document.createElement('video'); v.autoplay = true; v.srcObject = stream; v.id = stream.id metadataToBeLoaded.push(new Promise((resolve) => { v.addEventListener('loadedmetadata', () => { resolve(); }); })); }; // Use getUserMedia as getNoiseStream does not have enough entropy to ramp-up. const stream = await navigator.mediaDevices.getUserMedia({video: {width: 1280, height: 720}}); t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); const transceiver = pc1.addTransceiver(stream.getVideoTracks()[0], { streams: [stream], sendEncodings: rids.map(rid => ({rid})), }); if (codec) { preferCodec(transceiver, codec.mimeType, codec.sdpFmtpLine); } const offer = await pc1.createOffer(); await pc1.setLocalDescription(offer), await pc2.setRemoteDescription({ type: 'offer', sdp: swapRidAndMidExtensionsInSimulcastOffer(offer, rids), }); const answer = await pc2.createAnswer(); await pc2.setLocalDescription(answer); await pc1.setRemoteDescription({ type: 'answer', sdp: swapRidAndMidExtensionsInSimulcastAnswer(answer, pc1.localDescription, rids), }); assert_equals(metadataToBeLoaded.length, rids.length); return Promise.all(metadataToBeLoaded); }