sdp.js (26247B)
1 /* eslint-env node */ 2 "use strict"; 3 4 // SDP helpers. 5 var SDPUtils = {}; 6 7 // Generate an alphanumeric identifier for cname or mids. 8 // TODO: use UUIDs instead? https://gist.github.com/jed/982883 9 SDPUtils.generateIdentifier = function() { 10 return Math.random() 11 .toString(36) 12 .substr(2, 10); 13 }; 14 15 // The RTCP CNAME used by all peerconnections from the same JS. 16 SDPUtils.localCName = SDPUtils.generateIdentifier(); 17 18 // Splits SDP into lines, dealing with both CRLF and LF. 19 SDPUtils.splitLines = function(blob) { 20 return blob 21 .trim() 22 .split("\n") 23 .map(function(line) { 24 return line.trim(); 25 }); 26 }; 27 // Splits SDP into sessionpart and mediasections. Ensures CRLF. 28 SDPUtils.splitSections = function(blob) { 29 var parts = blob.split("\nm="); 30 return parts.map(function(part, index) { 31 return (index > 0 ? "m=" + part : part).trim() + "\r\n"; 32 }); 33 }; 34 35 // returns the session description. 36 SDPUtils.getDescription = function(blob) { 37 var sections = SDPUtils.splitSections(blob); 38 return sections && sections[0]; 39 }; 40 41 // returns the individual media sections. 42 SDPUtils.getMediaSections = function(blob) { 43 var sections = SDPUtils.splitSections(blob); 44 sections.shift(); 45 return sections; 46 }; 47 48 // Returns lines that start with a certain prefix. 49 SDPUtils.matchPrefix = function(blob, prefix) { 50 return SDPUtils.splitLines(blob).filter(function(line) { 51 return line.indexOf(prefix) === 0; 52 }); 53 }; 54 55 SDPUtils.matchPrefixAndTrim = function(blob, prefix) { 56 return SDPUtils.matchPrefix(blob, prefix).map(l => l.substr(prefix.length).trim()); 57 } 58 59 // Parses an ICE candidate line. Sample input: 60 // candidate:702786350 2 udp 41819902 8.8.8.8 60769 typ relay raddr 8.8.8.8 61 // rport 55996" 62 SDPUtils.parseCandidate = function(line) { 63 var parts; 64 // Parse both variants. 65 if (line.indexOf("a=candidate:") === 0) { 66 parts = line.substring(12).split(" "); 67 } else { 68 parts = line.substring(10).split(" "); 69 } 70 71 var candidate = { 72 foundation: parts[0], 73 component: parseInt(parts[1], 10), 74 protocol: parts[2].toLowerCase(), 75 priority: parseInt(parts[3], 10), 76 ip: parts[4], 77 address: parts[4], // address is an alias for ip. 78 port: parseInt(parts[5], 10), 79 // skip parts[6] == 'typ' 80 type: parts[7], 81 }; 82 83 for (var i = 8; i < parts.length; i += 2) { 84 switch (parts[i]) { 85 case "raddr": 86 candidate.relatedAddress = parts[i + 1]; 87 break; 88 case "rport": 89 candidate.relatedPort = parseInt(parts[i + 1], 10); 90 break; 91 case "tcptype": 92 candidate.tcpType = parts[i + 1]; 93 break; 94 case "ufrag": 95 candidate.ufrag = parts[i + 1]; // for backward compability. 96 candidate.usernameFragment = parts[i + 1]; 97 break; 98 default: 99 // extension handling, in particular ufrag 100 candidate[parts[i]] = parts[i + 1]; 101 break; 102 } 103 } 104 return candidate; 105 }; 106 107 // Translates a candidate object into SDP candidate attribute. 108 SDPUtils.writeCandidate = function(candidate) { 109 var sdp = []; 110 sdp.push(candidate.foundation); 111 sdp.push(candidate.component); 112 sdp.push(candidate.protocol.toUpperCase()); 113 sdp.push(candidate.priority); 114 sdp.push(candidate.address || candidate.ip); 115 sdp.push(candidate.port); 116 117 var type = candidate.type; 118 sdp.push("typ"); 119 sdp.push(type); 120 if (type !== "host" && candidate.relatedAddress && candidate.relatedPort) { 121 sdp.push("raddr"); 122 sdp.push(candidate.relatedAddress); 123 sdp.push("rport"); 124 sdp.push(candidate.relatedPort); 125 } 126 if (candidate.tcpType && candidate.protocol.toLowerCase() === "tcp") { 127 sdp.push("tcptype"); 128 sdp.push(candidate.tcpType); 129 } 130 if (candidate.usernameFragment || candidate.ufrag) { 131 sdp.push("ufrag"); 132 sdp.push(candidate.usernameFragment || candidate.ufrag); 133 } 134 return "candidate:" + sdp.join(" "); 135 }; 136 137 // Parses an ice-options line, returns an array of option tags. 138 // a=ice-options:foo bar 139 SDPUtils.parseIceOptions = function(line) { 140 return line.substr(14).split(" "); 141 }; 142 143 // Parses an rtpmap line, returns RTCRtpCoddecParameters. Sample input: 144 // a=rtpmap:111 opus/48000/2 145 SDPUtils.parseRtpMap = function(line) { 146 var parts = line.substr(9).split(" "); 147 var parsed = { 148 payloadType: parseInt(parts.shift(), 10), // was: id 149 }; 150 151 parts = parts[0].split("/"); 152 153 parsed.name = parts[0]; 154 parsed.clockRate = parseInt(parts[1], 10); // was: clockrate 155 parsed.channels = parts.length === 3 ? parseInt(parts[2], 10) : 1; 156 // legacy alias, got renamed back to channels in ORTC. 157 parsed.numChannels = parsed.channels; 158 return parsed; 159 }; 160 161 // Generate an a=rtpmap line from RTCRtpCodecCapability or 162 // RTCRtpCodecParameters. 163 SDPUtils.writeRtpMap = function(codec) { 164 var pt = codec.payloadType; 165 if (codec.preferredPayloadType !== undefined) { 166 pt = codec.preferredPayloadType; 167 } 168 var channels = codec.channels || codec.numChannels || 1; 169 return ( 170 "a=rtpmap:" + 171 pt + 172 " " + 173 codec.name + 174 "/" + 175 codec.clockRate + 176 (channels !== 1 ? "/" + channels : "") + 177 "\r\n" 178 ); 179 }; 180 181 // Parses an a=extmap line (headerextension from RFC 5285). Sample input: 182 // a=extmap:2 urn:ietf:params:rtp-hdrext:toffset 183 // a=extmap:2/sendonly urn:ietf:params:rtp-hdrext:toffset 184 SDPUtils.parseExtmap = function(line) { 185 var parts = line.substr(9).split(" "); 186 return { 187 id: parseInt(parts[0], 10), 188 direction: parts[0].indexOf("/") > 0 ? parts[0].split("/")[1] : "sendrecv", 189 uri: parts[1], 190 }; 191 }; 192 193 // Generates a=extmap line from RTCRtpHeaderExtensionParameters or 194 // RTCRtpHeaderExtension. 195 SDPUtils.writeExtmap = function(headerExtension) { 196 return ( 197 "a=extmap:" + 198 (headerExtension.id || headerExtension.preferredId) + 199 (headerExtension.direction && headerExtension.direction !== "sendrecv" 200 ? "/" + headerExtension.direction 201 : "") + 202 " " + 203 headerExtension.uri + 204 "\r\n" 205 ); 206 }; 207 208 // Parses an ftmp line, returns dictionary. Sample input: 209 // a=fmtp:96 vbr=on;cng=on 210 // Also deals with vbr=on; cng=on 211 SDPUtils.parseFmtp = function(line) { 212 var parsed = {}; 213 var kv; 214 var parts = line.substr(line.indexOf(" ") + 1).split(";"); 215 for (var j = 0; j < parts.length; j++) { 216 kv = parts[j].trim().split("="); 217 parsed[kv[0].trim()] = kv[1]; 218 } 219 return parsed; 220 }; 221 222 // Generates an a=ftmp line from RTCRtpCodecCapability or RTCRtpCodecParameters. 223 SDPUtils.writeFmtp = function(codec) { 224 var line = ""; 225 var pt = codec.payloadType; 226 if (codec.preferredPayloadType !== undefined) { 227 pt = codec.preferredPayloadType; 228 } 229 if (codec.parameters && Object.keys(codec.parameters).length) { 230 var params = []; 231 Object.keys(codec.parameters).forEach(function(param) { 232 if (codec.parameters[param]) { 233 params.push(param + "=" + codec.parameters[param]); 234 } else { 235 params.push(param); 236 } 237 }); 238 line += "a=fmtp:" + pt + " " + params.join(";") + "\r\n"; 239 } 240 return line; 241 }; 242 243 // Parses an rtcp-fb line, returns RTCPRtcpFeedback object. Sample input: 244 // a=rtcp-fb:98 nack rpsi 245 SDPUtils.parseRtcpFb = function(line) { 246 var parts = line.substr(line.indexOf(" ") + 1).split(" "); 247 return { 248 type: parts.shift(), 249 parameter: parts.join(" "), 250 }; 251 }; 252 // Generate a=rtcp-fb lines from RTCRtpCodecCapability or RTCRtpCodecParameters. 253 SDPUtils.writeRtcpFb = function(codec) { 254 var lines = ""; 255 var pt = codec.payloadType; 256 if (codec.preferredPayloadType !== undefined) { 257 pt = codec.preferredPayloadType; 258 } 259 if (codec.rtcpFeedback && codec.rtcpFeedback.length) { 260 // FIXME: special handling for trr-int? 261 codec.rtcpFeedback.forEach(function(fb) { 262 lines += 263 "a=rtcp-fb:" + 264 pt + 265 " " + 266 fb.type + 267 (fb.parameter && fb.parameter.length ? " " + fb.parameter : "") + 268 "\r\n"; 269 }); 270 } 271 return lines; 272 }; 273 274 // Parses an RFC 5576 ssrc media attribute. Sample input: 275 // a=ssrc:3735928559 cname:something 276 SDPUtils.parseSsrcMedia = function(line) { 277 var sp = line.indexOf(" "); 278 var parts = { 279 ssrc: parseInt(line.substr(7, sp - 7), 10), 280 }; 281 var colon = line.indexOf(":", sp); 282 if (colon > -1) { 283 parts.attribute = line.substr(sp + 1, colon - sp - 1); 284 parts.value = line.substr(colon + 1); 285 } else { 286 parts.attribute = line.substr(sp + 1); 287 } 288 return parts; 289 }; 290 291 SDPUtils.parseSsrcGroup = function(line) { 292 var parts = line.substr(13).split(" "); 293 return { 294 semantics: parts.shift(), 295 ssrcs: parts.map(function(ssrc) { 296 return parseInt(ssrc, 10); 297 }), 298 }; 299 }; 300 301 // Extracts the MID (RFC 5888) from a media section. 302 // returns the MID or undefined if no mid line was found. 303 SDPUtils.getMid = function(mediaSection) { 304 var mid = SDPUtils.matchPrefix(mediaSection, "a=mid:")[0]; 305 if (mid) { 306 return mid.substr(6); 307 } 308 }; 309 310 SDPUtils.parseFingerprint = function(line) { 311 var parts = line.substr(14).split(" "); 312 return { 313 algorithm: parts[0].toLowerCase(), // algorithm is case-sensitive in Edge. 314 value: parts[1], 315 }; 316 }; 317 318 // Extracts DTLS parameters from SDP media section or sessionpart. 319 // FIXME: for consistency with other functions this should only 320 // get the fingerprint line as input. See also getIceParameters. 321 SDPUtils.getDtlsParameters = function(mediaSection, sessionpart) { 322 var lines = SDPUtils.matchPrefix( 323 mediaSection + sessionpart, 324 "a=fingerprint:" 325 ); 326 // Note: a=setup line is ignored since we use the 'auto' role. 327 // Note2: 'algorithm' is not case sensitive except in Edge. 328 return { 329 role: "auto", 330 fingerprints: lines.map(SDPUtils.parseFingerprint), 331 }; 332 }; 333 334 // Serializes DTLS parameters to SDP. 335 SDPUtils.writeDtlsParameters = function(params, setupType) { 336 var sdp = "a=setup:" + setupType + "\r\n"; 337 params.fingerprints.forEach(function(fp) { 338 sdp += "a=fingerprint:" + fp.algorithm + " " + fp.value + "\r\n"; 339 }); 340 return sdp; 341 }; 342 343 // Parses a=crypto lines into 344 // https://rawgit.com/aboba/edgertc/master/msortc-rs4.html#dictionary-rtcsrtpsdesparameters-members 345 SDPUtils.parseCryptoLine = function(line) { 346 var parts = line.substr(9).split(" "); 347 return { 348 tag: parseInt(parts[0], 10), 349 cryptoSuite: parts[1], 350 keyParams: parts[2], 351 sessionParams: parts.slice(3), 352 }; 353 }; 354 355 SDPUtils.writeCryptoLine = function(parameters) { 356 return ( 357 "a=crypto:" + 358 parameters.tag + 359 " " + 360 parameters.cryptoSuite + 361 " " + 362 (typeof parameters.keyParams === "object" 363 ? SDPUtils.writeCryptoKeyParams(parameters.keyParams) 364 : parameters.keyParams) + 365 (parameters.sessionParams ? " " + parameters.sessionParams.join(" ") : "") + 366 "\r\n" 367 ); 368 }; 369 370 // Parses the crypto key parameters into 371 // https://rawgit.com/aboba/edgertc/master/msortc-rs4.html#rtcsrtpkeyparam* 372 SDPUtils.parseCryptoKeyParams = function(keyParams) { 373 if (keyParams.indexOf("inline:") !== 0) { 374 return null; 375 } 376 var parts = keyParams.substr(7).split("|"); 377 return { 378 keyMethod: "inline", 379 keySalt: parts[0], 380 lifeTime: parts[1], 381 mkiValue: parts[2] ? parts[2].split(":")[0] : undefined, 382 mkiLength: parts[2] ? parts[2].split(":")[1] : undefined, 383 }; 384 }; 385 386 SDPUtils.writeCryptoKeyParams = function(keyParams) { 387 return ( 388 keyParams.keyMethod + 389 ":" + 390 keyParams.keySalt + 391 (keyParams.lifeTime ? "|" + keyParams.lifeTime : "") + 392 (keyParams.mkiValue && keyParams.mkiLength 393 ? "|" + keyParams.mkiValue + ":" + keyParams.mkiLength 394 : "") 395 ); 396 }; 397 398 // Extracts all SDES paramters. 399 SDPUtils.getCryptoParameters = function(mediaSection, sessionpart) { 400 var lines = SDPUtils.matchPrefix(mediaSection + sessionpart, "a=crypto:"); 401 return lines.map(SDPUtils.parseCryptoLine); 402 }; 403 404 // Parses ICE information from SDP media section or sessionpart. 405 // FIXME: for consistency with other functions this should only 406 // get the ice-ufrag and ice-pwd lines as input. 407 SDPUtils.getIceParameters = function(mediaSection, sessionpart) { 408 var ufrag = SDPUtils.matchPrefix( 409 mediaSection + sessionpart, 410 "a=ice-ufrag:" 411 )[0]; 412 var pwd = SDPUtils.matchPrefix(mediaSection + sessionpart, "a=ice-pwd:")[0]; 413 if (!(ufrag && pwd)) { 414 return null; 415 } 416 return { 417 usernameFragment: ufrag.substr(12), 418 password: pwd.substr(10), 419 }; 420 }; 421 422 // Serializes ICE parameters to SDP. 423 SDPUtils.writeIceParameters = function(params) { 424 return ( 425 "a=ice-ufrag:" + 426 params.usernameFragment + 427 "\r\n" + 428 "a=ice-pwd:" + 429 params.password + 430 "\r\n" 431 ); 432 }; 433 434 // Parses the SDP media section and returns RTCRtpParameters. 435 SDPUtils.parseRtpParameters = function(mediaSection) { 436 var description = { 437 codecs: [], 438 headerExtensions: [], 439 fecMechanisms: [], 440 rtcp: [], 441 }; 442 var lines = SDPUtils.splitLines(mediaSection); 443 var mline = lines[0].split(" "); 444 for (var i = 3; i < mline.length; i++) { 445 // find all codecs from mline[3..] 446 var pt = mline[i]; 447 var rtpmapline = SDPUtils.matchPrefix( 448 mediaSection, 449 "a=rtpmap:" + pt + " " 450 )[0]; 451 if (rtpmapline) { 452 var codec = SDPUtils.parseRtpMap(rtpmapline); 453 var fmtps = SDPUtils.matchPrefix(mediaSection, "a=fmtp:" + pt + " "); 454 // Only the first a=fmtp:<pt> is considered. 455 codec.parameters = fmtps.length ? SDPUtils.parseFmtp(fmtps[0]) : {}; 456 codec.rtcpFeedback = SDPUtils.matchPrefix( 457 mediaSection, 458 "a=rtcp-fb:" + pt + " " 459 ).map(SDPUtils.parseRtcpFb); 460 description.codecs.push(codec); 461 // parse FEC mechanisms from rtpmap lines. 462 switch (codec.name.toUpperCase()) { 463 case "RED": 464 case "ULPFEC": 465 description.fecMechanisms.push(codec.name.toUpperCase()); 466 break; 467 default: 468 // only RED and ULPFEC are recognized as FEC mechanisms. 469 break; 470 } 471 } 472 } 473 SDPUtils.matchPrefix(mediaSection, "a=extmap:").forEach(function(line) { 474 description.headerExtensions.push(SDPUtils.parseExtmap(line)); 475 }); 476 // FIXME: parse rtcp. 477 return description; 478 }; 479 480 // Generates parts of the SDP media section describing the capabilities / 481 // parameters. 482 SDPUtils.writeRtpDescription = function(kind, caps) { 483 var sdp = ""; 484 485 // Build the mline. 486 sdp += "m=" + kind + " "; 487 sdp += caps.codecs.length > 0 ? "9" : "0"; // reject if no codecs. 488 sdp += " UDP/TLS/RTP/SAVPF "; 489 sdp += 490 caps.codecs 491 .map(function(codec) { 492 if (codec.preferredPayloadType !== undefined) { 493 return codec.preferredPayloadType; 494 } 495 return codec.payloadType; 496 }) 497 .join(" ") + "\r\n"; 498 499 sdp += "c=IN IP4 0.0.0.0\r\n"; 500 sdp += "a=rtcp:9 IN IP4 0.0.0.0\r\n"; 501 502 // Add a=rtpmap lines for each codec. Also fmtp and rtcp-fb. 503 caps.codecs.forEach(function(codec) { 504 sdp += SDPUtils.writeRtpMap(codec); 505 sdp += SDPUtils.writeFmtp(codec); 506 sdp += SDPUtils.writeRtcpFb(codec); 507 }); 508 var maxptime = 0; 509 caps.codecs.forEach(function(codec) { 510 if (codec.maxptime > maxptime) { 511 maxptime = codec.maxptime; 512 } 513 }); 514 if (maxptime > 0) { 515 sdp += "a=maxptime:" + maxptime + "\r\n"; 516 } 517 sdp += "a=rtcp-mux\r\n"; 518 519 if (caps.headerExtensions) { 520 caps.headerExtensions.forEach(function(extension) { 521 sdp += SDPUtils.writeExtmap(extension); 522 }); 523 } 524 // FIXME: write fecMechanisms. 525 return sdp; 526 }; 527 528 // Parses the SDP media section and returns an array of 529 // RTCRtpEncodingParameters. 530 SDPUtils.parseRtpEncodingParameters = function(mediaSection) { 531 var encodingParameters = []; 532 var description = SDPUtils.parseRtpParameters(mediaSection); 533 var hasRed = description.fecMechanisms.indexOf("RED") !== -1; 534 var hasUlpfec = description.fecMechanisms.indexOf("ULPFEC") !== -1; 535 536 // filter a=ssrc:... cname:, ignore PlanB-msid 537 var ssrcs = SDPUtils.matchPrefix(mediaSection, "a=ssrc:") 538 .map(function(line) { 539 return SDPUtils.parseSsrcMedia(line); 540 }) 541 .filter(function(parts) { 542 return parts.attribute === "cname"; 543 }); 544 var primarySsrc = ssrcs.length > 0 && ssrcs[0].ssrc; 545 var secondarySsrc; 546 547 var flows = SDPUtils.matchPrefix(mediaSection, "a=ssrc-group:FID").map( 548 function(line) { 549 var parts = line.substr(17).split(" "); 550 return parts.map(function(part) { 551 return parseInt(part, 10); 552 }); 553 } 554 ); 555 if (flows.length > 0 && flows[0].length > 1 && flows[0][0] === primarySsrc) { 556 secondarySsrc = flows[0][1]; 557 } 558 559 description.codecs.forEach(function(codec) { 560 if (codec.name.toUpperCase() === "RTX" && codec.parameters.apt) { 561 var encParam = { 562 ssrc: primarySsrc, 563 codecPayloadType: parseInt(codec.parameters.apt, 10), 564 }; 565 if (primarySsrc && secondarySsrc) { 566 encParam.rtx = { ssrc: secondarySsrc }; 567 } 568 encodingParameters.push(encParam); 569 if (hasRed) { 570 encParam = JSON.parse(JSON.stringify(encParam)); 571 encParam.fec = { 572 ssrc: primarySsrc, 573 mechanism: hasUlpfec ? "red+ulpfec" : "red", 574 }; 575 encodingParameters.push(encParam); 576 } 577 } 578 }); 579 if (encodingParameters.length === 0 && primarySsrc) { 580 encodingParameters.push({ 581 ssrc: primarySsrc, 582 }); 583 } 584 585 // we support both b=AS and b=TIAS but interpret AS as TIAS. 586 var bandwidth = SDPUtils.matchPrefix(mediaSection, "b="); 587 if (bandwidth.length) { 588 if (bandwidth[0].indexOf("b=TIAS:") === 0) { 589 bandwidth = parseInt(bandwidth[0].substr(7), 10); 590 } else if (bandwidth[0].indexOf("b=AS:") === 0) { 591 // use formula from JSEP to convert b=AS to TIAS value. 592 bandwidth = 593 parseInt(bandwidth[0].substr(5), 10) * 1000 * 0.95 - 50 * 40 * 8; 594 } else { 595 bandwidth = undefined; 596 } 597 encodingParameters.forEach(function(params) { 598 params.maxBitrate = bandwidth; 599 }); 600 } 601 return encodingParameters; 602 }; 603 604 // parses http://draft.ortc.org/#rtcrtcpparameters* 605 SDPUtils.parseRtcpParameters = function(mediaSection) { 606 var rtcpParameters = {}; 607 608 // Gets the first SSRC. Note tha with RTX there might be multiple 609 // SSRCs. 610 var remoteSsrc = SDPUtils.matchPrefix(mediaSection, "a=ssrc:") 611 .map(function(line) { 612 return SDPUtils.parseSsrcMedia(line); 613 }) 614 .filter(function(obj) { 615 return obj.attribute === "cname"; 616 })[0]; 617 if (remoteSsrc) { 618 rtcpParameters.cname = remoteSsrc.value; 619 rtcpParameters.ssrc = remoteSsrc.ssrc; 620 } 621 622 // Edge uses the compound attribute instead of reducedSize 623 // compound is !reducedSize 624 var rsize = SDPUtils.matchPrefix(mediaSection, "a=rtcp-rsize"); 625 rtcpParameters.reducedSize = rsize.length > 0; 626 rtcpParameters.compound = rsize.length === 0; 627 628 // parses the rtcp-mux attrÑ–bute. 629 // Note that Edge does not support unmuxed RTCP. 630 var mux = SDPUtils.matchPrefix(mediaSection, "a=rtcp-mux"); 631 rtcpParameters.mux = mux.length > 0; 632 633 return rtcpParameters; 634 }; 635 636 // parses either a=msid: or a=ssrc:... msid lines and returns 637 // the id of the MediaStream and MediaStreamTrack. 638 SDPUtils.parseMsid = function(mediaSection) { 639 var parts; 640 var spec = SDPUtils.matchPrefix(mediaSection, "a=msid:"); 641 if (spec.length === 1) { 642 parts = spec[0].substr(7).split(" "); 643 return { stream: parts[0], track: parts[1] }; 644 } 645 var planB = SDPUtils.matchPrefix(mediaSection, "a=ssrc:") 646 .map(function(line) { 647 return SDPUtils.parseSsrcMedia(line); 648 }) 649 .filter(function(msidParts) { 650 return msidParts.attribute === "msid"; 651 }); 652 if (planB.length > 0) { 653 parts = planB[0].value.split(" "); 654 return { stream: parts[0], track: parts[1] }; 655 } 656 }; 657 658 // SCTP 659 // parses draft-ietf-mmusic-sctp-sdp-26 first and falls back 660 // to draft-ietf-mmusic-sctp-sdp-05 661 SDPUtils.parseSctpDescription = function(mediaSection) { 662 var mline = SDPUtils.parseMLine(mediaSection); 663 var maxSizeLine = SDPUtils.matchPrefix(mediaSection, "a=max-message-size:"); 664 var maxMessageSize; 665 if (maxSizeLine.length > 0) { 666 maxMessageSize = parseInt(maxSizeLine[0].substr(19), 10); 667 } 668 if (isNaN(maxMessageSize)) { 669 maxMessageSize = 65536; 670 } 671 var sctpPort = SDPUtils.matchPrefix(mediaSection, "a=sctp-port:"); 672 if (sctpPort.length > 0) { 673 return { 674 port: parseInt(sctpPort[0].substr(12), 10), 675 protocol: mline.fmt, 676 maxMessageSize, 677 }; 678 } 679 var sctpMapLines = SDPUtils.matchPrefix(mediaSection, "a=sctpmap:"); 680 if (sctpMapLines.length > 0) { 681 var parts = SDPUtils.matchPrefix(mediaSection, "a=sctpmap:")[0] 682 .substr(10) 683 .split(" "); 684 return { 685 port: parseInt(parts[0], 10), 686 protocol: parts[1], 687 maxMessageSize, 688 }; 689 } 690 }; 691 692 // SCTP 693 // outputs the draft-ietf-mmusic-sctp-sdp-26 version that all browsers 694 // support by now receiving in this format, unless we originally parsed 695 // as the draft-ietf-mmusic-sctp-sdp-05 format (indicated by the m-line 696 // protocol of DTLS/SCTP -- without UDP/ or TCP/) 697 SDPUtils.writeSctpDescription = function(media, sctp) { 698 var output = []; 699 if (media.protocol !== "DTLS/SCTP") { 700 output = [ 701 "m=" + media.kind + " 9 " + media.protocol + " " + sctp.protocol + "\r\n", 702 "c=IN IP4 0.0.0.0\r\n", 703 "a=sctp-port:" + sctp.port + "\r\n", 704 ]; 705 } else { 706 output = [ 707 "m=" + media.kind + " 9 " + media.protocol + " " + sctp.port + "\r\n", 708 "c=IN IP4 0.0.0.0\r\n", 709 "a=sctpmap:" + sctp.port + " " + sctp.protocol + " 65535\r\n", 710 ]; 711 } 712 if (sctp.maxMessageSize !== undefined) { 713 output.push("a=max-message-size:" + sctp.maxMessageSize + "\r\n"); 714 } 715 return output.join(""); 716 }; 717 718 // Generate a session ID for SDP. 719 // https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-20#section-5.2.1 720 // recommends using a cryptographically random +ve 64-bit value 721 // but right now this should be acceptable and within the right range 722 SDPUtils.generateSessionId = function() { 723 return Math.floor((Math.random() * 4294967296) + 1); 724 }; 725 726 // Write boilder plate for start of SDP 727 // sessId argument is optional - if not supplied it will 728 // be generated randomly 729 // sessVersion is optional and defaults to 2 730 // sessUser is optional and defaults to 'thisisadapterortc' 731 SDPUtils.writeSessionBoilerplate = function(sessId, sessVer, sessUser) { 732 var sessionId; 733 var version = sessVer !== undefined ? sessVer : 2; 734 if (sessId) { 735 sessionId = sessId; 736 } else { 737 sessionId = SDPUtils.generateSessionId(); 738 } 739 var user = sessUser || "thisisadapterortc"; 740 // FIXME: sess-id should be an NTP timestamp. 741 return ( 742 "v=0\r\n" + 743 "o=" + 744 user + 745 " " + 746 sessionId + 747 " " + 748 version + 749 " IN IP4 127.0.0.1\r\n" + 750 "s=-\r\n" + 751 "t=0 0\r\n" 752 ); 753 }; 754 755 SDPUtils.writeMediaSection = function(transceiver, caps, type, stream) { 756 var sdp = SDPUtils.writeRtpDescription(transceiver.kind, caps); 757 758 // Map ICE parameters (ufrag, pwd) to SDP. 759 sdp += SDPUtils.writeIceParameters( 760 transceiver.iceGatherer.getLocalParameters() 761 ); 762 763 // Map DTLS parameters to SDP. 764 sdp += SDPUtils.writeDtlsParameters( 765 transceiver.dtlsTransport.getLocalParameters(), 766 type === "offer" ? "actpass" : "active" 767 ); 768 769 sdp += "a=mid:" + transceiver.mid + "\r\n"; 770 771 if (transceiver.direction) { 772 sdp += "a=" + transceiver.direction + "\r\n"; 773 } else if (transceiver.rtpSender && transceiver.rtpReceiver) { 774 sdp += "a=sendrecv\r\n"; 775 } else if (transceiver.rtpSender) { 776 sdp += "a=sendonly\r\n"; 777 } else if (transceiver.rtpReceiver) { 778 sdp += "a=recvonly\r\n"; 779 } else { 780 sdp += "a=inactive\r\n"; 781 } 782 783 if (transceiver.rtpSender) { 784 // spec. 785 var msid = 786 "msid:" + stream.id + " " + transceiver.rtpSender.track.id + "\r\n"; 787 sdp += "a=" + msid; 788 789 // for Chrome. 790 sdp += "a=ssrc:" + transceiver.sendEncodingParameters[0].ssrc + " " + msid; 791 if (transceiver.sendEncodingParameters[0].rtx) { 792 sdp += 793 "a=ssrc:" + transceiver.sendEncodingParameters[0].rtx.ssrc + " " + msid; 794 sdp += 795 "a=ssrc-group:FID " + 796 transceiver.sendEncodingParameters[0].ssrc + 797 " " + 798 transceiver.sendEncodingParameters[0].rtx.ssrc + 799 "\r\n"; 800 } 801 } 802 // FIXME: this should be written by writeRtpDescription. 803 sdp += 804 "a=ssrc:" + 805 transceiver.sendEncodingParameters[0].ssrc + 806 " cname:" + 807 SDPUtils.localCName + 808 "\r\n"; 809 if (transceiver.rtpSender && transceiver.sendEncodingParameters[0].rtx) { 810 sdp += 811 "a=ssrc:" + 812 transceiver.sendEncodingParameters[0].rtx.ssrc + 813 " cname:" + 814 SDPUtils.localCName + 815 "\r\n"; 816 } 817 return sdp; 818 }; 819 820 // Gets the direction from the mediaSection or the sessionpart. 821 SDPUtils.getDirection = function(mediaSection, sessionpart) { 822 // Look for sendrecv, sendonly, recvonly, inactive, default to sendrecv. 823 var lines = SDPUtils.splitLines(mediaSection); 824 for (var i = 0; i < lines.length; i++) { 825 switch (lines[i]) { 826 case "a=sendrecv": 827 case "a=sendonly": 828 case "a=recvonly": 829 case "a=inactive": 830 return lines[i].substr(2); 831 default: 832 // FIXME: What should happen here? 833 } 834 } 835 if (sessionpart) { 836 return SDPUtils.getDirection(sessionpart); 837 } 838 return "sendrecv"; 839 }; 840 841 SDPUtils.getKind = function(mediaSection) { 842 var lines = SDPUtils.splitLines(mediaSection); 843 var mline = lines[0].split(" "); 844 return mline[0].substr(2); 845 }; 846 847 SDPUtils.isRejected = function(mediaSection) { 848 return mediaSection.split(" ", 2)[1] === "0"; 849 }; 850 851 SDPUtils.parseMLine = function(mediaSection) { 852 var lines = SDPUtils.splitLines(mediaSection); 853 var parts = lines[0].substr(2).split(" "); 854 return { 855 kind: parts[0], 856 port: parseInt(parts[1], 10), 857 protocol: parts[2], 858 fmt: parts.slice(3).join(" "), 859 }; 860 }; 861 862 SDPUtils.parseOLine = function(mediaSection) { 863 var line = SDPUtils.matchPrefix(mediaSection, "o=")[0]; 864 var parts = line.substr(2).split(" "); 865 return { 866 username: parts[0], 867 sessionId: parts[1], 868 sessionVersion: parseInt(parts[2], 10), 869 netType: parts[3], 870 addressType: parts[4], 871 address: parts[5], 872 }; 873 }; 874 875 // a very naive interpretation of a valid SDP. 876 SDPUtils.isValidSDP = function(blob) { 877 if (typeof blob !== "string" || blob.length === 0) { 878 return false; 879 } 880 var lines = SDPUtils.splitLines(blob); 881 for (var i = 0; i < lines.length; i++) { 882 if (lines[i].length < 2 || lines[i].charAt(1) !== "=") { 883 return false; 884 } 885 // TODO: check the modifier a bit more. 886 } 887 return true; 888 }; 889 890 // Expose public methods. 891 if (typeof module === "object") { 892 module.exports = SDPUtils; 893 }