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