"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. */ // Borrowed from wpt, with some dependencies removed. const ridExtensions = [ "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id", "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id", ]; function ridToMid(description, rids) { const sections = SDPUtils.splitSections(description.sdp); const dtls = SDPUtils.getDtlsParameters(sections[1], sections[0]); const ice = SDPUtils.getIceParameters(sections[1], sections[0]); const rtpParameters = SDPUtils.parseRtpParameters(sections[1]); const setupValue = description.sdp.match(/a=setup:(.*)/)[1]; const directionValue = description.sdp.match(/a=sendrecv|a=sendonly|a=recvonly|a=inactive/) || "a=sendrecv"; const mline = SDPUtils.parseMLine(sections[1]); // Skip mid extension; we are replacing it with the rid extmap rtpParameters.headerExtensions = rtpParameters.headerExtensions.filter( ext => ext.uri != "urn:ietf:params:rtp-hdrext:sdes:mid" ); for (const ext of rtpParameters.headerExtensions) { if (ext.uri == "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id") { ext.uri = "urn:ietf:params:rtp-hdrext:sdes:mid"; } } // 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" ); if (!rids) { rids = Array.from(description.sdp.matchAll(/a=rid:(.*) send/g)).map( r => r[1] ); } let sdp = SDPUtils.writeSessionBoilerplate() + SDPUtils.writeDtlsParameters(dtls, setupValue) + SDPUtils.writeIceParameters(ice) + "a=group:BUNDLE " + rids.join(" ") + "\r\n"; const baseRtpDescription = SDPUtils.writeRtpDescription( mline.kind, rtpParameters ); for (const rid of rids) { sdp += baseRtpDescription + "a=mid:" + rid + "\r\n" + "a=msid:rid-" + rid + " rid-" + rid + "\r\n"; sdp += directionValue + "\r\n"; } return sdp; } function midToRid(description, localDescription, rids) { const sections = SDPUtils.splitSections(description.sdp); const dtls = SDPUtils.getDtlsParameters(sections[1], sections[0]); const ice = SDPUtils.getIceParameters(sections[1], sections[0]); const rtpParameters = SDPUtils.parseRtpParameters(sections[1]); const setupValue = description.sdp.match(/a=setup:(.*)/)[1]; const directionValue = description.sdp.match(/a=sendrecv|a=sendonly|a=recvonly|a=inactive/) || "a=sendrecv"; const mline = SDPUtils.parseMLine(sections[1]); // Skip rid extensions; we are replacing them with the mid extmap rtpParameters.headerExtensions = rtpParameters.headerExtensions.filter( ext => !ridExtensions.includes(ext.uri) ); for (const ext of rtpParameters.headerExtensions) { if (ext.uri == "urn:ietf:params:rtp-hdrext:sdes:mid") { ext.uri = "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id"; } } const localMid = localDescription ? SDPUtils.getMid(SDPUtils.splitSections(localDescription.sdp)[1]) : "0"; if (!rids) { rids = []; for (let i = 1; i < sections.length; i++) { rids.push(SDPUtils.getMid(sections[i])); } } let sdp = SDPUtils.writeSessionBoilerplate() + SDPUtils.writeDtlsParameters(dtls, setupValue) + SDPUtils.writeIceParameters(ice) + "a=group:BUNDLE " + localMid + "\r\n"; sdp += SDPUtils.writeRtpDescription(mline.kind, rtpParameters); // Although we are converting mids to rids, we still need a mid. // The first one will be consistent with trickle ICE candidates. sdp += "a=mid:" + localMid + "\r\n"; sdp += directionValue + "\r\n"; for (const rid of rids) { const stringrid = String(rid); // allow integers const choices = stringrid.split(","); choices.forEach(choice => { sdp += "a=rid:" + choice + " recv\r\n"; }); } if (rids.length) { sdp += "a=simulcast:recv " + rids.join(";") + "\r\n"; } return sdp; } async function doOfferToSendSimulcast(offerer, answerer) { await offerer.setLocalDescription(); // Is this a renegotiation? If so, we cannot remove (or reorder!) any mids, // even if some rids have been removed or reordered. let mids = []; if (answerer.localDescription) { // Renegotiation. Mids must be the same as before, because renegotiation // can never remove or reorder mids, nor can it expand the simulcast // envelope. mids = [...answerer.localDescription.sdp.matchAll(/a=mid:(.*)/g)].map( e => e[1] ); } else { // First negotiation; the mids will be exactly the same as the rids const simulcastAttr = offerer.localDescription.sdp.match( /a=simulcast:send (.*)/ ); if (simulcastAttr) { mids = simulcastAttr[1].split(";"); } } const nonSimulcastOffer = ridToMid(offerer.localDescription, mids); await answerer.setRemoteDescription({ type: "offer", sdp: nonSimulcastOffer, }); } async function doAnswerToRecvSimulcast(offerer, answerer, rids) { await answerer.setLocalDescription(); const simulcastAnswer = midToRid( answerer.localDescription, offerer.localDescription, rids ); await offerer.setRemoteDescription({ type: "answer", sdp: simulcastAnswer }); } async function doOfferToRecvSimulcast(offerer, answerer, rids) { await offerer.setLocalDescription(); const simulcastOffer = midToRid( offerer.localDescription, answerer.localDescription, rids ); await answerer.setRemoteDescription({ type: "offer", sdp: simulcastOffer }); } async function doAnswerToSendSimulcast(offerer, answerer) { await answerer.setLocalDescription(); // See which mids the offerer had; it will barf if we remove or reorder them const mids = [...offerer.localDescription.sdp.matchAll(/a=mid:(.*)/g)].map( e => e[1] ); const nonSimulcastAnswer = ridToMid(answerer.localDescription, mids); await offerer.setRemoteDescription({ type: "answer", sdp: nonSimulcastAnswer, }); } async function doOfferToSendSimulcastAndAnswer(offerer, answerer, rids) { await doOfferToSendSimulcast(offerer, answerer); await doAnswerToRecvSimulcast(offerer, answerer, rids); } async function doOfferToRecvSimulcastAndAnswer(offerer, answerer, rids) { await doOfferToRecvSimulcast(offerer, answerer, rids); await doAnswerToSendSimulcast(offerer, answerer); } // This would be useful for cases other than simulcast, but we do not use it // anywhere else right now, nor do we have a place for wpt-friendly helpers at // the moment. function createPlaybackElement(track) { const elem = document.createElement(track.kind); elem.autoplay = true; elem.srcObject = new MediaStream([track]); elem.id = track.id; return elem; } async function getPlaybackWithLoadedMetadata(track) { const elem = createPlaybackElement(track); return new Promise(resolve => { elem.addEventListener("loadedmetadata", () => { resolve(elem); }); }); }