simulcast.js (7515B)
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 // Borrowed from wpt, with some dependencies removed. 12 13 const ridExtensions = [ 14 "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id", 15 "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id", 16 ]; 17 18 function ridToMid(description, rids) { 19 const sections = SDPUtils.splitSections(description.sdp); 20 const dtls = SDPUtils.getDtlsParameters(sections[1], sections[0]); 21 const ice = SDPUtils.getIceParameters(sections[1], sections[0]); 22 const rtpParameters = SDPUtils.parseRtpParameters(sections[1]); 23 const setupValue = description.sdp.match(/a=setup:(.*)/)[1]; 24 const directionValue = 25 description.sdp.match(/a=sendrecv|a=sendonly|a=recvonly|a=inactive/) || 26 "a=sendrecv"; 27 const mline = SDPUtils.parseMLine(sections[1]); 28 29 // Skip mid extension; we are replacing it with the rid extmap 30 rtpParameters.headerExtensions = rtpParameters.headerExtensions.filter( 31 ext => ext.uri != "urn:ietf:params:rtp-hdrext:sdes:mid" 32 ); 33 34 for (const ext of rtpParameters.headerExtensions) { 35 if (ext.uri == "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id") { 36 ext.uri = "urn:ietf:params:rtp-hdrext:sdes:mid"; 37 } 38 } 39 40 // Filter rtx as we have no way to (re)interpret rrid. 41 // Not doing this makes probing use RTX, it's not understood and ramp-up is slower. 42 rtpParameters.codecs = rtpParameters.codecs.filter( 43 c => c.name.toUpperCase() !== "RTX" 44 ); 45 46 if (!rids) { 47 rids = Array.from(description.sdp.matchAll(/a=rid:(.*) send/g)).map( 48 r => r[1] 49 ); 50 } 51 52 let sdp = 53 SDPUtils.writeSessionBoilerplate() + 54 SDPUtils.writeDtlsParameters(dtls, setupValue) + 55 SDPUtils.writeIceParameters(ice) + 56 "a=group:BUNDLE " + 57 rids.join(" ") + 58 "\r\n"; 59 const baseRtpDescription = SDPUtils.writeRtpDescription( 60 mline.kind, 61 rtpParameters 62 ); 63 for (const rid of rids) { 64 sdp += 65 baseRtpDescription + 66 "a=mid:" + 67 rid + 68 "\r\n" + 69 "a=msid:rid-" + 70 rid + 71 " rid-" + 72 rid + 73 "\r\n"; 74 sdp += directionValue + "\r\n"; 75 } 76 return sdp; 77 } 78 79 function midToRid(description, localDescription, rids) { 80 const sections = SDPUtils.splitSections(description.sdp); 81 const dtls = SDPUtils.getDtlsParameters(sections[1], sections[0]); 82 const ice = SDPUtils.getIceParameters(sections[1], sections[0]); 83 const rtpParameters = SDPUtils.parseRtpParameters(sections[1]); 84 const setupValue = description.sdp.match(/a=setup:(.*)/)[1]; 85 const directionValue = 86 description.sdp.match(/a=sendrecv|a=sendonly|a=recvonly|a=inactive/) || 87 "a=sendrecv"; 88 const mline = SDPUtils.parseMLine(sections[1]); 89 90 // Skip rid extensions; we are replacing them with the mid extmap 91 rtpParameters.headerExtensions = rtpParameters.headerExtensions.filter( 92 ext => !ridExtensions.includes(ext.uri) 93 ); 94 95 for (const ext of rtpParameters.headerExtensions) { 96 if (ext.uri == "urn:ietf:params:rtp-hdrext:sdes:mid") { 97 ext.uri = "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id"; 98 } 99 } 100 101 const localMid = localDescription 102 ? SDPUtils.getMid(SDPUtils.splitSections(localDescription.sdp)[1]) 103 : "0"; 104 105 if (!rids) { 106 rids = []; 107 for (let i = 1; i < sections.length; i++) { 108 rids.push(SDPUtils.getMid(sections[i])); 109 } 110 } 111 112 let sdp = 113 SDPUtils.writeSessionBoilerplate() + 114 SDPUtils.writeDtlsParameters(dtls, setupValue) + 115 SDPUtils.writeIceParameters(ice) + 116 "a=group:BUNDLE " + 117 localMid + 118 "\r\n"; 119 sdp += SDPUtils.writeRtpDescription(mline.kind, rtpParameters); 120 // Although we are converting mids to rids, we still need a mid. 121 // The first one will be consistent with trickle ICE candidates. 122 sdp += "a=mid:" + localMid + "\r\n"; 123 sdp += directionValue + "\r\n"; 124 125 for (const rid of rids) { 126 const stringrid = String(rid); // allow integers 127 const choices = stringrid.split(","); 128 choices.forEach(choice => { 129 sdp += "a=rid:" + choice + " recv\r\n"; 130 }); 131 } 132 if (rids.length) { 133 sdp += "a=simulcast:recv " + rids.join(";") + "\r\n"; 134 } 135 136 return sdp; 137 } 138 139 async function doOfferToSendSimulcast(offerer, answerer) { 140 await offerer.setLocalDescription(); 141 142 // Is this a renegotiation? If so, we cannot remove (or reorder!) any mids, 143 // even if some rids have been removed or reordered. 144 let mids = []; 145 if (answerer.localDescription) { 146 // Renegotiation. Mids must be the same as before, because renegotiation 147 // can never remove or reorder mids, nor can it expand the simulcast 148 // envelope. 149 mids = [...answerer.localDescription.sdp.matchAll(/a=mid:(.*)/g)].map( 150 e => e[1] 151 ); 152 } else { 153 // First negotiation; the mids will be exactly the same as the rids 154 const simulcastAttr = offerer.localDescription.sdp.match( 155 /a=simulcast:send (.*)/ 156 ); 157 if (simulcastAttr) { 158 mids = simulcastAttr[1].split(";"); 159 } 160 } 161 162 const nonSimulcastOffer = ridToMid(offerer.localDescription, mids); 163 await answerer.setRemoteDescription({ 164 type: "offer", 165 sdp: nonSimulcastOffer, 166 }); 167 } 168 169 async function doAnswerToRecvSimulcast(offerer, answerer, rids) { 170 await answerer.setLocalDescription(); 171 const simulcastAnswer = midToRid( 172 answerer.localDescription, 173 offerer.localDescription, 174 rids 175 ); 176 await offerer.setRemoteDescription({ type: "answer", sdp: simulcastAnswer }); 177 } 178 179 async function doOfferToRecvSimulcast(offerer, answerer, rids) { 180 await offerer.setLocalDescription(); 181 const simulcastOffer = midToRid( 182 offerer.localDescription, 183 answerer.localDescription, 184 rids 185 ); 186 await answerer.setRemoteDescription({ type: "offer", sdp: simulcastOffer }); 187 } 188 189 async function doAnswerToSendSimulcast(offerer, answerer) { 190 await answerer.setLocalDescription(); 191 192 // See which mids the offerer had; it will barf if we remove or reorder them 193 const mids = [...offerer.localDescription.sdp.matchAll(/a=mid:(.*)/g)].map( 194 e => e[1] 195 ); 196 197 const nonSimulcastAnswer = ridToMid(answerer.localDescription, mids); 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 // This would be useful for cases other than simulcast, but we do not use it 215 // anywhere else right now, nor do we have a place for wpt-friendly helpers at 216 // the moment. 217 function createPlaybackElement(track) { 218 const elem = document.createElement(track.kind); 219 elem.autoplay = true; 220 elem.srcObject = new MediaStream([track]); 221 elem.id = track.id; 222 elem.width = 240; 223 elem.height = 180; 224 document.body.appendChild(elem); 225 return elem; 226 } 227 228 async function getPlaybackWithLoadedMetadata(track) { 229 const elem = createPlaybackElement(track); 230 return new Promise(resolve => { 231 elem.addEventListener("loadedmetadata", () => { 232 resolve(elem); 233 }); 234 }); 235 }