'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 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 = SDPUtils.matchPrefix(description.sdp, 'a=setup:')[0].substring(8); const direction = SDPUtils.getDirection(sections[1]); 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 = SDPUtils.matchPrefix(sections[1], 'a=rid:') .filter(line => line.endsWith(' send')) .map(line => line.substring(6).split(' ')[0]); } let sdp = SDPUtils.writeSessionBoilerplate() + SDPUtils.writeDtlsParameters(dtls, setupValue) + SDPUtils.writeIceParameters(ice) + 'a=group:BUNDLE ' + rids.join(' ') + '\r\n' + 'a=msid-semantic: WMS *\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 += 'a=' + direction + '\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 direction = SDPUtils.getDirection(sections[1]); 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 (localDescription) { const localVideoSection = SDPUtils.splitSections(localDescription.sdp)[1]; const localParameters = SDPUtils.parseRtpParameters(localVideoSection); const localMidExtension = localParameters.headerExtensions .find(ext => ext.uri === 'urn:ietf:params:rtp-hdrext:sdes:mid'); if (localMidExtension) { rtpParameters.headerExtensions.push(localMidExtension); } } else { // Find unused id in remote description to formally have a mid. for (let id = 1; id < 15; id++) { if (rtpParameters.headerExtensions.find(ext => ext.id === id) === undefined) { rtpParameters.headerExtensions.push( {id, uri: 'urn:ietf:params:rtp-hdrext:sdes:mid'}); break; } } } 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' + 'a=msid-semantic: WMS *\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 += 'a=' + direction + '\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. const sections = SDPUtils.splitSections(answerer.localDescription.sdp); sections.shift(); mids = sections.map(section => SDPUtils.getMid(section)); } else { // First negotiation; the mids will be exactly the same as the rids const simulcastAttr = SDPUtils.matchPrefix(offerer.localDescription.sdp, 'a=simulcast:send ')[0]; if (simulcastAttr) { mids = simulcastAttr.split(' ')[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 sections = SDPUtils.splitSections(offerer.localDescription.sdp); sections.shift(); const mids = sections.map(section => SDPUtils.getMid(section)); let nonSimulcastAnswer = ridToMid(answerer.localDescription, mids); // Restore MID RTP header extension. const localParameters = SDPUtils.parseRtpParameters(sections[0]); const localMidExtension = localParameters.headerExtensions .find(ext => ext.uri === 'urn:ietf:params:rtp-hdrext:sdes:mid'); if (localMidExtension) { nonSimulcastAnswer += SDPUtils.writeExtmap(localMidExtension); } 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); } function swapRidAndMidExtensionsInSimulcastOffer(offer, rids) { return ridToMid(offer, rids); } function swapRidAndMidExtensionsInSimulcastAnswer(answer, localDescription, rids) { return midToRid(answer, localDescription, rids); } async function negotiateSimulcastAndWaitForVideo( t, rids, pc1, pc2, codec, scalabilityMode = undefined) { 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(); }); })); }; const sendEncodings = rids.map(rid => ({rid})); // Use a 2X downscale factor between each layer. To improve ramp-up time, the // top layer is scaled down by a factor 2. Smaller layer comes first. For // example if MediaStreamTrack is 720p and we want to send three layers we'll // get {90p, 180p, 360p}. let scaleResolutionDownBy = 2; for (let i = sendEncodings.length - 1; i >= 0; --i) { if (scalabilityMode) { sendEncodings[i].scalabilityMode = scalabilityMode; } sendEncodings[i].scaleResolutionDownBy = scaleResolutionDownBy; scaleResolutionDownBy *= 2; } // Use getUserMedia as getNoiseStream does not have enough entropy to ramp-up. await setMediaPermission(); 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: sendEncodings, }); 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); }