pax_global_header00006660000000000000000000000064135215124060014511gustar00rootroot0000000000000052 comment=bfdc0c1d0f9b6d647d96ab9fcb513fd305a5dbf8 sdp-2.10.0/000077500000000000000000000000001352151240600123575ustar00rootroot00000000000000sdp-2.10.0/.eslintrc000066400000000000000000000026451352151240600142120ustar00rootroot00000000000000{ "rules": { "array-bracket-spacing": 2, "block-spacing": [2, "never"], "brace-style": [2, "1tbs", {"allowSingleLine": false}], "camelcase": [2, {"properties": "always"}], "curly": 2, "default-case": 2, "dot-notation": 2, "eqeqeq": 2, "indent": [ 2, 2, {"SwitchCase": 1} ], "key-spacing": [2, {"beforeColon": false, "afterColon": true}], "max-len": [2, 80, 2, {"ignoreUrls": true}], "new-cap": [2, {"newIsCapExceptions": [ "webkitRTCPeerConnection", "mozRTCPeerConnection" ]}], "no-console": 0, "no-else-return": 2, "no-eval": 2, "no-multi-spaces": 2, "no-multiple-empty-lines": [2, {"max": 2}], "no-shadow": 2, "no-trailing-spaces": 2, "no-unused-expressions": 2, "no-unused-vars": [2, {"args": "none"}], "object-curly-spacing": [2, "never"], "padded-blocks": [2, "never"], "quotes": [ 2, "single" ], "semi": [ 2, "always" ], "keyword-spacing": 2, "space-before-blocks": 2, "space-before-function-paren": [2, "never"], "space-unary-ops": 2, "space-infix-ops": 2, "spaced-comment": 2, "valid-typeof": 2 }, "env": { "es6": false, "browser": true, "node": true }, "extends": ["eslint:recommended"], "globals": { "module": true, "require": true, "process": true, "Promise": true, } } sdp-2.10.0/.gitignore000066400000000000000000000000451352151240600143460ustar00rootroot00000000000000node_modules/ .nyc_output/ coverage/ sdp-2.10.0/.npmignore000066400000000000000000000000201352151240600143460ustar00rootroot00000000000000test/ coverage/ sdp-2.10.0/.travis.yml000066400000000000000000000001461352151240600144710ustar00rootroot00000000000000sudo: false language: node_js node_js: - 8 script: - npm test after_success: - npm run coverage sdp-2.10.0/LICENSE000066400000000000000000000020421352151240600133620ustar00rootroot00000000000000Copyright (c) 2017 Philipp Hancke Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. sdp-2.10.0/README.md000066400000000000000000000003561352151240600136420ustar00rootroot00000000000000# SDP parsing and utlities originally for generating SDP from ORTC and vice versa -- see https://github.com/fippo/adapter/tree/shim-ortc But it turned out to be useful for manipulating SDP outside the context of Microsofts Edge browser. sdp-2.10.0/package.json000066400000000000000000000012541352151240600146470ustar00rootroot00000000000000{ "name": "sdp", "version": "2.10.0", "description": "SDP parsing and serialization utilities", "main": "sdp.js", "repository": { "type": "git", "url": "git+https://github.com/fippo/sdp.git" }, "scripts": { "coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov", "test": "eslint sdp.js test/sdp.js && nyc --reporter html mocha test/sdp.js" }, "keywords": [ "sdp", "webrtc" ], "author": "Philipp Hancke", "license": "MIT", "devDependencies": { "chai": "^4.0.0", "codecov": "^3.0.4", "eslint": "^6.0.1", "mocha": "^5.2.0", "nyc": "^14.1.1", "sinon": "^2.3.2", "sinon-chai": "^2.10.0" } } sdp-2.10.0/sdp.js000066400000000000000000000571701352151240600135150ustar00rootroot00000000000000/* eslint-env node */ 'use strict'; // SDP helpers. var SDPUtils = {}; // Generate an alphanumeric identifier for cname or mids. // TODO: use UUIDs instead? https://gist.github.com/jed/982883 SDPUtils.generateIdentifier = function() { return Math.random().toString(36).substr(2, 10); }; // The RTCP CNAME used by all peerconnections from the same JS. SDPUtils.localCName = SDPUtils.generateIdentifier(); // Splits SDP into lines, dealing with both CRLF and LF. SDPUtils.splitLines = function(blob) { return blob.trim().split('\n').map(function(line) { return line.trim(); }); }; // Splits SDP into sessionpart and mediasections. Ensures CRLF. SDPUtils.splitSections = function(blob) { var parts = blob.split('\nm='); return parts.map(function(part, index) { return (index > 0 ? 'm=' + part : part).trim() + '\r\n'; }); }; // returns the session description. SDPUtils.getDescription = function(blob) { var sections = SDPUtils.splitSections(blob); return sections && sections[0]; }; // returns the individual media sections. SDPUtils.getMediaSections = function(blob) { var sections = SDPUtils.splitSections(blob); sections.shift(); return sections; }; // Returns lines that start with a certain prefix. SDPUtils.matchPrefix = function(blob, prefix) { return SDPUtils.splitLines(blob).filter(function(line) { return line.indexOf(prefix) === 0; }); }; // Parses an ICE candidate line. Sample input: // candidate:702786350 2 udp 41819902 8.8.8.8 60769 typ relay raddr 8.8.8.8 // rport 55996" SDPUtils.parseCandidate = function(line) { var parts; // Parse both variants. if (line.indexOf('a=candidate:') === 0) { parts = line.substring(12).split(' '); } else { parts = line.substring(10).split(' '); } var candidate = { foundation: parts[0], component: parseInt(parts[1], 10), protocol: parts[2].toLowerCase(), priority: parseInt(parts[3], 10), ip: parts[4], address: parts[4], // address is an alias for ip. port: parseInt(parts[5], 10), // skip parts[6] == 'typ' type: parts[7] }; for (var i = 8; i < parts.length; i += 2) { switch (parts[i]) { case 'raddr': candidate.relatedAddress = parts[i + 1]; break; case 'rport': candidate.relatedPort = parseInt(parts[i + 1], 10); break; case 'tcptype': candidate.tcpType = parts[i + 1]; break; case 'ufrag': candidate.ufrag = parts[i + 1]; // for backward compability. candidate.usernameFragment = parts[i + 1]; break; default: // extension handling, in particular ufrag candidate[parts[i]] = parts[i + 1]; break; } } return candidate; }; // Translates a candidate object into SDP candidate attribute. SDPUtils.writeCandidate = function(candidate) { var sdp = []; sdp.push(candidate.foundation); sdp.push(candidate.component); sdp.push(candidate.protocol.toUpperCase()); sdp.push(candidate.priority); sdp.push(candidate.address || candidate.ip); sdp.push(candidate.port); var type = candidate.type; sdp.push('typ'); sdp.push(type); if (type !== 'host' && candidate.relatedAddress && candidate.relatedPort) { sdp.push('raddr'); sdp.push(candidate.relatedAddress); sdp.push('rport'); sdp.push(candidate.relatedPort); } if (candidate.tcpType && candidate.protocol.toLowerCase() === 'tcp') { sdp.push('tcptype'); sdp.push(candidate.tcpType); } if (candidate.usernameFragment || candidate.ufrag) { sdp.push('ufrag'); sdp.push(candidate.usernameFragment || candidate.ufrag); } return 'candidate:' + sdp.join(' '); }; // Parses an ice-options line, returns an array of option tags. // a=ice-options:foo bar SDPUtils.parseIceOptions = function(line) { return line.substr(14).split(' '); }; // Parses an rtpmap line, returns RTCRtpCoddecParameters. Sample input: // a=rtpmap:111 opus/48000/2 SDPUtils.parseRtpMap = function(line) { var parts = line.substr(9).split(' '); var parsed = { payloadType: parseInt(parts.shift(), 10) // was: id }; parts = parts[0].split('/'); parsed.name = parts[0]; parsed.clockRate = parseInt(parts[1], 10); // was: clockrate parsed.channels = parts.length === 3 ? parseInt(parts[2], 10) : 1; // legacy alias, got renamed back to channels in ORTC. parsed.numChannels = parsed.channels; return parsed; }; // Generate an a=rtpmap line from RTCRtpCodecCapability or // RTCRtpCodecParameters. SDPUtils.writeRtpMap = function(codec) { var pt = codec.payloadType; if (codec.preferredPayloadType !== undefined) { pt = codec.preferredPayloadType; } var channels = codec.channels || codec.numChannels || 1; return 'a=rtpmap:' + pt + ' ' + codec.name + '/' + codec.clockRate + (channels !== 1 ? '/' + channels : '') + '\r\n'; }; // Parses an a=extmap line (headerextension from RFC 5285). Sample input: // a=extmap:2 urn:ietf:params:rtp-hdrext:toffset // a=extmap:2/sendonly urn:ietf:params:rtp-hdrext:toffset SDPUtils.parseExtmap = function(line) { var parts = line.substr(9).split(' '); return { id: parseInt(parts[0], 10), direction: parts[0].indexOf('/') > 0 ? parts[0].split('/')[1] : 'sendrecv', uri: parts[1] }; }; // Generates a=extmap line from RTCRtpHeaderExtensionParameters or // RTCRtpHeaderExtension. SDPUtils.writeExtmap = function(headerExtension) { return 'a=extmap:' + (headerExtension.id || headerExtension.preferredId) + (headerExtension.direction && headerExtension.direction !== 'sendrecv' ? '/' + headerExtension.direction : '') + ' ' + headerExtension.uri + '\r\n'; }; // Parses an ftmp line, returns dictionary. Sample input: // a=fmtp:96 vbr=on;cng=on // Also deals with vbr=on; cng=on SDPUtils.parseFmtp = function(line) { var parsed = {}; var kv; var parts = line.substr(line.indexOf(' ') + 1).split(';'); for (var j = 0; j < parts.length; j++) { kv = parts[j].trim().split('='); parsed[kv[0].trim()] = kv[1]; } return parsed; }; // Generates an a=ftmp line from RTCRtpCodecCapability or RTCRtpCodecParameters. SDPUtils.writeFmtp = function(codec) { var line = ''; var pt = codec.payloadType; if (codec.preferredPayloadType !== undefined) { pt = codec.preferredPayloadType; } if (codec.parameters && Object.keys(codec.parameters).length) { var params = []; Object.keys(codec.parameters).forEach(function(param) { if (codec.parameters[param]) { params.push(param + '=' + codec.parameters[param]); } else { params.push(param); } }); line += 'a=fmtp:' + pt + ' ' + params.join(';') + '\r\n'; } return line; }; // Parses an rtcp-fb line, returns RTCPRtcpFeedback object. Sample input: // a=rtcp-fb:98 nack rpsi SDPUtils.parseRtcpFb = function(line) { var parts = line.substr(line.indexOf(' ') + 1).split(' '); return { type: parts.shift(), parameter: parts.join(' ') }; }; // Generate a=rtcp-fb lines from RTCRtpCodecCapability or RTCRtpCodecParameters. SDPUtils.writeRtcpFb = function(codec) { var lines = ''; var pt = codec.payloadType; if (codec.preferredPayloadType !== undefined) { pt = codec.preferredPayloadType; } if (codec.rtcpFeedback && codec.rtcpFeedback.length) { // FIXME: special handling for trr-int? codec.rtcpFeedback.forEach(function(fb) { lines += 'a=rtcp-fb:' + pt + ' ' + fb.type + (fb.parameter && fb.parameter.length ? ' ' + fb.parameter : '') + '\r\n'; }); } return lines; }; // Parses an RFC 5576 ssrc media attribute. Sample input: // a=ssrc:3735928559 cname:something SDPUtils.parseSsrcMedia = function(line) { var sp = line.indexOf(' '); var parts = { ssrc: parseInt(line.substr(7, sp - 7), 10) }; var colon = line.indexOf(':', sp); if (colon > -1) { parts.attribute = line.substr(sp + 1, colon - sp - 1); parts.value = line.substr(colon + 1); } else { parts.attribute = line.substr(sp + 1); } return parts; }; SDPUtils.parseSsrcGroup = function(line) { var parts = line.substr(13).split(' '); return { semantics: parts.shift(), ssrcs: parts.map(function(ssrc) { return parseInt(ssrc, 10); }) }; }; // Extracts the MID (RFC 5888) from a media section. // returns the MID or undefined if no mid line was found. SDPUtils.getMid = function(mediaSection) { var mid = SDPUtils.matchPrefix(mediaSection, 'a=mid:')[0]; if (mid) { return mid.substr(6); } }; SDPUtils.parseFingerprint = function(line) { var parts = line.substr(14).split(' '); return { algorithm: parts[0].toLowerCase(), // algorithm is case-sensitive in Edge. value: parts[1] }; }; // Extracts DTLS parameters from SDP media section or sessionpart. // FIXME: for consistency with other functions this should only // get the fingerprint line as input. See also getIceParameters. SDPUtils.getDtlsParameters = function(mediaSection, sessionpart) { var lines = SDPUtils.matchPrefix(mediaSection + sessionpart, 'a=fingerprint:'); // Note: a=setup line is ignored since we use the 'auto' role. // Note2: 'algorithm' is not case sensitive except in Edge. return { role: 'auto', fingerprints: lines.map(SDPUtils.parseFingerprint) }; }; // Serializes DTLS parameters to SDP. SDPUtils.writeDtlsParameters = function(params, setupType) { var sdp = 'a=setup:' + setupType + '\r\n'; params.fingerprints.forEach(function(fp) { sdp += 'a=fingerprint:' + fp.algorithm + ' ' + fp.value + '\r\n'; }); return sdp; }; // Parses ICE information from SDP media section or sessionpart. // FIXME: for consistency with other functions this should only // get the ice-ufrag and ice-pwd lines as input. SDPUtils.getIceParameters = function(mediaSection, sessionpart) { var lines = SDPUtils.splitLines(mediaSection); // Search in session part, too. lines = lines.concat(SDPUtils.splitLines(sessionpart)); var iceParameters = { usernameFragment: lines.filter(function(line) { return line.indexOf('a=ice-ufrag:') === 0; })[0].substr(12), password: lines.filter(function(line) { return line.indexOf('a=ice-pwd:') === 0; })[0].substr(10) }; return iceParameters; }; // Serializes ICE parameters to SDP. SDPUtils.writeIceParameters = function(params) { return 'a=ice-ufrag:' + params.usernameFragment + '\r\n' + 'a=ice-pwd:' + params.password + '\r\n'; }; // Parses the SDP media section and returns RTCRtpParameters. SDPUtils.parseRtpParameters = function(mediaSection) { var description = { codecs: [], headerExtensions: [], fecMechanisms: [], rtcp: [] }; var lines = SDPUtils.splitLines(mediaSection); var mline = lines[0].split(' '); for (var i = 3; i < mline.length; i++) { // find all codecs from mline[3..] var pt = mline[i]; var rtpmapline = SDPUtils.matchPrefix( mediaSection, 'a=rtpmap:' + pt + ' ')[0]; if (rtpmapline) { var codec = SDPUtils.parseRtpMap(rtpmapline); var fmtps = SDPUtils.matchPrefix( mediaSection, 'a=fmtp:' + pt + ' '); // Only the first a=fmtp: is considered. codec.parameters = fmtps.length ? SDPUtils.parseFmtp(fmtps[0]) : {}; codec.rtcpFeedback = SDPUtils.matchPrefix( mediaSection, 'a=rtcp-fb:' + pt + ' ') .map(SDPUtils.parseRtcpFb); description.codecs.push(codec); // parse FEC mechanisms from rtpmap lines. switch (codec.name.toUpperCase()) { case 'RED': case 'ULPFEC': description.fecMechanisms.push(codec.name.toUpperCase()); break; default: // only RED and ULPFEC are recognized as FEC mechanisms. break; } } } SDPUtils.matchPrefix(mediaSection, 'a=extmap:').forEach(function(line) { description.headerExtensions.push(SDPUtils.parseExtmap(line)); }); // FIXME: parse rtcp. return description; }; // Generates parts of the SDP media section describing the capabilities / // parameters. SDPUtils.writeRtpDescription = function(kind, caps) { var sdp = ''; // Build the mline. sdp += 'm=' + kind + ' '; sdp += caps.codecs.length > 0 ? '9' : '0'; // reject if no codecs. sdp += ' UDP/TLS/RTP/SAVPF '; sdp += caps.codecs.map(function(codec) { if (codec.preferredPayloadType !== undefined) { return codec.preferredPayloadType; } return codec.payloadType; }).join(' ') + '\r\n'; sdp += 'c=IN IP4 0.0.0.0\r\n'; sdp += 'a=rtcp:9 IN IP4 0.0.0.0\r\n'; // Add a=rtpmap lines for each codec. Also fmtp and rtcp-fb. caps.codecs.forEach(function(codec) { sdp += SDPUtils.writeRtpMap(codec); sdp += SDPUtils.writeFmtp(codec); sdp += SDPUtils.writeRtcpFb(codec); }); var maxptime = 0; caps.codecs.forEach(function(codec) { if (codec.maxptime > maxptime) { maxptime = codec.maxptime; } }); if (maxptime > 0) { sdp += 'a=maxptime:' + maxptime + '\r\n'; } sdp += 'a=rtcp-mux\r\n'; if (caps.headerExtensions) { caps.headerExtensions.forEach(function(extension) { sdp += SDPUtils.writeExtmap(extension); }); } // FIXME: write fecMechanisms. return sdp; }; // Parses the SDP media section and returns an array of // RTCRtpEncodingParameters. SDPUtils.parseRtpEncodingParameters = function(mediaSection) { var encodingParameters = []; var description = SDPUtils.parseRtpParameters(mediaSection); var hasRed = description.fecMechanisms.indexOf('RED') !== -1; var hasUlpfec = description.fecMechanisms.indexOf('ULPFEC') !== -1; // filter a=ssrc:... cname:, ignore PlanB-msid var ssrcs = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:') .map(function(line) { return SDPUtils.parseSsrcMedia(line); }) .filter(function(parts) { return parts.attribute === 'cname'; }); var primarySsrc = ssrcs.length > 0 && ssrcs[0].ssrc; var secondarySsrc; var flows = SDPUtils.matchPrefix(mediaSection, 'a=ssrc-group:FID') .map(function(line) { var parts = line.substr(17).split(' '); return parts.map(function(part) { return parseInt(part, 10); }); }); if (flows.length > 0 && flows[0].length > 1 && flows[0][0] === primarySsrc) { secondarySsrc = flows[0][1]; } description.codecs.forEach(function(codec) { if (codec.name.toUpperCase() === 'RTX' && codec.parameters.apt) { var encParam = { ssrc: primarySsrc, codecPayloadType: parseInt(codec.parameters.apt, 10) }; if (primarySsrc && secondarySsrc) { encParam.rtx = {ssrc: secondarySsrc}; } encodingParameters.push(encParam); if (hasRed) { encParam = JSON.parse(JSON.stringify(encParam)); encParam.fec = { ssrc: primarySsrc, mechanism: hasUlpfec ? 'red+ulpfec' : 'red' }; encodingParameters.push(encParam); } } }); if (encodingParameters.length === 0 && primarySsrc) { encodingParameters.push({ ssrc: primarySsrc }); } // we support both b=AS and b=TIAS but interpret AS as TIAS. var bandwidth = SDPUtils.matchPrefix(mediaSection, 'b='); if (bandwidth.length) { if (bandwidth[0].indexOf('b=TIAS:') === 0) { bandwidth = parseInt(bandwidth[0].substr(7), 10); } else if (bandwidth[0].indexOf('b=AS:') === 0) { // use formula from JSEP to convert b=AS to TIAS value. bandwidth = parseInt(bandwidth[0].substr(5), 10) * 1000 * 0.95 - (50 * 40 * 8); } else { bandwidth = undefined; } encodingParameters.forEach(function(params) { params.maxBitrate = bandwidth; }); } return encodingParameters; }; // parses http://draft.ortc.org/#rtcrtcpparameters* SDPUtils.parseRtcpParameters = function(mediaSection) { var rtcpParameters = {}; // Gets the first SSRC. Note tha with RTX there might be multiple // SSRCs. var remoteSsrc = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:') .map(function(line) { return SDPUtils.parseSsrcMedia(line); }) .filter(function(obj) { return obj.attribute === 'cname'; })[0]; if (remoteSsrc) { rtcpParameters.cname = remoteSsrc.value; rtcpParameters.ssrc = remoteSsrc.ssrc; } // Edge uses the compound attribute instead of reducedSize // compound is !reducedSize var rsize = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-rsize'); rtcpParameters.reducedSize = rsize.length > 0; rtcpParameters.compound = rsize.length === 0; // parses the rtcp-mux attrіbute. // Note that Edge does not support unmuxed RTCP. var mux = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-mux'); rtcpParameters.mux = mux.length > 0; return rtcpParameters; }; // parses either a=msid: or a=ssrc:... msid lines and returns // the id of the MediaStream and MediaStreamTrack. SDPUtils.parseMsid = function(mediaSection) { var parts; var spec = SDPUtils.matchPrefix(mediaSection, 'a=msid:'); if (spec.length === 1) { parts = spec[0].substr(7).split(' '); return {stream: parts[0], track: parts[1]}; } var planB = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:') .map(function(line) { return SDPUtils.parseSsrcMedia(line); }) .filter(function(msidParts) { return msidParts.attribute === 'msid'; }); if (planB.length > 0) { parts = planB[0].value.split(' '); return {stream: parts[0], track: parts[1]}; } }; // SCTP // parses draft-ietf-mmusic-sctp-sdp-26 first and falls back // to draft-ietf-mmusic-sctp-sdp-05 SDPUtils.parseSctpDescription = function(mediaSection) { var mline = SDPUtils.parseMLine(mediaSection); var maxSizeLine = SDPUtils.matchPrefix(mediaSection, 'a=max-message-size:'); var maxMessageSize; if (maxSizeLine.length > 0) { maxMessageSize = parseInt(maxSizeLine[0].substr(19), 10); } if (isNaN(maxMessageSize)) { maxMessageSize = 65536; } var sctpPort = SDPUtils.matchPrefix(mediaSection, 'a=sctp-port:'); if (sctpPort.length > 0) { return { port: parseInt(sctpPort[0].substr(12), 10), protocol: mline.fmt, maxMessageSize: maxMessageSize }; } var sctpMapLines = SDPUtils.matchPrefix(mediaSection, 'a=sctpmap:'); if (sctpMapLines.length > 0) { var parts = SDPUtils.matchPrefix(mediaSection, 'a=sctpmap:')[0] .substr(10) .split(' '); return { port: parseInt(parts[0], 10), protocol: parts[1], maxMessageSize: maxMessageSize }; } }; // SCTP // outputs the draft-ietf-mmusic-sctp-sdp-26 version that all browsers // support by now receiving in this format, unless we originally parsed // as the draft-ietf-mmusic-sctp-sdp-05 format (indicated by the m-line // protocol of DTLS/SCTP -- without UDP/ or TCP/) SDPUtils.writeSctpDescription = function(media, sctp) { var output = []; if (media.protocol !== 'DTLS/SCTP') { output = [ 'm=' + media.kind + ' 9 ' + media.protocol + ' ' + sctp.protocol + '\r\n', 'c=IN IP4 0.0.0.0\r\n', 'a=sctp-port:' + sctp.port + '\r\n' ]; } else { output = [ 'm=' + media.kind + ' 9 ' + media.protocol + ' ' + sctp.port + '\r\n', 'c=IN IP4 0.0.0.0\r\n', 'a=sctpmap:' + sctp.port + ' ' + sctp.protocol + ' 65535\r\n' ]; } if (sctp.maxMessageSize !== undefined) { output.push('a=max-message-size:' + sctp.maxMessageSize + '\r\n'); } return output.join(''); }; // Generate a session ID for SDP. // https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-20#section-5.2.1 // recommends using a cryptographically random +ve 64-bit value // but right now this should be acceptable and within the right range SDPUtils.generateSessionId = function() { return Math.random().toString().substr(2, 21); }; // Write boilder plate for start of SDP // sessId argument is optional - if not supplied it will // be generated randomly // sessVersion is optional and defaults to 2 // sessUser is optional and defaults to 'thisisadapterortc' SDPUtils.writeSessionBoilerplate = function(sessId, sessVer, sessUser) { var sessionId; var version = sessVer !== undefined ? sessVer : 2; if (sessId) { sessionId = sessId; } else { sessionId = SDPUtils.generateSessionId(); } var user = sessUser || 'thisisadapterortc'; // FIXME: sess-id should be an NTP timestamp. return 'v=0\r\n' + 'o=' + user + ' ' + sessionId + ' ' + version + ' IN IP4 127.0.0.1\r\n' + 's=-\r\n' + 't=0 0\r\n'; }; SDPUtils.writeMediaSection = function(transceiver, caps, type, stream) { var sdp = SDPUtils.writeRtpDescription(transceiver.kind, caps); // Map ICE parameters (ufrag, pwd) to SDP. sdp += SDPUtils.writeIceParameters( transceiver.iceGatherer.getLocalParameters()); // Map DTLS parameters to SDP. sdp += SDPUtils.writeDtlsParameters( transceiver.dtlsTransport.getLocalParameters(), type === 'offer' ? 'actpass' : 'active'); sdp += 'a=mid:' + transceiver.mid + '\r\n'; if (transceiver.direction) { sdp += 'a=' + transceiver.direction + '\r\n'; } else if (transceiver.rtpSender && transceiver.rtpReceiver) { sdp += 'a=sendrecv\r\n'; } else if (transceiver.rtpSender) { sdp += 'a=sendonly\r\n'; } else if (transceiver.rtpReceiver) { sdp += 'a=recvonly\r\n'; } else { sdp += 'a=inactive\r\n'; } if (transceiver.rtpSender) { // spec. var msid = 'msid:' + stream.id + ' ' + transceiver.rtpSender.track.id + '\r\n'; sdp += 'a=' + msid; // for Chrome. sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc + ' ' + msid; if (transceiver.sendEncodingParameters[0].rtx) { sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc + ' ' + msid; sdp += 'a=ssrc-group:FID ' + transceiver.sendEncodingParameters[0].ssrc + ' ' + transceiver.sendEncodingParameters[0].rtx.ssrc + '\r\n'; } } // FIXME: this should be written by writeRtpDescription. sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc + ' cname:' + SDPUtils.localCName + '\r\n'; if (transceiver.rtpSender && transceiver.sendEncodingParameters[0].rtx) { sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc + ' cname:' + SDPUtils.localCName + '\r\n'; } return sdp; }; // Gets the direction from the mediaSection or the sessionpart. SDPUtils.getDirection = function(mediaSection, sessionpart) { // Look for sendrecv, sendonly, recvonly, inactive, default to sendrecv. var lines = SDPUtils.splitLines(mediaSection); for (var i = 0; i < lines.length; i++) { switch (lines[i]) { case 'a=sendrecv': case 'a=sendonly': case 'a=recvonly': case 'a=inactive': return lines[i].substr(2); default: // FIXME: What should happen here? } } if (sessionpart) { return SDPUtils.getDirection(sessionpart); } return 'sendrecv'; }; SDPUtils.getKind = function(mediaSection) { var lines = SDPUtils.splitLines(mediaSection); var mline = lines[0].split(' '); return mline[0].substr(2); }; SDPUtils.isRejected = function(mediaSection) { return mediaSection.split(' ', 2)[1] === '0'; }; SDPUtils.parseMLine = function(mediaSection) { var lines = SDPUtils.splitLines(mediaSection); var parts = lines[0].substr(2).split(' '); return { kind: parts[0], port: parseInt(parts[1], 10), protocol: parts[2], fmt: parts.slice(3).join(' ') }; }; SDPUtils.parseOLine = function(mediaSection) { var line = SDPUtils.matchPrefix(mediaSection, 'o=')[0]; var parts = line.substr(2).split(' '); return { username: parts[0], sessionId: parts[1], sessionVersion: parseInt(parts[2], 10), netType: parts[3], addressType: parts[4], address: parts[5] }; }; // a very naive interpretation of a valid SDP. SDPUtils.isValidSDP = function(blob) { if (typeof blob !== 'string' || blob.length === 0) { return false; } var lines = SDPUtils.splitLines(blob); for (var i = 0; i < lines.length; i++) { if (lines[i].length < 2 || lines[i].charAt(1) !== '=') { return false; } // TODO: check the modifier a bit more. } return true; }; // Expose public methods. if (typeof module === 'object') { module.exports = SDPUtils; } sdp-2.10.0/test/000077500000000000000000000000001352151240600133365ustar00rootroot00000000000000sdp-2.10.0/test/.eslintrc000066400000000000000000000001301352151240600151540ustar00rootroot00000000000000{ "env": { "node": true, "mocha": true, "es6": true } } sdp-2.10.0/test/sdp.js000066400000000000000000000763601352151240600144760ustar00rootroot00000000000000'use strict'; const SDPUtils = require('../sdp.js'); const chai = require('chai'); const expect = chai.expect; require('sinon'); chai.use(require('sinon-chai')); const videoSDP = 'v=0\r\no=- 1376706046264470145 3 IN IP4 127.0.0.1\r\ns=-\r\n' + 't=0 0\r\na=group:BUNDLE video\r\n' + 'a=msid-semantic: WMS EZVtYL50wdbfttMdmVFITVoKc4XgA0KBZXzd\r\n' + 'm=video 9 UDP/TLS/RTP/SAVPF 100 101 107 116 117 96 97 99 98\r\n' + 'c=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\n' + 'a=ice-ufrag:npaLWmWDg3Yp6vJt\r\na=ice-pwd:pdfQZAiFbcsFmUKWw55g4TD5\r\n' + 'a=fingerprint:sha-256 3D:05:43:01:66:AC:57:DC:17:55:08:5C:D4:25:D7:CA:FD' + ':E1:0E:C1:F4:F8:43:3E:10:CE:3E:E7:6E:20:B9:90\r\n' + 'a=setup:actpass\r\na=mid:video\r\n' + 'a=extmap:2 urn:ietf:params:rtp-hdrext:toffset\r\n' + 'a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n' + 'a=extmap:4 urn:3gpp:video-orientation\r\na=sendrecv\r\na=rtcp-mux\r\n' + 'a=rtcp-rsize\r\na=rtpmap:100 VP8/90000\r\n' + 'a=rtcp-fb:100 ccm fir\r\na=rtcp-fb:100 nack\r\na=rtcp-fb:100 nack pli\r\n' + 'a=rtcp-fb:100 goog-remb\r\na=rtcp-fb:100 transport-cc\r\n' + 'a=rtpmap:101 VP9/90000\r\na=rtcp-fb:101 ccm fir\r\na=rtcp-fb:101 nack\r\n' + 'a=rtcp-fb:101 nack pli\r\na=rtcp-fb:101 goog-remb\r\n' + 'a=rtcp-fb:101 transport-cc\r\na=rtpmap:107 H264/90000\r\n' + 'a=rtcp-fb:107 ccm fir\r\na=rtcp-fb:107 nack\r\na=rtcp-fb:107 nack pli\r\n' + 'a=rtcp-fb:107 goog-remb\r\na=rtcp-fb:107 transport-cc\r\n' + 'a=rtpmap:116 red/90000\r\na=rtpmap:117 ulpfec/90000\r\n' + 'a=rtpmap:96 rtx/90000\r\na=fmtp:96 apt=100\r\na=rtpmap:97 rtx/90000\r\n' + 'a=fmtp:97 apt=101\r\na=rtpmap:99 rtx/90000\r\na=fmtp:99 apt=107\r\n' + 'a=rtpmap:98 rtx/90000\r\na=fmtp:98 apt=116\r\n' + 'a=ssrc-group:FID 1734522595 2715962409\r\n' + 'a=ssrc:1734522595 cname:VrveQctHgkwqDKj6\r\n' + 'a=ssrc:1734522595 msid:EZVtYL50wdbfttMdmVFITVoKc4XgA0KBZXzd ' + '63238d63-9a20-4afc-832c-48678926afce\r\na=ssrc:1734522595 ' + 'mslabel:EZVtYL50wdbfttMdmVFITVoKc4XgA0KBZXzd\r\n' + 'a=ssrc:1734522595 label:63238d63-9a20-4afc-832c-48678926afce\r\n' + 'a=ssrc:2715962409 cname:VrveQctHgkwqDKj6\r\n' + 'a=ssrc:2715962409 msid:EZVtYL50wdbfttMdmVFITVoKc4XgA0KBZXzd ' + '63238d63-9a20-4afc-832c-48678926afce\r\n' + 'a=ssrc:2715962409 mslabel:EZVtYL50wdbfttMdmVFITVoKc4XgA0KBZXzd\r\n' + 'a=ssrc:2715962409 label:63238d63-9a20-4afc-832c-48678926afce\r\n'; // Firefox offer const videoSDP2 = 'v=0\r\n' + 'o=mozilla...THIS_IS_SDPARTA-45.0 5508396880163053452 0 IN IP4 0.0.0.0\r\n' + 's=-\r\nt=0 0\r\n' + 'a=fingerprint:sha-256 CC:0D:FB:A8:9F:59:36:57:69:F6:2C:0E:A3:EA:19:5A:E0' + ':D4:37:82:D4:7B:FB:94:3D:F6:0E:F8:29:A7:9E:9C\r\n' + 'a=ice-options:trickle\r\na=msid-semantic:WMS *\r\n' + 'm=video 9 UDP/TLS/RTP/SAVPF 120 126 97\r\n' + 'c=IN IP4 0.0.0.0\r\na=sendrecv\r\n' + 'a=fmtp:126 profile-level-id=42e01f;level-asymmetry-allowed=1;' + 'packetization-mode=1\r\n' + 'a=fmtp:97 profile-level-id=42e01f;level-asymmetry-allowed=1\r\n' + 'a=fmtp:120 max-fs=12288;max-fr=60\r\n' + 'a=ice-pwd:e81aeca45422c37aeb669274d8959200\r\n' + 'a=ice-ufrag:30607a5c\r\na=mid:sdparta_0\r\n' + 'a=msid:{782ddf65-d10e-4dad-80b9-27e9f3928d82} ' + '{37802bbd-01e2-481e-a2e8-acb5423b7a55}\r\n' + 'a=rtcp-fb:120 nack\r\na=rtcp-fb:120 nack pli\r\na=rtcp-fb:120 ccm fir\r\n' + 'a=rtcp-fb:126 nack\r\na=rtcp-fb:126 nack pli\r\na=rtcp-fb:126 ccm fir\r\n' + 'a=rtcp-fb:97 nack\r\na=rtcp-fb:97 nack pli\r\na=rtcp-fb:97 ccm fir\r\n' + 'a=rtcp-mux\r\na=rtpmap:120 VP8/90000\r\na=rtpmap:126 H264/90000\r\n' + 'a=rtpmap:97 H264/90000\r\na=setup:actpass\r\n' + 'a=ssrc:98927270 cname:{0817e909-53be-4a3f-ac45-b5a0e5edc3a7}\r\n'; describe('splitSections', () => { let parsed; it('returns an array', () => { parsed = SDPUtils.splitSections(videoSDP); expect(parsed).to.be.an('Array'); }); it('splits video-only SDP with only LF into two sections', () => { parsed = SDPUtils.splitSections(videoSDP.replace(/\r\n/g, '\n')); expect(parsed.length).to.equal(2); }); it('splits video-only SDP into two sections', () => { parsed = SDPUtils.splitSections(videoSDP); expect(parsed.length).to.equal(2); }); it('every section ends with CRLF', () => { expect(parsed.every(function(section) { return section.substr(-2) === '\r\n'; })).to.equal(true); }); it('joining sections without separator recreates SDP', () => { expect(parsed.join('')).to.equal(videoSDP); }); }); describe('getDescription', () => { let parsed; it('returns a string with the session description part', () => { parsed = SDPUtils.getDescription(videoSDP); expect(parsed).to.be.an('String'); }); it('ends with a CRLF', () => { parsed = SDPUtils.getDescription(videoSDP); expect(parsed.substr(-2)).to.equal('\r\n'); }); }); describe('getMediaSections', () => { let parsed; it('returns an array', () => { parsed = SDPUtils.getMediaSections(videoSDP); expect(parsed).to.be.an('Array'); }); it('splits video-only SDP into one section', () => { parsed = SDPUtils.getMediaSections(videoSDP); expect(parsed.length).to.equal(1); }); }); describe('parseRtpParameters with the video sdp example', () => { const sections = SDPUtils.splitSections(videoSDP); const parsed = SDPUtils.parseRtpParameters(sections[1]); it('parses 9 codecs', () => { expect(parsed.codecs.length).to.equal(9); }); describe('fecMechanisms', () => { it('parses 2 fecMechanisms', () => { expect(parsed.fecMechanisms.length).to.equal(2); }); it('parses RED as FEC mechanism', () => { expect(parsed.fecMechanisms).to.contain('RED'); }); it('parses ULPFEC as FEC mechanism', () => { expect(parsed.fecMechanisms).to.contain('ULPFEC'); }); }); it('parses 3 headerExtensions', () => { expect(parsed.headerExtensions.length).to.equal(3); }); }); describe('fmtp', () => { const line = 'a=fmtp:111 minptime=10; useinbandfec=1'; const parsed = SDPUtils.parseFmtp(line); describe('parsing', () => { it('parses 2 parameters', () => { expect(Object.keys(parsed).length).to.equal(2); }); it('parses minptime', () => { expect(parsed.minptime).to.equal('10'); }); it('parses useinbandfec', () => { expect(parsed.useinbandfec).to.equal('1'); }); }); describe('serialization', () => { it('uses preferredPayloadType', () => { let out = SDPUtils.writeFmtp({ preferredPayloadType: 111, parameters: {minptime: '10'} }).trim(); expect(out).to.equal('a=fmtp:111 minptime=10'); }); it('returns an empty string if there are no parameters', () => { let out = SDPUtils.writeFmtp({ preferredPayloadType: 111, parameters: {} }).trim(); expect(out).to.equal(''); }); // TODO: is this safe or can the order change? // serialization strings the extra whitespace after ';' it('does not add extra spaces between parameters', () => { let out = SDPUtils.writeFmtp({ payloadType: 111, parameters: parsed }).trim(); expect(out).to.equal(line.replace('; ', ';')); }); it('serializes non-key-value telephone-event', () => { const out = SDPUtils.writeFmtp({payloadType: 100, parameters: {'0-15': undefined}}); expect(out).to.equal('a=fmtp:100 0-15\r\n'); }); }); }); describe('rtpmap', () => { const line = 'a=rtpmap:111 opus/48000/2'; const parsed = SDPUtils.parseRtpMap(line); describe('parsing', () => { it('parses codec name', () => { expect(parsed.name).to.equal('opus'); }); it('parses payloadType as integer', () => { expect(parsed.payloadType).to.equal(111); }); it('parses clockRate as an integer', () => { expect(parsed.clockRate).to.equal(48000); }); it('parses channels as an integer', () => { expect(parsed.channels).to.equal(2); }); it('parses numChannels (legacy) as an integer', () => { expect(parsed.numChannels).to.equal(2); }); it('parses numChannels and defaults to 1 if not present', () => { expect(SDPUtils.parseRtpMap('a=rtpmap:0 PCMU/8000').numChannels) .to.equal(1); }); }); describe('serialization', () => { it('generates the expected output', () => { let out = SDPUtils.writeRtpMap({ payloadType: 111, name: 'opus', clockRate: 48000, numChannels: 2 }).trim(); expect(out).to.equal(line); }); it('uses preferredPayloadType', () => { let out = SDPUtils.writeRtpMap({ preferredPayloadType: 111, name: 'opus', clockRate: 48000, numChannels: 2 }).trim(); expect(out).to.equal(line); }); it('does not append channels when there is only one channel', () => { let out = SDPUtils.writeRtpMap({ payloadType: 0, name: 'pcmu', clockRate: 8000, channels: 1 }).trim(); expect(out).to.equal('a=rtpmap:0 pcmu/8000'); }); it('does not append channels when channels is undefined', () => { let out = SDPUtils.writeRtpMap({ payloadType: 0, name: 'pcmu', clockRate: 8000 }).trim(); expect(out).to.equal('a=rtpmap:0 pcmu/8000'); }); }); }); describe('parseRtpEncodingParameters', () => { let sections = SDPUtils.splitSections(videoSDP); let data = SDPUtils.parseRtpEncodingParameters(sections[1]); it('parses 8 encoding parameters for four codecs with fec', () => { expect(data.length).to.equal(8); }); it('parses primary ssrc', () => { expect(data[0].ssrc).to.equal(1734522595); }); it('parses RTX encoding and ssrc', () => { expect(data[0].rtx); expect(data[0].rtx.ssrc).to.equal(2715962409); }); it('parses ssrc from cname as a fallback', () => { sections = SDPUtils.splitSections(videoSDP2); data = SDPUtils.parseRtpEncodingParameters(sections[1]); expect(data.length).to.equal(1); expect(data[0].ssrc).to.equal(98927270); }); describe('bandwidth modifier', () => { it('of type AS is parsed', () => { sections = SDPUtils.splitSections( videoSDP.replace('c=IN IP4 0.0.0.0\r\n', 'c=IN IP4 0.0.0.0\r\nb=AS:512\r\n') ); data = SDPUtils.parseRtpEncodingParameters(sections[1]); // conversion formula from jsep. expect(data[0].maxBitrate).to.equal(512 * 1000 * 0.95 - (50 * 40 * 8)); }); it('of type TIAS is parsed', () => { sections = SDPUtils.splitSections( videoSDP.replace('c=IN IP4 0.0.0.0\r\n', 'c=IN IP4 0.0.0.0\r\nb=TIAS:512000\r\n') ); data = SDPUtils.parseRtpEncodingParameters(sections[1]); expect(data[0].maxBitrate).to.equal(512000); }); it('of unknown type is ignored', () => { sections = SDPUtils.splitSections( videoSDP.replace('c=IN IP4 0.0.0.0\r\n', 'c=IN IP4 0.0.0.0\r\nb=something:1\r\n') ); data = SDPUtils.parseRtpEncodingParameters(sections[1]); expect(data[0].maxBitrate).to.equal(undefined); }); }); }); describe('rtcp feedback', () => { describe('serialization', () => { it('serializes', () => { const codec = {payloadType: 100, rtcpFeedback: [ {type: 'nack', parameter: 'pli'}, {type: 'nack'} ] }; const expected = 'a=rtcp-fb:100 nack pli\r\n' + 'a=rtcp-fb:100 nack\r\n'; expect(SDPUtils.writeRtcpFb(codec)).to.equal(expected); }); it('serialized preferredPayloadType', () => { const codec = {preferredPayloadType: 100, rtcpFeedback: [ {type: 'nack'} ] }; const expected = 'a=rtcp-fb:100 nack\r\n'; expect(SDPUtils.writeRtcpFb(codec)).to.equal(expected); }); it('does nothing if there is no rtcp feedback', () => { const codec = {payloadType: 100, rtcpFeedback: [] }; expect(SDPUtils.writeRtcpFb(codec)).to.equal(''); }); }); }); it('getKind', () => { const mediaSection = 'm=video 9 UDP/TLS/RTP/SAVPF 120 126 97\r\n' + 'c=IN IP4 0.0.0.0\r\na=sendrecv\r\n'; expect(SDPUtils.getKind(mediaSection)).to.equal('video'); }); describe('getDirection', () => { const mediaSection = 'm=video 9 UDP/TLS/RTP/SAVPF 120 126 97\r\n' + 'c=IN IP4 0.0.0.0\r\na=sendonly\r\n'; describe('parses the direction from the mediaSection', () => { ['sendrecv', 'sendonly', 'recvonly', 'inactive'].forEach((direction) => { const modifiedSection = mediaSection.replace('sendonly', direction); expect(SDPUtils.getDirection(modifiedSection)).to.equal(direction); }); }); it('falls back to sendrecv', () => { expect(SDPUtils.getDirection('')).to.equal('sendrecv'); }); it('falls back to getting the direction from the session part', () => { expect(SDPUtils.getDirection('', 'a=sendonly')).to.equal('sendonly'); }); }); describe('isRejected', () => { it('returns true if the m-lines port is 0', () => { const rej = 'm=video 0 UDP/TLS/RTP/SAVPF 120 126 97\r\n'; expect(SDPUtils.isRejected(rej)).to.equal(true); }); it('returns false for a non-zero port', () => { const ok = 'm=video 9 UDP/TLS/RTP/SAVPF 120 126 97\r\n'; expect(SDPUtils.isRejected(ok)).to.equal(false); }); }); describe('parseMsid', () => { const spec = 'a=msid:stream track\r\n'; const planB = 'a=ssrc:1 msid:stream track\r\n'; const parsed = SDPUtils.parseMsid(spec); it('returns undefined if no msid is found', () => { expect(SDPUtils.parseMsid('')).to.equal(undefined); }); it('returns an object if a msid is found', () => { expect(parsed).to.be.an('Object'); }); it('parses the stream id', () => { expect(parsed.stream).to.equal('stream'); }); it('parses the track id', () => { expect(parsed.track).to.equal('track'); }); it('parses legacy plan B stuff', () => { let legacy = SDPUtils.parseMsid(planB); expect(legacy).to.be.an('Object'); expect(legacy.stream).to.equal('stream'); expect(legacy.track).to.equal('track'); }); }); describe('parseSsrcGroup', () => { const line = 'a=ssrc-group:FID 1 2'; const parsed = SDPUtils.parseSsrcGroup(line); it('returns an object', () => { expect(parsed).to.be.an('Object'); }); it('parses the semantics', () => { expect(parsed.semantics).to.equal('FID'); }); it('parses the ssrcs', () => { expect(parsed.ssrcs).to.deep.equal([1, 2]); }); }); describe('parseRtcpParameters', () => { const rtcp = SDPUtils.parseRtcpParameters(videoSDP); it('parses cname', () => { expect(rtcp.cname).to.equal('VrveQctHgkwqDKj6'); }); it('parses ssrc', () => { expect(rtcp.ssrc).to.equal(1734522595); }); it('parses reduced size', () => { expect(rtcp.reducedSize).to.equal(true); }); it('parses compoind', () => { expect(rtcp.compound).to.equal(false); }); it('parses mux', () => { expect(rtcp.mux).to.equal(true); }); }); describe('parseFingerprint', () => { const res = SDPUtils.parseFingerprint('a=fingerprint:ALG fp'); it('parses and lowercaseѕ the algorithm', () => { expect(res.algorithm).to.equal('alg'); }); it('parses the fingerprint value', () => { expect(res.value).to.equal('fp'); }); }); describe('getDtlsParameters', () => { const fp = 'a=fingerprint:sha-256 so:me:th:in:g1\r\n' + 'a=fingerprint:SHA-1 somethingelse'; const dtlsParameters = SDPUtils.getDtlsParameters(fp, ''); it('sets the role to auto', () => { expect(dtlsParameters.role).to.equal('auto'); }); it('parses two fingerprints', () => { expect(dtlsParameters.fingerprints.length).to.equal(2); }); it('extracts the algorithm', () => { expect(dtlsParameters.fingerprints[0].algorithm).to.equal('sha-256'); expect(dtlsParameters.fingerprints[1].algorithm).to.equal('sha-1'); }); it('extracts the fingerprints', () => { expect(dtlsParameters.fingerprints[0].value).to.equal('so:me:th:in:g1'); expect(dtlsParameters.fingerprints[1].value).to.equal('somethingelse'); }); }); describe('writeDtlsParameters', () => { const type = 'actpass'; const parameters = {fingerprints: [ {algorithm: 'sha-256', value: 'so:me:th:in:g1'}, {algorithm: 'SHA-1', value: 'somethingelse'} ]}; const serialized = SDPUtils.writeDtlsParameters(parameters, type); it('serializes the fingerprints', () => { expect(serialized).to.contain('a=fingerprint:sha-256 so:me:th:in:g1'); expect(serialized).to.contain('a=fingerprint:SHA-1 somethingelse'); }); it('serializes the type', () => { expect(serialized).to.contain('a=setup:' + type); }); }); describe('getIceParameters', () => { const sections = SDPUtils.splitSections(videoSDP); const ice = SDPUtils.getIceParameters(sections[1], sections[0]); it('returns an object', () => { expect(ice).to.be.an('Object'); }); it('parses the ufrag', () => { expect(ice.usernameFragment).to.equal('npaLWmWDg3Yp6vJt'); }); it('parses the password', () => { expect(ice.password).to.equal('pdfQZAiFbcsFmUKWw55g4TD5'); }); }); describe('writeIceParameters', () => { const serialized = SDPUtils.writeIceParameters({ usernameFragment: 'foo', password: 'bar' }); it('serializes the usernameFragment', () => { expect(serialized).to.contain('a=ice-ufrag:foo'); }); it('serializes the password', () => { expect(serialized).to.contain('a=ice-pwd:bar'); }); }); describe('getMid', () => { const mediaSection = 'm=video 9 UDP/TLS/RTP/SAVPF 120 126 97\r\n' + 'c=IN IP4 0.0.0.0\r\na=sendrecv\r\n'; it('returns undefined if no mid attribute is found', () => { expect(SDPUtils.getMid(mediaSection)).to.equal(undefined); }); it('returns the mid attribute', () => { expect(SDPUtils.getMid(mediaSection + 'a=mid:foo\r\n')).to.equal('foo'); }); }); describe('parseIceOptions', () => { const result = SDPUtils.parseIceOptions('a=ice-options:trickle something'); it('returns an array of options', () => { expect(result).to.be.an('Array'); expect(result.length).to.equal(2); expect(result[0]).to.equal('trickle'); expect(result[1]).to.equal('something'); }); }); describe('extmap', () => { describe('parseExtmap', () => { let res = SDPUtils.parseExtmap('a=extmap:2 uri'); it('parses the extmap id', () => { expect(res.id).to.equal(2); }); it('parses the extmap uri', () => { expect(res.uri).to.equal('uri'); }); it('parses the direction defaulting to sendrecv', () => { expect(res.direction).to.equal('sendrecv'); }); it('parses id and direction when direction is present', () => { res = SDPUtils.parseExtmap('a=extmap:2/sendonly uri'); expect(res.id === 2, 'parses extmap id when direction is present'); expect(res.direction === 'sendonly', 'parses extmap direction'); }); }); describe('writeExtmap', () => { it('writes extmap without direction', () => { expect(SDPUtils.writeExtmap({id: 1, uri: 'uri'})) .to.equal('a=extmap:1 uri\r\n'); }); it('writes extmap without direction when direction is ' + 'sendrecv (default)', () => { const result = SDPUtils.writeExtmap({id: 1, uri: 'uri', direction: 'sendrecv'}); expect(result).to.equal('a=extmap:1 uri\r\n'); }); it('writes extmap with direction when direction is not sendrecv', () => { expect(SDPUtils.writeExtmap({id: 1, uri: 'uri', direction: 'sendonly'})) .to.equal('a=extmap:1/sendonly uri\r\n'); }); it('writes extmap with preferredId when id is not present', () => { expect(SDPUtils.writeExtmap({preferredId: 1, uri: 'uri'})) .to.equal('a=extmap:1 uri\r\n'); }); }); }); describe('ice candidate', () => { describe('parsing', () => { const candidateString = 'candidate:702786350 2 udp 41819902 8.8.8.8 ' + '60769 typ relay raddr 8.8.8.8 rport 1234 ' + 'tcptype active ' + 'ufrag abc ' + 'generation 0'; const candidate = SDPUtils.parseCandidate(candidateString); it('parses foundation', () => { expect(candidate.foundation).to.equal('702786350'); }); it('parses component', () => { expect(candidate.component).to.equal(2); }); it('parses priority', () => { expect(candidate.priority).to.equal(41819902, 'parses priority'); }); it('parses ip', () => { expect(candidate.ip).to.equal('8.8.8.8'); }); it('sets address as an alias for ip', () => { expect(candidate.address).to.equal('8.8.8.8'); }); it('parses protocol', () => { expect(candidate.protocol).to.equal('udp'); }); it('parses port', () => { expect(candidate.port).to.equal(60769); }); it('parses type', () => { expect(candidate.type).to.equal('relay'); }); it('parses tcpType', () => { expect(candidate.tcpType).to.equal('active'); }); it('parses relatedAddress', () => { expect(candidate.relatedAddress).to.equal('8.8.8.8'); }); it('parses relatedPort', () => { expect(candidate.relatedPort).to.equal(1234); }); it('parses ufrag', () => { expect(candidate.ufrag).to.equal('abc'); }); it('parses ufrag as usernameFragment', () => { expect(candidate.usernameFragment).to.equal('abc'); }); it('parses an unknown key-value element like generation', () => { expect(candidate.generation).to.equal('0'); }); it('parses the candidate with the legacy a= prefix', () => { expect(SDPUtils.parseCandidate('a=' + candidateString).foundation) .to.equal('702786350'); }); }); describe('serialization', () => { let serialized; let candidate; beforeEach(() => { candidate = { foundation: '702786350', component: 2, protocol: 'udp', priority: 4189902, ip: '8.8.8.8', port: 60769, type: 'host' }; }); it('serializes a candidate with everything', () => { candidate.protocol = 'tcp'; candidate.tcpType = 'active'; candidate.type = 'relay'; candidate.relatedAddress = '8.8.8.8'; candidate.relatedPort = 1234; serialized = SDPUtils.writeCandidate(candidate).trim(); expect(serialized).to.equal('candidate:702786350 2 TCP 4189902 8.8.8.8 ' + '60769 typ relay raddr 8.8.8.8 rport 1234 tcptype active'); }); it('adds ufrag if present', () => { candidate.ufrag = 'abc'; serialized = SDPUtils.writeCandidate(candidate).trim(); expect(serialized).to.equal('candidate:702786350 2 UDP 4189902 8.8.8.8 ' + '60769 typ host ufrag abc'); }); it('adds ufrag if usernameFragment is present', () => { candidate.usernameFragment = 'abc'; serialized = SDPUtils.writeCandidate(candidate).trim(); expect(serialized).to.equal('candidate:702786350 2 UDP 4189902 8.8.8.8 ' + '60769 typ host ufrag abc'); }); it('does not add relatedAddress and relatedPort for host ' + 'candidates', () => { candidate.relatedAddress = '8.8.8.8'; candidate.relatedPort = 1234; serialized = SDPUtils.writeCandidate(candidate).trim(); expect(serialized).to.equal('candidate:702786350 2 UDP 4189902 8.8.8.8 ' + '60769 typ host'); }); it('ignores tcpType for udp candidates', () => { candidate.tcpType = 'active'; serialized = SDPUtils.writeCandidate(candidate).trim(); expect(serialized).to.equal('candidate:702786350 2 UDP 4189902 8.8.8.8 ' + '60769 typ host'); }); }); }); describe('writeRtpDescription', () => { const kind = 'audio'; let parameters; beforeEach(() => { parameters = { codecs: [{ payloadType: 111, name: 'opus', clockRate: 48000, numChannels: 2, parameters: { minptime: '10' } }], headerExtensions: [{ id: 2, uri: 'some:uri' }] }; }); it('generates a rejected m-line if the codecs are empty', () => { parameters.codecs = []; const serialized = SDPUtils.writeRtpDescription(kind, parameters); expect(serialized).to.contain('m=' + kind + ' 0 '); }); it('generates rtpmap lines', () => { const serialized = SDPUtils.writeRtpDescription(kind, parameters); expect(serialized).to.contain('a=rtpmap:111'); }); it('generates rtpmap lines using preferredPayloadType', () => { delete parameters.codecs[0].payloadType; parameters.codecs[0].preferredPayloadType = 111; const serialized = SDPUtils.writeRtpDescription(kind, parameters); expect(serialized).to.contain('a=rtpmap:111'); }); it('generates fmtp lines for codecs with parameters', () => { const serialized = SDPUtils.writeRtpDescription(kind, parameters); expect(serialized).to.contain('a=fmtp:'); }); it('does not generate fmtp lines for codecs with no parameters', () => { delete parameters.codecs[0].parameters; const serialized = SDPUtils.writeRtpDescription(kind, parameters); expect(serialized).not.to.contain('a=fmtp:'); }); it('generates rtcp-fb lines for codecs with rtcp-feedback', () => { parameters.codecs[0].rtcpFeedback = [{type: 'nack'}]; const serialized = SDPUtils.writeRtpDescription(kind, parameters); expect(serialized).to.contain('a=rtcp-fb:'); }); it('does not generate rtcp-fb lines for codecs with no feedback', () => { const serialized = SDPUtils.writeRtpDescription(kind, parameters); expect(serialized).not.to.contain('a=rtcp-fb:'); }); it('generates extmap lines for headerExtensions', () => { const serialized = SDPUtils.writeRtpDescription(kind, parameters); expect(serialized).to.contain('a=extmap:2 some:uri\r\n'); }); it('does not generate extmap lines if headerExtensions is empty', () => { parameters.headerExtensions = []; const serialized = SDPUtils.writeRtpDescription(kind, parameters); expect(serialized).not.to.contain('a=extmap:'); }); it('does not generate extmap lines if there are no headerExtensions', () => { delete parameters.headerExtensions; const serialized = SDPUtils.writeRtpDescription(kind, parameters); expect(serialized).not.to.contain('a=extmap:'); }); }); describe('sctp', () => { const oldSDP = 'm=application 63743 DTLS/SCTP 5000\r\n' + 'a=sctpmap:5000 webrtc-datachannel 256\r\n'; const parsedOld = SDPUtils.parseSctpDescription(oldSDP); describe('parsing old form', () => { it('parses port', () => { expect(parsedOld.port).to.equal(5000); }); it('parses protocol', () => { expect(parsedOld.protocol).to.equal('webrtc-datachannel'); }); it('default max message size', () => { expect(parsedOld.maxMessageSize).to.equal(65536); }); }); const newSDP = 'm=application 54111 UDP/DTLS/SCTP webrtc-datachannel\r\n' + 'a=sctp-port:5000\r\n' + 'a=max-message-size:1024\r\n'; const parsedNew = SDPUtils.parseSctpDescription(newSDP); describe('parsing new form', () => { it('parses port', () => { expect(parsedNew.port).to.equal(5000); }); it('parsed protocol from m-line', () => { expect(parsedNew.protocol).to.equal('webrtc-datachannel'); }); it('parsed max message size', () => { expect(parsedNew.maxMessageSize).to.equal(1024); }); }); }); describe('writeSctpDescription', () => { let parameters; let media = { kind: 'application', protocol: 'UDP/DTLS/SCTP' }; beforeEach(() => { parameters = { protocol: 'webrtc-datachannel', port: 5000, maxMessageSize: 1024 }; }); it('generates correct m line', () => { const serialized = SDPUtils.writeSctpDescription(media, parameters); expect(serialized).to.contain( 'm=application 9 UDP/DTLS/SCTP webrtc-datachannel' ); }); it('generates sctp-port lines', () => { const serialized = SDPUtils.writeSctpDescription(media, parameters); expect(serialized).to.contain('a=sctp-port:5000'); }); it('generates max-message-size lines', () => { const serialized = SDPUtils.writeSctpDescription(media, parameters); expect(serialized).to.contain('a=max-message-size:1024'); }); }); describe('writeSctpDescription (old format answer)', () => { let parameters; let media = { kind: 'application', protocol: 'DTLS/SCTP' }; beforeEach(() => { parameters = { protocol: 'webrtc-datachannel', port: 5000, maxMessageSize: 1024 }; }); it('generates correct m line', () => { const serialized = SDPUtils.writeSctpDescription(media, parameters); expect(serialized).to.contain( 'm=application 9 DTLS/SCTP 5000' ); }); it('generates sctpmap lines', () => { const serialized = SDPUtils.writeSctpDescription(media, parameters); expect(serialized).to.contain('a=sctpmap:5000 webrtc-datachannel 65535'); }); it('generates max-message-size lines', () => { const serialized = SDPUtils.writeSctpDescription(media, parameters); expect(serialized).to.contain('a=max-message-size:1024'); }); }); describe('writeBoilerPlate', () => { let sdp; beforeEach(() => { sdp = SDPUtils.writeSessionBoilerplate(); }); it('returns a string', () => { expect(sdp).to.be.a('String'); }); it('generates unique id', () => { expect(sdp).to.not.equal(SDPUtils.writeSessionBoilerplate()); }); it('uses passed in session id', () => { let sessionId = 'uniqueTestSessionId1'; let sdpWithSession = SDPUtils.writeSessionBoilerplate(sessionId); expect(sdpWithSession).to.include(' ' + sessionId + ' '); }); it('defaults to version 2', () => { let ver = 4404; let id = 123; let sdpWithSessionVersion = SDPUtils.writeSessionBoilerplate(id, ver); expect(sdpWithSessionVersion).to.include(id + ' ' + ver + ' '); }); it('uses passed session version', () => { let ver = 4404; let sdpWithSessionVersion = SDPUtils.writeSessionBoilerplate(undefined, ver); expect(sdpWithSessionVersion).to.include(' ' + ver + ' '); }); }); describe('parseMLine', () => { const mLine = 'm=video 9 UDP/TLS/RTP/SAVPF 100 101 107 116 117 96 97 99 98'; const result = SDPUtils.parseMLine(mLine); it('parses the kind', () => { expect(result.kind).to.equal('video'); }); it('parses the port as an integer', () => { expect(result.port).to.equal(9); }); it('parses the protocol', () => { expect(result.protocol).to.equal('UDP/TLS/RTP/SAVPF'); }); it('parses the format list', () => { expect(result.fmt).to.equal('100 101 107 116 117 96 97 99 98'); }); }); describe('parseOLine', () => { const result = SDPUtils.parseOLine('o=username someid 15 IN IP4 0.0.0.0'); it('parses the username', () => expect(result.username).to.equal('username')); it('parse the session id', () => expect(result.sessionId).to.equal('someid')); it('parses the session version', () => expect(result.sessionVersion).to.equal(15)); it('parses the netType', () => expect(result.netType).to.equal('IN')); it('parses the addressType', () => expect(result.addressType).to.equal('IP4')); it('parses the address', () => expect(result.address).to.equal('0.0.0.0')); }); describe('isValidSDP', () => { it('returns false for non-string input', () => expect(SDPUtils.isValidSDP(1)).to.equal(false)); it('returns false for the empty string', () => expect(SDPUtils.isValidSDP('')).to.equal(false)); it('returns false if there are empty lines', () => expect(SDPUtils.isValidSDP('v=0\r\n\r\nm=...\r\n')).to.equal(false)); it('returns false if the syntax is not key equals value', () => expect(SDPUtils.isValidSDP('v?0\r\n')).to.equal(false)); it('returns true for valid sdp', () => expect(SDPUtils.isValidSDP(videoSDP)).to.equal(true)); });