sdpUtils.js (13418B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 var sdputils = { 6 // Finds the codec id / payload type given a codec format 7 // (e.g., "VP8", "VP9/90000"). `offset` tells us which one to use in case of 8 // multiple matches. 9 findCodecId(sdp, format, offset = 0) { 10 let regex = new RegExp("rtpmap:([0-9]+) " + format, "gi"); 11 let match; 12 for (let i = 0; i <= offset; ++i) { 13 match = regex.exec(sdp); 14 if (!match) { 15 throw new Error( 16 "Couldn't find offset " + 17 i + 18 " of codec " + 19 format + 20 " while looking for offset " + 21 offset + 22 " in sdp:\n" + 23 sdp 24 ); 25 } 26 } 27 // match[0] is the full matched string 28 // match[1] is the first parenthesis group 29 return match[1]; 30 }, 31 32 // Returns a list of all payload types, excluding rtx, in an sdp. 33 getPayloadTypes(sdp) { 34 const regex = /^a=rtpmap:([0-9]+) (?:(?!rtx).)*$/gim; 35 const pts = []; 36 for (const [line, pt] of sdp.matchAll(regex)) { 37 pts.push(pt); 38 } 39 return pts; 40 }, 41 42 // Finds all the extmap ids in the given sdp. Note that this does NOT 43 // consider m-sections, so a more generic version would need to 44 // look at each m-section separately. 45 findExtmapIds(sdp) { 46 var sdpExtmapIds = []; 47 extmapRegEx = /^a=extmap:([0-9+])/gm; 48 // must call exec on the regex to get each match in the string 49 while ((searchResults = extmapRegEx.exec(sdp)) !== null) { 50 // returned array has the matched text as the first item, 51 // and then one item for each capturing parenthesis that 52 // matched containing the text that was captured. 53 sdpExtmapIds.push(searchResults[1]); 54 } 55 return sdpExtmapIds; 56 }, 57 58 findExtmapIdsUrnsDirections(sdp) { 59 var sdpExtmap = []; 60 extmapRegEx = /^a=extmap:([0-9+])([A-Za-z/]*) ([A-Za-z0-9_:#\-\/\.]+)/gm; 61 // must call exec on the regex to get each match in the string 62 while ((searchResults = extmapRegEx.exec(sdp)) !== null) { 63 // returned array has the matched text as the first item, 64 // and then one item for each capturing parenthesis that 65 // matched containing the text that was captured. 66 var idUrn = []; 67 idUrn.push(searchResults[1]); 68 idUrn.push(searchResults[3]); 69 idUrn.push(searchResults[2].slice(1)); 70 sdpExtmap.push(idUrn); 71 } 72 return sdpExtmap; 73 }, 74 75 verify_unique_extmap_ids(sdp) { 76 const sdpExtmapIds = sdputils.findExtmapIdsUrnsDirections(sdp); 77 78 return sdpExtmapIds.reduce(function (result, item, index) { 79 const [id, urn, dir] = item; 80 ok( 81 !(id in result) || (result[id][0] === urn && result[id][1] === dir), 82 "ID " + id + " is unique ID for " + urn + " and direction " + dir 83 ); 84 result[id] = [urn, dir]; 85 return result; 86 }, {}); 87 }, 88 89 getMSections(sdp) { 90 return sdp 91 .split(new RegExp("^m=", "gm")) 92 .slice(1) 93 .map(s => "m=" + s); 94 }, 95 96 getAudioMSections(sdp) { 97 return this.getMSections(sdp).filter(section => 98 section.startsWith("m=audio") 99 ); 100 }, 101 102 getVideoMSections(sdp) { 103 return this.getMSections(sdp).filter(section => 104 section.startsWith("m=video") 105 ); 106 }, 107 108 checkSdpAfterEndOfTrickle(description, testOptions, label) { 109 info("EOC-SDP: " + JSON.stringify(description)); 110 111 const checkForTransportAttributes = msection => { 112 info("Checking msection: " + msection); 113 ok( 114 msection.includes("a=end-of-candidates"), 115 label + ": SDP contains end-of-candidates" 116 ); 117 118 if (!msection.startsWith("m=application")) { 119 if (testOptions.rtcpmux) { 120 ok( 121 msection.includes("a=rtcp-mux"), 122 label + ": SDP contains rtcp-mux" 123 ); 124 } else { 125 ok(msection.includes("a=rtcp:"), label + ": SDP contains rtcp port"); 126 } 127 } 128 }; 129 130 const hasOwnTransport = msection => { 131 const port0Check = new RegExp(/^m=\S+ 0 /).exec(msection); 132 if (port0Check) { 133 return false; 134 } 135 const midMatch = new RegExp(/\r\na=mid:(\S+)/).exec(msection); 136 if (!midMatch) { 137 return true; 138 } 139 const mid = midMatch[1]; 140 const bundleGroupMatch = new RegExp( 141 "\\r\\na=group:BUNDLE \\S.* " + mid + "\\s+" 142 ).exec(description.sdp); 143 return bundleGroupMatch == null; 144 }; 145 146 const msectionsWithOwnTransports = this.getMSections( 147 description.sdp 148 ).filter(hasOwnTransport); 149 150 ok( 151 msectionsWithOwnTransports.length, 152 "SDP should contain at least one msection with a transport" 153 ); 154 msectionsWithOwnTransports.forEach(checkForTransportAttributes); 155 156 if (testOptions.ssrc) { 157 ok(description.sdp.includes("a=ssrc"), label + ": SDP contains a=ssrc"); 158 } else { 159 ok( 160 !description.sdp.includes("a=ssrc"), 161 label + ": SDP does not contain a=ssrc" 162 ); 163 } 164 }, 165 166 // Note, we don't bother removing the fmtp lines, which makes a good test 167 // for some SDP parsing issues. 168 removeCodec(sdp, codec) { 169 var updated_sdp = sdp.replace( 170 new RegExp("a=rtpmap:" + codec + ".*\\/[0-9/]+\\r\\n", ""), 171 "" 172 ); 173 updated_sdp = updated_sdp.replace( 174 new RegExp("(RTP\\/SAVPF.*)( " + codec + ")(.*\\r\\n)", ""), 175 "$1$3" 176 ); 177 updated_sdp = updated_sdp.replace( 178 new RegExp("a=rtcp-fb:" + codec + " nack\\r\\n", ""), 179 "" 180 ); 181 updated_sdp = updated_sdp.replace( 182 new RegExp("a=rtcp-fb:" + codec + " nack pli\\r\\n", ""), 183 "" 184 ); 185 updated_sdp = updated_sdp.replace( 186 new RegExp("a=rtcp-fb:" + codec + " ccm fir\\r\\n", ""), 187 "" 188 ); 189 return updated_sdp; 190 }, 191 192 removeCodecs(sdp, codecs) { 193 var updated_sdp = sdp; 194 codecs.forEach(codec => { 195 updated_sdp = this.removeCodec(updated_sdp, codec); 196 }); 197 return updated_sdp; 198 }, 199 200 // Returns an array of objects with keys {pt, codec}. 201 enumerateCodecs(sdp) { 202 const codecs = []; 203 const regex = /a=rtpmap:(\d+)\s+([^\/]+)\/[0-9\/]+/gi; 204 let match; 205 while ((match = regex.exec(sdp))) { 206 if (match.length < 3) { 207 continue; 208 } 209 const pt = parseInt(match[1]); 210 if (isNaN(pt)) { 211 continue; 212 } 213 const codec = match[2]; 214 codecs.push({ pt, codec }); 215 } 216 return codecs; 217 }, 218 219 removeAllButPayloadType(sdp, payloadType) { 220 const codecs = this.enumerateCodecs(sdp); 221 const pts = codecs.map(({ pt }) => pt).filter(pt => pt != payloadType); 222 return this.removeCodecs(sdp, pts); 223 }, 224 225 removeAllButCodec(sdp, codecToKeep) { 226 const codecs = this.enumerateCodecs(sdp); 227 const pts = codecs 228 .filter(({ codec }) => codec.toLowerCase() != codecToKeep.toLowerCase()) 229 .map(({ pt }) => pt); 230 return this.removeCodecs(sdp, pts); 231 }, 232 233 removeRtpMapForPayloadType(sdp, pt) { 234 return sdp.replace(new RegExp("a=rtpmap:" + pt + ".*\\r\\n", "gi"), ""); 235 }, 236 237 removeRtcpMux(sdp) { 238 return sdp.replace(/a=rtcp-mux\r\n/g, ""); 239 }, 240 241 removeSSRCs(sdp) { 242 return sdp.replace(/a=ssrc.*\r\n/g, ""); 243 }, 244 245 removeBundle(sdp) { 246 return sdp.replace(/a=group:BUNDLE .*\r\n/g, ""); 247 }, 248 249 reduceAudioMLineToPcmuPcma(sdp) { 250 return sdp.replace( 251 /m=audio .*\r\n/g, 252 "m=audio 9 UDP/TLS/RTP/SAVPF 0 8\r\n" 253 ); 254 }, 255 256 setAllMsectionsInactive(sdp) { 257 return sdp 258 .replace(/\r\na=sendrecv/g, "\r\na=inactive") 259 .replace(/\r\na=sendonly/g, "\r\na=inactive") 260 .replace(/\r\na=recvonly/g, "\r\na=inactive"); 261 }, 262 263 removeAllRtpMaps(sdp) { 264 return sdp.replace(/a=rtpmap:.*\r\n/g, ""); 265 }, 266 267 reduceAudioMLineToDynamicPtAndOpus(sdp) { 268 return sdp.replace( 269 /m=audio .*\r\n/g, 270 "m=audio 9 UDP/TLS/RTP/SAVPF 101 109\r\n" 271 ); 272 }, 273 274 addTiasBps(sdp, bps) { 275 return sdp.replace(/c=IN (.*)\r\n/g, "c=IN $1\r\nb=TIAS:" + bps + "\r\n"); 276 }, 277 278 removeSimulcastProperties(sdp) { 279 return sdp 280 .replace(/a=simulcast:.*\r\n/g, "") 281 .replace(/a=rid:.*\r\n/g, "") 282 .replace( 283 /a=extmap:[^\s]* urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id.*\r\n/g, 284 "" 285 ) 286 .replace( 287 /a=extmap:[^\s]* urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id.*\r\n/g, 288 "" 289 ); 290 }, 291 292 transferSimulcastProperties(offer_sdp, answer_sdp) { 293 if (!offer_sdp.includes("a=simulcast:")) { 294 return answer_sdp; 295 } 296 ok( 297 offer_sdp.includes("a=simulcast:send "), 298 "Offer contains simulcast attribute" 299 ); 300 var o_simul = offer_sdp.match(/simulcast:send (.*)([\n$])*/i); 301 var new_answer_sdp = answer_sdp + "a=simulcast:recv " + o_simul[1] + "\r\n"; 302 ok(offer_sdp.includes("a=rid:"), "Offer contains RID attribute"); 303 var o_rids = offer_sdp.match(/a=rid:(.*)/gi); 304 o_rids.forEach(o_rid => { 305 new_answer_sdp = new_answer_sdp + o_rid.replace(/send/, "recv") + "\r\n"; 306 }); 307 var extmap_id = offer_sdp.match( 308 "a=extmap:([0-9+])/sendonly urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id" 309 ); 310 ok(extmap_id != null, "Offer contains RID RTP header extension"); 311 new_answer_sdp = 312 new_answer_sdp + 313 "a=extmap:" + 314 extmap_id[1] + 315 "/recvonly urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\n"; 316 var extmap_id = offer_sdp.match( 317 "a=extmap:([0-9+])/sendonly urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id" 318 ); 319 if (extmap_id != null) { 320 new_answer_sdp = 321 new_answer_sdp + 322 "a=extmap:" + 323 extmap_id[1] + 324 "/recvonly urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\n"; 325 } 326 327 return new_answer_sdp; 328 }, 329 330 verifySdp( 331 desc, 332 expectedType, 333 offerConstraintsList, 334 offerOptions, 335 testOptions 336 ) { 337 info("Examining this SessionDescription: " + JSON.stringify(desc)); 338 info("offerConstraintsList: " + JSON.stringify(offerConstraintsList)); 339 info("offerOptions: " + JSON.stringify(offerOptions)); 340 info("testOptions: " + JSON.stringify(testOptions)); 341 ok(desc, "SessionDescription is not null"); 342 is(desc.type, expectedType, "SessionDescription type is " + expectedType); 343 ok(desc.sdp.length > 10, "SessionDescription body length is plausible"); 344 ok(desc.sdp.includes("a=ice-ufrag"), "ICE username is present in SDP"); 345 ok(desc.sdp.includes("a=ice-pwd"), "ICE password is present in SDP"); 346 ok(desc.sdp.includes("a=fingerprint"), "ICE fingerprint is present in SDP"); 347 var requiresTrickleIce = !desc.sdp.includes("a=candidate"); 348 if (requiresTrickleIce) { 349 info("No ICE candidate in SDP -> requiring trickle ICE"); 350 } else { 351 info("at least one ICE candidate is present in SDP"); 352 } 353 354 //TODO: how can we check for absence/presence of m=application? 355 356 var audioTracks = 357 sdputils.countTracksInConstraint("audio", offerConstraintsList) || 358 (offerOptions && offerOptions.offerToReceiveAudio ? 1 : 0); 359 360 info("expected audio tracks: " + audioTracks); 361 if (audioTracks == 0) { 362 ok(!desc.sdp.includes("m=audio"), "audio m-line is absent from SDP"); 363 } else { 364 ok(desc.sdp.includes("m=audio"), "audio m-line is present in SDP"); 365 is( 366 testOptions.opus, 367 desc.sdp.includes("a=rtpmap:109 opus/48000/2"), 368 "OPUS codec is present in SDP" 369 ); 370 //TODO: ideally the rtcp-mux should be for the m=audio, and not just 371 // anywhere in the SDP (JS SDP parser bug 1045429) 372 is( 373 testOptions.rtcpmux, 374 desc.sdp.includes("a=rtcp-mux"), 375 "RTCP Mux is offered in SDP" 376 ); 377 } 378 379 var videoTracks = 380 sdputils.countTracksInConstraint("video", offerConstraintsList) || 381 (offerOptions && offerOptions.offerToReceiveVideo ? 1 : 0); 382 383 info("expected video tracks: " + videoTracks); 384 if (videoTracks == 0) { 385 ok(!desc.sdp.includes("m=video"), "video m-line is absent from SDP"); 386 } else { 387 ok(desc.sdp.includes("m=video"), "video m-line is present in SDP"); 388 if (testOptions.h264) { 389 ok( 390 desc.sdp.includes("a=rtpmap:126 H264/90000") || 391 desc.sdp.includes("a=rtpmap:97 H264/90000") || 392 desc.sdp.includes("a=rtpmap:103 H264/90000") || 393 desc.sdp.includes("a=rtpmap:105 H264/90000"), 394 "H.264 codec is present in SDP" 395 ); 396 } 397 if (testOptions.av1) { 398 ok( 399 desc.sdp.includes("a=rtpmap:99 AV1/90000"), 400 "AV1 codec is present in SDP" 401 ); 402 } 403 if (!testOptions.h264 && !testOptions.av1) { 404 ok( 405 desc.sdp.includes("a=rtpmap:120 VP8/90000") || 406 desc.sdp.includes("a=rtpmap:121 VP9/90000"), 407 "VP8 or VP9 codec is present in SDP" 408 ); 409 } 410 is( 411 testOptions.rtcpmux, 412 desc.sdp.includes("a=rtcp-mux"), 413 "RTCP Mux is offered in SDP" 414 ); 415 is( 416 testOptions.ssrc, 417 desc.sdp.includes("a=ssrc"), 418 "a=ssrc signaled in SDP" 419 ); 420 } 421 422 return requiresTrickleIce; 423 }, 424 425 /** 426 * Counts the amount of audio tracks in a given media constraint. 427 * 428 * @param constraints 429 * The contraint to be examined. 430 */ 431 countTracksInConstraint(type, constraints) { 432 if (!Array.isArray(constraints)) { 433 return 0; 434 } 435 return constraints.reduce((sum, c) => sum + (c[type] ? 1 : 0), 0); 436 }, 437 };