simulcast.js (10416B)
1 'use strict'; 2 /* Helper functions to munge SDP and split the sending track into 3 * separate tracks on the receiving end. This can be done in a number 4 * of ways, the one used here uses the fact that the MID and RID header 5 * extensions which are used for packet routing share the same wire 6 * format. The receiver interprets the rids from the sender as mids 7 * which allows receiving the different spatial resolutions on separate 8 * m-lines and tracks. 9 */ 10 11 const ridExtensions = [ 12 'urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id', 13 'urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id', 14 ]; 15 16 function ridToMid(description, rids) { 17 const sections = SDPUtils.splitSections(description.sdp); 18 const dtls = SDPUtils.getDtlsParameters(sections[1], sections[0]); 19 const ice = SDPUtils.getIceParameters(sections[1], sections[0]); 20 const rtpParameters = SDPUtils.parseRtpParameters(sections[1]); 21 const setupValue = SDPUtils.matchPrefix(description.sdp, 'a=setup:')[0].substring(8); 22 const direction = SDPUtils.getDirection(sections[1]); 23 const mline = SDPUtils.parseMLine(sections[1]); 24 25 // Skip mid extension; we are replacing it with the rid extmap 26 rtpParameters.headerExtensions = rtpParameters.headerExtensions.filter( 27 ext => ext.uri !== 'urn:ietf:params:rtp-hdrext:sdes:mid' 28 ); 29 30 for (const ext of rtpParameters.headerExtensions) { 31 if (ext.uri === 'urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id') { 32 ext.uri = 'urn:ietf:params:rtp-hdrext:sdes:mid'; 33 } 34 } 35 36 // Filter rtx as we have no way to (re)interpret rrid. 37 // Not doing this makes probing use RTX, it's not understood and ramp-up is slower. 38 rtpParameters.codecs = rtpParameters.codecs.filter(c => c.name.toUpperCase() !== 'RTX'); 39 if (!rids) { 40 rids = SDPUtils.matchPrefix(sections[1], 'a=rid:') 41 .filter(line => line.endsWith(' send')) 42 .map(line => line.substring(6).split(' ')[0]); 43 } 44 45 let sdp = SDPUtils.writeSessionBoilerplate() + 46 SDPUtils.writeDtlsParameters(dtls, setupValue) + 47 SDPUtils.writeIceParameters(ice) + 48 'a=group:BUNDLE ' + rids.join(' ') + '\r\n' + 49 'a=msid-semantic: WMS *\r\n'; 50 const baseRtpDescription = SDPUtils.writeRtpDescription(mline.kind, rtpParameters); 51 for (const rid of rids) { 52 sdp += baseRtpDescription + 53 'a=mid:' + rid + '\r\n' + 54 'a=msid:rid-' + rid + ' rid-' + rid + '\r\n'; 55 sdp += 'a=' + direction + '\r\n'; 56 } 57 return sdp; 58 } 59 60 function midToRid(description, localDescription, rids) { 61 const sections = SDPUtils.splitSections(description.sdp); 62 const dtls = SDPUtils.getDtlsParameters(sections[1], sections[0]); 63 const ice = SDPUtils.getIceParameters(sections[1], sections[0]); 64 const rtpParameters = SDPUtils.parseRtpParameters(sections[1]); 65 const setupValue = description.sdp.match(/a=setup:(.*)/)[1]; 66 const direction = SDPUtils.getDirection(sections[1]); 67 const mline = SDPUtils.parseMLine(sections[1]); 68 69 // Skip rid extensions; we are replacing them with the mid extmap 70 rtpParameters.headerExtensions = rtpParameters.headerExtensions.filter( 71 ext => !ridExtensions.includes(ext.uri) 72 ); 73 74 for (const ext of rtpParameters.headerExtensions) { 75 if (ext.uri === 'urn:ietf:params:rtp-hdrext:sdes:mid') { 76 ext.uri = 'urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id'; 77 } 78 } 79 80 const localMid = localDescription ? SDPUtils.getMid(SDPUtils.splitSections(localDescription.sdp)[1]) : '0'; 81 if (localDescription) { 82 const localVideoSection = SDPUtils.splitSections(localDescription.sdp)[1]; 83 const localParameters = SDPUtils.parseRtpParameters(localVideoSection); 84 85 const localMidExtension = localParameters.headerExtensions 86 .find(ext => ext.uri === 'urn:ietf:params:rtp-hdrext:sdes:mid'); 87 if (localMidExtension) { 88 rtpParameters.headerExtensions.push(localMidExtension); 89 } 90 } else { 91 // Find unused id in remote description to formally have a mid. 92 for (let id = 1; id < 15; id++) { 93 if (rtpParameters.headerExtensions.find(ext => ext.id === id) === undefined) { 94 rtpParameters.headerExtensions.push( 95 {id, uri: 'urn:ietf:params:rtp-hdrext:sdes:mid'}); 96 break; 97 } 98 } 99 } 100 101 if (!rids) { 102 rids = []; 103 for (let i = 1; i < sections.length; i++) { 104 rids.push(SDPUtils.getMid(sections[i])); 105 } 106 } 107 108 let sdp = SDPUtils.writeSessionBoilerplate() + 109 SDPUtils.writeDtlsParameters(dtls, setupValue) + 110 SDPUtils.writeIceParameters(ice) + 111 'a=group:BUNDLE ' + localMid + '\r\n' + 112 'a=msid-semantic: WMS *\r\n'; 113 sdp += SDPUtils.writeRtpDescription(mline.kind, rtpParameters); 114 // Although we are converting mids to rids, we still need a mid. 115 // The first one will be consistent with trickle ICE candidates. 116 sdp += 'a=mid:' + localMid + '\r\n'; 117 sdp += 'a=' + direction + '\r\n'; 118 119 for (const rid of rids) { 120 const stringrid = String(rid); // allow integers 121 const choices = stringrid.split(','); 122 choices.forEach(choice => { 123 sdp += 'a=rid:' + choice + ' recv\r\n'; 124 }); 125 } 126 if (rids.length) { 127 sdp += 'a=simulcast:recv ' + rids.join(';') + '\r\n'; 128 } 129 130 return sdp; 131 } 132 133 async function doOfferToSendSimulcast(offerer, answerer) { 134 await offerer.setLocalDescription(); 135 136 // Is this a renegotiation? If so, we cannot remove (or reorder!) any mids, 137 // even if some rids have been removed or reordered. 138 let mids = []; 139 if (answerer.localDescription) { 140 // Renegotiation. Mids must be the same as before, because renegotiation 141 // can never remove or reorder mids, nor can it expand the simulcast 142 // envelope. 143 const sections = SDPUtils.splitSections(answerer.localDescription.sdp); 144 sections.shift(); 145 mids = sections.map(section => SDPUtils.getMid(section)); 146 } else { 147 // First negotiation; the mids will be exactly the same as the rids 148 const simulcastAttr = SDPUtils.matchPrefix(offerer.localDescription.sdp, 149 'a=simulcast:send ')[0]; 150 if (simulcastAttr) { 151 mids = simulcastAttr.split(' ')[1].split(';'); 152 } 153 } 154 155 const nonSimulcastOffer = ridToMid(offerer.localDescription, mids); 156 await answerer.setRemoteDescription({ 157 type: 'offer', 158 sdp: nonSimulcastOffer, 159 }); 160 } 161 162 async function doAnswerToRecvSimulcast(offerer, answerer, rids) { 163 await answerer.setLocalDescription(); 164 const simulcastAnswer = midToRid( 165 answerer.localDescription, 166 offerer.localDescription, 167 rids 168 ); 169 await offerer.setRemoteDescription({ type: 'answer', sdp: simulcastAnswer }); 170 } 171 172 async function doOfferToRecvSimulcast(offerer, answerer, rids) { 173 await offerer.setLocalDescription(); 174 const simulcastOffer = midToRid( 175 offerer.localDescription, 176 answerer.localDescription, 177 rids 178 ); 179 await answerer.setRemoteDescription({ type: 'offer', sdp: simulcastOffer }); 180 } 181 182 async function doAnswerToSendSimulcast(offerer, answerer) { 183 await answerer.setLocalDescription(); 184 185 // See which mids the offerer had; it will barf if we remove or reorder them. 186 const sections = SDPUtils.splitSections(offerer.localDescription.sdp); 187 sections.shift(); 188 const mids = sections.map(section => SDPUtils.getMid(section)); 189 let nonSimulcastAnswer = ridToMid(answerer.localDescription, mids); 190 // Restore MID RTP header extension. 191 const localParameters = SDPUtils.parseRtpParameters(sections[0]); 192 193 const localMidExtension = localParameters.headerExtensions 194 .find(ext => ext.uri === 'urn:ietf:params:rtp-hdrext:sdes:mid'); 195 if (localMidExtension) { 196 nonSimulcastAnswer += SDPUtils.writeExtmap(localMidExtension); 197 } 198 await offerer.setRemoteDescription({ 199 type: 'answer', 200 sdp: nonSimulcastAnswer, 201 }); 202 } 203 204 async function doOfferToSendSimulcastAndAnswer(offerer, answerer, rids) { 205 await doOfferToSendSimulcast(offerer, answerer); 206 await doAnswerToRecvSimulcast(offerer, answerer, rids); 207 } 208 209 async function doOfferToRecvSimulcastAndAnswer(offerer, answerer, rids) { 210 await doOfferToRecvSimulcast(offerer, answerer, rids); 211 await doAnswerToSendSimulcast(offerer, answerer); 212 } 213 214 function swapRidAndMidExtensionsInSimulcastOffer(offer, rids) { 215 return ridToMid(offer, rids); 216 } 217 218 function swapRidAndMidExtensionsInSimulcastAnswer(answer, localDescription, rids) { 219 return midToRid(answer, localDescription, rids); 220 } 221 222 async function negotiateSimulcastAndWaitForVideo( 223 t, stream, rids, pc1, pc2, codec, scalabilityMode = undefined) { 224 exchangeIceCandidates(pc1, pc2); 225 226 const metadataToBeLoaded = []; 227 pc2.ontrack = (e) => { 228 const stream = e.streams[0]; 229 const v = document.createElement('video'); 230 v.autoplay = true; 231 v.srcObject = stream; 232 v.id = stream.id 233 metadataToBeLoaded.push(new Promise((resolve) => { 234 v.addEventListener('loadedmetadata', () => { 235 resolve(); 236 }); 237 })); 238 }; 239 240 const sendEncodings = rids.map(rid => ({rid})); 241 // Use a 2X downscale factor between each layer. To improve ramp-up time, the 242 // top layer is scaled down by a factor 2. Smaller layer comes first. For 243 // example if MediaStreamTrack is 720p and we want to send three layers we'll 244 // get {90p, 180p, 360p}. 245 let scaleResolutionDownBy = 2; 246 for (let i = sendEncodings.length - 1; i >= 0; --i) { 247 if (scalabilityMode) { 248 sendEncodings[i].scalabilityMode = scalabilityMode; 249 } 250 sendEncodings[i].scaleResolutionDownBy = scaleResolutionDownBy; 251 scaleResolutionDownBy *= 2; 252 } 253 254 const transceiver = pc1.addTransceiver(stream.getVideoTracks()[0], { 255 streams: [stream], 256 sendEncodings: sendEncodings, 257 }); 258 if (codec) { 259 preferCodec(transceiver, codec.mimeType, codec.sdpFmtpLine); 260 } 261 262 const offer = await pc1.createOffer(); 263 await pc1.setLocalDescription(offer), 264 await pc2.setRemoteDescription({ 265 type: 'offer', 266 sdp: swapRidAndMidExtensionsInSimulcastOffer(offer, rids), 267 }); 268 const answer = await pc2.createAnswer(); 269 await pc2.setLocalDescription(answer); 270 await pc1.setRemoteDescription({ 271 type: 'answer', 272 sdp: swapRidAndMidExtensionsInSimulcastAnswer(answer, pc1.localDescription, rids), 273 }); 274 assert_equals(metadataToBeLoaded.length, rids.length); 275 return Promise.all(metadataToBeLoaded); 276 } 277 278 async function getCameraStream(t) { 279 // Use getUserMedia as getNoiseStream does not have enough entropy to ramp-up. 280 await setMediaPermission(); 281 const stream = await navigator.mediaDevices.getUserMedia({video: {width: 640, height: 480}}); 282 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 283 return stream; 284 }