pax_global_header00006660000000000000000000000064125023721600014510gustar00rootroot0000000000000052 comment=a20ed4914b644dd7ab7fa2444409e85fb0af7db9 sdp-transform-1.4.0/000077500000000000000000000000001250237216000143115ustar00rootroot00000000000000sdp-transform-1.4.0/.gitignore000066400000000000000000000000251250237216000162760ustar00rootroot00000000000000node_modules lib-cov sdp-transform-1.4.0/.travis.yml000066400000000000000000000001471250237216000164240ustar00rootroot00000000000000language: node_js node_js: - 0.10 notifications: email: true after_script: - npm run coveralls sdp-transform-1.4.0/CHANGELOG.md000066400000000000000000000070041250237216000161230ustar00rootroot000000000000001.4.0 / 2015-03-18 ================== * Add support for `a=rtcp-rsize` 1.3.0 / 2015-03-16 ================== * Add support for `a=end-of-candidates` trickle ice attribute 1.2.1 / 2015-03-15 ================== * Add parsing for a=ssrc-group 1.2.0 / 2015-03-05 ================== * a=msid attributes support and msid-semantic improvements * writer now ignores `undefined` or `null` values 1.1.0 / 2014-10-20 ================== * Add support for parsing session level `a=ice-lite` 1.0.0 / 2014-09-30 ================== * Be more lenient with nelines. Allow \r\n, \r or \n. 0.6.1 / 2014-07-25 ================== * Documentation and test coverage release 0.6.0 / 2014-02-18 ================== * invalid a= lines are now parsed verbatim in `media[i].invalid` (#19) * everything in `media[i].invalid` is written out verbatim (#19) * add basic RTSP support (a=control lines) (#20) 0.5.3 / 2014-01-17 ================== * ICE candidates now parsed fully (no longer ignoring optional attrs) (#13) 0.5.2 / 2014-01-17 ================== * Remove `util` dependency to help browserify users * Better parsing of `a=extmap`, `a=crypto` and `a=rtcp-fb` lines * `sdp-verify` bin file included to help discover effects of `write ∘ parse` 0.5.1 / 2014-01-16 ================== * Correctly parse a=rtpmap with telephone-event codec #16 * Correctly parse a=rtcp lines that conditionally include the IP #16 0.5.0 / 2014-01-14 ================== * Enforce spec mandated \r\n line endings over \n (#15) * Parsing of opus rtpmap wrong because encoding parameters were discarded (#12) 0.4.1 / 2013-12-19 ================== * Changed 'sendrecv' key on media streams to be called 'direction' to match SDP related RFCs (thanks to @saghul) 0.3.3 / 2013-12-10 ================== * Fixed a bug that caused time description lines ("t=" and "z=") to be in the wrong place 0.3.2 / 2013-10-21 ================== * Fixed a bug where large sessionId values where being rounded (#8) * Optionally specify the `outerOrder` and `innerOrder` for the writer (allows working around Chrome not following the RFC specified order in #7) 0.3.1 / 2013-10-19 ================== * Fixed a bug that meant the writer didn't write the last newline (#6) 0.3.0 / 2013-10-18 ================== * Changed ext grammar to parse id and direction as one (fixes writing bug) * Allow mid to be a string (fixes bug) * Add support for maxptime value * Add support for ice-options * Add support for grouping frameworks * Add support for msid-semantic * Add support for ssrc * Add support for rtcp-mux * Writer improvements: add support for session level push attributes 0.2.1 / 2013-07-31 ================== * Support release thanks to @legastero, following was pulled from his fork: * Add support for rtcp-fb attributes. * Add support for header extension (extmap) attributes. * Add support for crypto attributes. * Add remote-candidates attribute support and parser. 0.2.0 / 2013-07-27 ================== * parse most normal lines sensibly * factored out grammar properly * added a writer that uses common grammar * stop preprocessing parse object explicitly (so that parser ∘ writer == Id) these parser helpers are instead exposed (may in the future be extended) 0.1.0 / 2013-07-21 ================== * rewrite parsing mechanism * parse origin lines more efficiently * parsing output now significantly different 0.0.2 / 2012-07-18 ================== * ice properties parsed 0.0.1 / 2012-07-17 ================== * Original release sdp-transform-1.4.0/LICENSE000066400000000000000000000020671250237216000153230ustar00rootroot00000000000000(The MIT License) Copyright (c) 2013 Eirik Albrigtsen 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-transform-1.4.0/README.md000066400000000000000000000125541250237216000155770ustar00rootroot00000000000000# SDP Transform [![npm status](http://img.shields.io/npm/v/sdp-transform.svg)](https://www.npmjs.org/package/sdp-transform) [![build status](https://secure.travis-ci.org/clux/sdp-transform.svg)](http://travis-ci.org/clux/sdp-transform) [![dependency status](https://david-dm.org/clux/sdp-transform.svg)](https://david-dm.org/clux/sdp-transform) [![coverage status](http://img.shields.io/coveralls/clux/sdp-transform.svg)](https://coveralls.io/r/clux/sdp-transform) [![unstable](http://img.shields.io/badge/stability-unstable-E5AE13.svg)](http://nodejs.org/api/documentation.html#documentation_stability_index) A simple parser and writer of SDP. Defines internal grammar based on [RFC4566 - SDP](http://tools.ietf.org/html/rfc4566), [RFC5245 - ICE](http://tools.ietf.org/html/rfc5245), and many more. For simplicity it will force values that are integers to integers and leave everything else as strings when parsing. The module should be simple to extend or build upon, and is constructed rigorously. ## Usage - Parser Require it and pass it an unprocessed SDP string. ```js var transform = require('sdp-transform'); var sdpStr = "v=0\r\n\ o=- 20518 0 IN IP4 203.0.113.1\r\n\ s= \r\n\ t=0 0\r\n\ c=IN IP4 203.0.113.1\r\n\ a=ice-ufrag:F7gI\r\n\ a=ice-pwd:x9cml/YzichV2+XlhiMu8g\r\n\ a=fingerprint:sha-1 42:89:c5:c6:55:9d:6e:c8:e8:83:55:2a:39:f9:b6:eb:e9:a3:a9:e7\r\n\ m=audio 54400 RTP/SAVPF 0 96\r\n\ a=rtpmap:0 PCMU/8000\r\n\ a=rtpmap:96 opus/48000\r\n\ a=ptime:20\r\n\ a=sendrecv\r\n\ a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host\r\n\ a=candidate:1 2 UDP 2113667326 203.0.113.1 54401 typ host\r\n\ m=video 55400 RTP/SAVPF 97 98\r\n\ a=rtpmap:97 H264/90000\r\n\ a=fmtp:97 profile-level-id=4d0028;packetization-mode=1\r\n\ a=rtpmap:98 VP8/90000\r\n\ a=sendrecv\r\n\ a=candidate:0 1 UDP 2113667327 203.0.113.1 55400 typ host\r\n\ a=candidate:1 2 UDP 2113667326 203.0.113.1 55401 typ host\r\n\ "; var res = transform.parse(sdpStr); res; { version: 0, origin: { username: '-', sessionId: 20518, sessionVersion: 0, netType: 'IN', ipVer: 4, address: '203.0.113.1' }, name: '', timing: { start: 0, stop: 0 }, connection: { version: 4, ip: '203.0.113.1' }, iceUfrag: 'F7gI', icePwd: 'x9cml/YzichV2+XlhiMu8g', fingerprint: { type: 'sha-1', hash: '42:89:c5:c6:55:9d:6e:c8:e8:83:55:2a:39:f9:b6:eb:e9:a3:a9:e7' }, media: [ { rtp: [Object], fmtp: [], type: 'audio', port: 54400, protocol: 'RTP/SAVPF', payloads: '0 96', ptime: 20, direction: 'sendrecv', candidates: [Object] }, { rtp: [Object], fmtp: [Object], type: 'video', port: 55400, protocol: 'RTP/SAVPF', payloads: '97 98', direction: 'sendrecv', candidates: [Object] } ] } // each media line is parsed into the following format res.media[1]; { rtp: [ { payload: 97, codec: 'H264', rate: 90000 }, { payload: 98, codec: 'VP8', rate: 90000 } ], fmtp: [ { payload: 97, config: 'profile-level-id=4d0028;packetization-mode=1' } ], type: 'video', port: 55400, protocol: 'RTP/SAVPF', payloads: '97 98', direction: 'sendrecv', candidates: [ { foundation: 0, component: 1, transport: 'UDP', priority: 2113667327, ip: '203.0.113.1', port: 55400, type: 'host' }, { foundation: 1, component: 2, transport: 'UDP', priority: 2113667326, ip: '203.0.113.1', port: 55401, type: 'host' } ] } ``` In this example, only slightly dodgy string coercion case here is for `candidates[i].foundation`, which can be a string, but in this case can be equally parsed as an integer. ### Parser Postprocessing No excess parsing is done to the raw strings apart from maybe coercing to ints, because the writer is built to be the inverse of the parser. That said, a few helpers have been built in: ```js // to parse the fmtp.config from the previous example transform.parseFmtpConfig(res.media[1].fmtp[0].config); { 'profile-level-id': '4d0028', 'packetization-mode': 1 } // what payloads where actually advertised in the main m-line ? transform.parsePayloads(res.media[1].payloads); [97, 98] ``` ## Usage - Writer The writer is the inverse of the parser, and will need a struct equivalent to the one returned by it. ```js transform.write(res).split('\r\n'); // res parsed above [ 'v=0', 'o=- 20518 0 IN IP4 203.0.113.1', 's= ', 'c=IN IP4 203.0.113.1', 't=0 0', 'a=ice-ufrag:F7gI', 'a=ice-pwd:x9cml/YzichV2+XlhiMu8g', 'a=fingerprint:sha-1 42:89:c5:c6:55:9d:6e:c8:e8:83:55:2a:39:f9:b6:eb:e9:a3:a9:e7', 'm=audio 54400 RTP/SAVPF 0 96', 'a=rtpmap:0 PCMU/8000', 'a=rtpmap:96 opus/48000', 'a=ptime:20', 'a=sendrecv', 'a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host', 'a=candidate:1 2 UDP 2113667326 203.0.113.1 54401 typ host', 'm=video 55400 RTP/SAVPF 97 98', 'a=rtpmap:97 H264/90000', 'a=rtpmap:98 VP8/90000', 'a=fmtp:97 profile-level-id=4d0028;packetization-mode=1', 'a=sendrecv', 'a=candidate:0 1 UDP 2113667327 203.0.113.1 55400 typ host', 'a=candidate:1 2 UDP 2113667326 203.0.113.1 55401 typ host' ] ``` The only thing different from the original input is we follow the order specified by the SDP RFC, and we will always do so. ## Installation Install locally from npm: ```bash $ npm install sdp-transform ``` ## License MIT-Licensed. See LICENSE file for details. sdp-transform-1.4.0/checker.js000077500000000000000000000024701250237216000162610ustar00rootroot00000000000000#!/usr/bin/env node var transform = require('./') , file = require('path').join(process.cwd(), process.argv[2]) , sdp = require('fs').readFileSync(file).toString() , parsed = transform.parse(sdp) , written = transform.write(parsed) , writtenLines = written.split('\r\n') , origLines = sdp.split('\r\n') , numMissing = 0 , numNew = 0 ; var parseFails = 0; parsed.media.forEach(function (media, i) { (media.invalid || []).forEach(function (inv) { console.warn('unrecognized a=' + inv.value + ' belonging to m=' + media.type); parseFails += 1; }); }); var parseStr = parseFails + ' unrecognized line(s) copied blindly'; origLines.forEach(function (line, i) { if (writtenLines.indexOf(line) < 0) { console.error('l' + i + ' lost (' + line + ')'); numMissing += 1; } }); writtenLines.forEach(function (line, i) { if (origLines.indexOf(line) < 0) { console.error('l' + i + ' new (' + line + ')'); numNew += 1; } }); var failed = (numMissing > 0 || numNew > 0); if (failed) { console.log('\n' + file + ' changes during transform:'); console.log(numMissing + ' missing line(s), ' + numNew + ' new line(s)%s', parseFails > 0 ? ', ' + parseStr : '' ); } else { console.log(file + ' verified%s', parseFails > 0 ? ', but had ' + parseStr : ''); } process.exit(failed ? 1 : 0); sdp-transform-1.4.0/lib/000077500000000000000000000000001250237216000150575ustar00rootroot00000000000000sdp-transform-1.4.0/lib/grammar.js000066400000000000000000000167061250237216000170550ustar00rootroot00000000000000var grammar = module.exports = { v: [{ name: 'version', reg: /^(\d*)$/ }], o: [{ //o=- 20518 0 IN IP4 203.0.113.1 // NB: sessionId will be a String in most cases because it is huge name: 'origin', reg: /^(\S*) (\d*) (\d*) (\S*) IP(\d) (\S*)/, names: ['username', 'sessionId', 'sessionVersion', 'netType', 'ipVer', 'address'], format: "%s %s %d %s IP%d %s" }], // default parsing of these only (though some of these feel outdated) s: [{ name: 'name' }], i: [{ name: 'description' }], u: [{ name: 'uri' }], e: [{ name: 'email' }], p: [{ name: 'phone' }], z: [{ name: 'timezones' }], // TODO: this one can actually be parsed properly.. r: [{ name: 'repeats' }], // TODO: this one can also be parsed properly //k: [{}], // outdated thing ignored t: [{ //t=0 0 name: 'timing', reg: /^(\d*) (\d*)/, names: ['start', 'stop'], format: "%d %d" }], c: [{ //c=IN IP4 10.47.197.26 name: 'connection', reg: /^IN IP(\d) (\S*)/, names: ['version', 'ip'], format: "IN IP%d %s" }], b: [{ //b=AS:4000 push: 'bandwidth', reg: /^(TIAS|AS|CT|RR|RS):(\d*)/, names: ['type', 'limit'], format: "%s:%s" }], m: [{ //m=video 51744 RTP/AVP 126 97 98 34 31 // NB: special - pushes to session // TODO: rtp/fmtp should be filtered by the payloads found here? reg: /^(\w*) (\d*) ([\w\/]*)(?: (.*))?/, names: ['type', 'port', 'protocol', 'payloads'], format: "%s %d %s %s" }], a: [ { //a=rtpmap:110 opus/48000/2 push: 'rtp', reg: /^rtpmap:(\d*) ([\w\-]*)\/(\d*)(?:\s*\/(\S*))?/, names: ['payload', 'codec', 'rate', 'encoding'], format: function (o) { return (o.encoding) ? "rtpmap:%d %s/%s/%s": "rtpmap:%d %s/%s"; } }, { //a=fmtp:108 profile-level-id=24;object=23;bitrate=64000 push: 'fmtp', reg: /^fmtp:(\d*) (\S*)/, names: ['payload', 'config'], format: "fmtp:%d %s" }, { //a=control:streamid=0 name: 'control', reg: /^control:(.*)/, format: "control:%s" }, { //a=rtcp:65179 IN IP4 193.84.77.194 name: 'rtcp', reg: /^rtcp:(\d*)(?: (\S*) IP(\d) (\S*))?/, names: ['port', 'netType', 'ipVer', 'address'], format: function (o) { return (o.address != null) ? "rtcp:%d %s IP%d %s": "rtcp:%d"; } }, { //a=rtcp-fb:98 trr-int 100 push: 'rtcpFbTrrInt', reg: /^rtcp-fb:(\*|\d*) trr-int (\d*)/, names: ['payload', 'value'], format: "rtcp-fb:%d trr-int %d" }, { //a=rtcp-fb:98 nack rpsi push: 'rtcpFb', reg: /^rtcp-fb:(\*|\d*) ([\w-_]*)(?: ([\w-_]*))?/, names: ['payload', 'type', 'subtype'], format: function (o) { return (o.subtype != null) ? "rtcp-fb:%s %s %s": "rtcp-fb:%s %s"; } }, { //a=extmap:2 urn:ietf:params:rtp-hdrext:toffset //a=extmap:1/recvonly URI-gps-string push: 'ext', reg: /^extmap:([\w_\/]*) (\S*)(?: (\S*))?/, names: ['value', 'uri', 'config'], // value may include "/direction" suffix format: function (o) { return (o.config != null) ? "extmap:%s %s %s": "extmap:%s %s"; } }, { //a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|2^20|1:32 push: 'crypto', reg: /^crypto:(\d*) ([\w_]*) (\S*)(?: (\S*))?/, names: ['id', 'suite', 'config', 'sessionConfig'], format: function (o) { return (o.sessionConfig != null) ? "crypto:%d %s %s %s": "crypto:%d %s %s"; } }, { //a=setup:actpass name: 'setup', reg: /^setup:(\w*)/, format: "setup:%s" }, { //a=mid:1 name: 'mid', reg: /^mid:([^\s]*)/, format: "mid:%s" }, { //a=msid:0c8b064d-d807-43b4-b434-f92a889d8587 98178685-d409-46e0-8e16-7ef0db0db64a name: 'msid', reg: /^msid:(.*)/, format: "msid:%s" }, { //a=ptime:20 name: 'ptime', reg: /^ptime:(\d*)/, format: "ptime:%d" }, { //a=maxptime:60 name: 'maxptime', reg: /^maxptime:(\d*)/, format: "maxptime:%d" }, { //a=sendrecv name: 'direction', reg: /^(sendrecv|recvonly|sendonly|inactive)/ }, { //a=ice-lite name: 'icelite', reg: /^(ice-lite)/ }, { //a=ice-ufrag:F7gI name: 'iceUfrag', reg: /^ice-ufrag:(\S*)/, format: "ice-ufrag:%s" }, { //a=ice-pwd:x9cml/YzichV2+XlhiMu8g name: 'icePwd', reg: /^ice-pwd:(\S*)/, format: "ice-pwd:%s" }, { //a=fingerprint:SHA-1 00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33 name: 'fingerprint', reg: /^fingerprint:(\S*) (\S*)/, names: ['type', 'hash'], format: "fingerprint:%s %s" }, { //a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host //a=candidate:1162875081 1 udp 2113937151 192.168.34.75 60017 typ host generation 0 //a=candidate:3289912957 2 udp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 generation 0 push:'candidates', reg: /^candidate:(\S*) (\d*) (\S*) (\d*) (\S*) (\d*) typ (\S*)(?: raddr (\S*) rport (\d*))?(?: generation (\d*))?/, names: ['foundation', 'component', 'transport', 'priority', 'ip', 'port', 'type', 'raddr', 'rport', 'generation'], format: function (o) { var str = "candidate:%s %d %s %d %s %d typ %s"; // NB: candidate has two optional chunks, so %void middle one if it's missing str += (o.raddr != null) ? " raddr %s rport %d" : "%v%v"; if (o.generation != null) { str += " generation %d"; } return str; } }, { //a=end-of-candidates (keep after the candidates line for readability) name: 'endOfCandidates', reg: /^(end-of-candidates)/ }, { //a=remote-candidates:1 203.0.113.1 54400 2 203.0.113.1 54401 ... name: 'remoteCandidates', reg: /^remote-candidates:(.*)/, format: "remote-candidates:%s" }, { //a=ice-options:google-ice name: 'iceOptions', reg: /^ice-options:(\S*)/, format: "ice-options:%s" }, { //a=ssrc:2566107569 cname:t9YU8M1UxTF8Y1A1 push: "ssrcs", reg: /^ssrc:(\d*) ([\w_]*):(.*)/, names: ['id', 'attribute', 'value'], format: "ssrc:%d %s:%s" }, { //a=ssrc-group:FEC 1 2 push: "ssrcGroups", reg: /^ssrc-group:(\w*) (.*)/, names: ['semantics', 'ssrcs'], format: "ssrc-group:%s %s" }, { //a=msid-semantic: WMS Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlV name: "msidSemantic", reg: /^msid-semantic:\s?(\w*) (\S*)/, names: ['semantic', 'token'], format: "msid-semantic: %s %s" // space after ":" is not accidental }, { //a=group:BUNDLE audio video push: 'groups', reg: /^group:(\w*) (.*)/, names: ['type', 'mids'], format: "group:%s %s" }, { //a=rtcp-mux name: 'rtcpMux', reg: /^(rtcp-mux)/ }, { //a=rtcp-rsize name: 'rtcpRsize', reg: /^(rtcp-rsize)/ }, { // any a= that we don't understand is kepts verbatim on media.invalid push: 'invalid', names: ["value"] } ] }; // set sensible defaults to avoid polluting the grammar with boring details Object.keys(grammar).forEach(function (key) { var objs = grammar[key]; objs.forEach(function (obj) { if (!obj.reg) { obj.reg = /(.*)/; } if (!obj.format) { obj.format = "%s"; } }); }); sdp-transform-1.4.0/lib/index.js000066400000000000000000000004311250237216000165220ustar00rootroot00000000000000var parser = require('./parser'); var writer = require('./writer'); exports.write = writer; exports.parse = parser.parse; exports.parseFmtpConfig = parser.parseFmtpConfig; exports.parsePayloads = parser.parsePayloads; exports.parseRemoteCandidates = parser.parseRemoteCandidates; sdp-transform-1.4.0/lib/parser.js000066400000000000000000000045201250237216000167120ustar00rootroot00000000000000var toIntIfInt = function (v) { return String(Number(v)) === v ? Number(v) : v; }; var attachProperties = function (match, location, names, rawName) { if (rawName && !names) { location[rawName] = toIntIfInt(match[1]); } else { for (var i = 0; i < names.length; i += 1) { if (match[i+1] != null) { location[names[i]] = toIntIfInt(match[i+1]); } } } }; var parseReg = function (obj, location, content) { var needsBlank = obj.name && obj.names; if (obj.push && !location[obj.push]) { location[obj.push] = []; } else if (needsBlank && !location[obj.name]) { location[obj.name] = {}; } var keyLocation = obj.push ? {} : // blank object that will be pushed needsBlank ? location[obj.name] : location; // otherwise, named location or root attachProperties(content.match(obj.reg), keyLocation, obj.names, obj.name); if (obj.push) { location[obj.push].push(keyLocation); } }; var grammar = require('./grammar'); var validLine = RegExp.prototype.test.bind(/^([a-z])=(.*)/); exports.parse = function (sdp) { var session = {} , media = [] , location = session; // points at where properties go under (one of the above) // parse lines we understand sdp.split(/(\r\n|\r|\n)/).filter(validLine).forEach(function (l) { var type = l[0]; var content = l.slice(2); if (type === 'm') { media.push({rtp: [], fmtp: []}); location = media[media.length-1]; // point at latest media line } for (var j = 0; j < (grammar[type] || []).length; j += 1) { var obj = grammar[type][j]; if (obj.reg.test(content)) { return parseReg(obj, location, content); } } }); session.media = media; // link it up return session; }; var fmtpReducer = function (acc, expr) { var s = expr.split('='); if (s.length === 2) { acc[s[0]] = toIntIfInt(s[1]); } return acc; }; exports.parseFmtpConfig = function (str) { return str.split(';').reduce(fmtpReducer, {}); }; exports.parsePayloads = function (str) { return str.split(' ').map(Number); }; exports.parseRemoteCandidates = function (str) { var candidates = []; var parts = str.split(' ').map(toIntIfInt); for (var i = 0; i < parts.length; i += 3) { candidates.push({ component: parts[i], ip: parts[i + 1], port: parts[i + 2] }); } return candidates; }; sdp-transform-1.4.0/lib/writer.js000066400000000000000000000060151250237216000167330ustar00rootroot00000000000000var grammar = require('./grammar'); // customized util.format - discards excess arguments and can void middle ones var formatRegExp = /%[sdv%]/g; var format = function (formatStr) { var i = 1; var args = arguments; var len = args.length; return formatStr.replace(formatRegExp, function (x) { if (i >= len) { return x; // missing argument } var arg = args[i]; i += 1; switch (x) { case '%%': return '%'; case '%s': return String(arg); case '%d': return Number(arg); case '%v': return ''; } }); // NB: we discard excess arguments - they are typically undefined from makeLine }; var makeLine = function (type, obj, location) { var str = obj.format instanceof Function ? (obj.format(obj.push ? location : location[obj.name])) : obj.format; var args = [type + '=' + str]; if (obj.names) { for (var i = 0; i < obj.names.length; i += 1) { var n = obj.names[i]; if (obj.name) { args.push(location[obj.name][n]); } else { // for mLine and push attributes args.push(location[obj.names[i]]); } } } else { args.push(location[obj.name]); } return format.apply(null, args); }; // RFC specified order // TODO: extend this with all the rest var defaultOuterOrder = [ 'v', 'o', 's', 'i', 'u', 'e', 'p', 'c', 'b', 't', 'r', 'z', 'a' ]; var defaultInnerOrder = ['i', 'c', 'b', 'a']; module.exports = function (session, opts) { opts = opts || {}; // ensure certain properties exist if (session.version == null) { session.version = 0; // "v=0" must be there (only defined version atm) } if (session.name == null) { session.name = " "; // "s= " must be there if no meaningful name set } session.media.forEach(function (mLine) { if (mLine.payloads == null) { mLine.payloads = ""; } }); var outerOrder = opts.outerOrder || defaultOuterOrder; var innerOrder = opts.innerOrder || defaultInnerOrder; var sdp = []; // loop through outerOrder for matching properties on session outerOrder.forEach(function (type) { grammar[type].forEach(function (obj) { if (obj.name in session && session[obj.name] != null) { sdp.push(makeLine(type, obj, session)); } else if (obj.push in session && session[obj.push] != null) { session[obj.push].forEach(function (el) { sdp.push(makeLine(type, obj, el)); }); } }); }); // then for each media line, follow the innerOrder session.media.forEach(function (mLine) { sdp.push(makeLine('m', grammar.m[0], mLine)); innerOrder.forEach(function (type) { grammar[type].forEach(function (obj) { if (obj.name in mLine && mLine[obj.name] != null) { sdp.push(makeLine(type, obj, mLine)); } else if (obj.push in mLine && mLine[obj.push] != null) { mLine[obj.push].forEach(function (el) { sdp.push(makeLine(type, obj, el)); }); } }); }); }); return sdp.join('\r\n') + '\r\n'; }; sdp-transform-1.4.0/package.json000066400000000000000000000015321250237216000166000ustar00rootroot00000000000000{ "name": "sdp-transform", "description": "A simple parser/writer for the Session Description Protocol", "author": "Eirik Albrigtsen ", "version": "1.4.0", "stability": "unstable", "repository": { "type": "git", "url": "clux/sdp-transform" }, "keywords": [ "sdp", "webrtc", "serializer" ], "main": "./lib/", "bin": { "sdp-verify": "./checker.js" }, "scripts": { "test": "nodeunit --reporter=verbose test/*.js", "coverage": "jscoverage lib && SDP_TRANSFORM_COV=1 nodeunit --reporter=lcov test", "coveralls": "npm run coverage | coveralls" }, "dependencies": {}, "devDependencies": { "coveralls": "^2.11.1", "jscoverage": "^0.5.5", "nodeunit": "^0.9.0" }, "bugs": { "url": "http://github.com/clux/sdp-transform/issues" }, "license": "MIT" } sdp-transform-1.4.0/test/000077500000000000000000000000001250237216000152705ustar00rootroot00000000000000sdp-transform-1.4.0/test/chrome.sdp000066400000000000000000000034231250237216000172570ustar00rootroot00000000000000v=0 o=- 3710604898417546434 2 IN IP4 127.0.0.1 s=- t=0 0 a=group:BUNDLE audio video a=msid-semantic: WMS Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlV m=audio 1 RTP/SAVPF 111 103 104 0 8 107 106 105 13 126 c=IN IP4 0.0.0.0 a=rtcp:1 IN IP4 0.0.0.0 a=ice-ufrag:lat6xwB1/flm+VwG a=ice-pwd:L5+HonleGeFHa8jPZLc/kr0E a=ice-options:google-ice a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level a=sendrecv a=mid:audio a=rtcp-mux a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:8QVQSHJ2AM8gIumHpYRRdWHyZ5NkLhaTD1AENOWx a=rtpmap:111 opus/48000/2 a=fmtp:111 minptime=10 a=rtpmap:103 ISAC/16000 a=rtpmap:104 ISAC/32000 a=rtpmap:0 PCMU/8000 a=rtpmap:8 PCMA/8000 a=rtpmap:107 CN/48000 a=rtpmap:106 CN/32000 a=rtpmap:105 CN/16000 a=rtpmap:13 CN/8000 a=rtpmap:126 telephone-event/8000 a=maxptime:60 a=ssrc:2754920552 cname:t9YU8M1UxTF8Y1A1 a=ssrc:2754920552 msid:Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlV Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlVa0 a=ssrc:2754920552 mslabel:Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlV a=ssrc:2754920552 label:Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlVa0 m=video 1 RTP/SAVPF 100 116 117 c=IN IP4 0.0.0.0 a=rtcp:12312 a=ice-ufrag:lat6xwB1/flm+VwG a=ice-pwd:L5+HonleGeFHa8jPZLc/kr0E a=ice-options:google-ice a=extmap:2 urn:ietf:params:rtp-hdrext:toffset a=sendrecv a=mid:video a=rtcp-mux a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:8QVQSHJ2AM8gIumHpYRRdWHyZ5NkLhaTD1AENOWx a=rtpmap:100 VP8/90000 a=rtcp-fb:100 ccm fir a=rtcp-fb:100 nack a=rtcp-fb:100 goog-remb a=rtpmap:116 red/90000 a=rtpmap:117 ulpfec/90000 a=ssrc:2566107569 cname:t9YU8M1UxTF8Y1A1 a=ssrc:2566107569 msid:Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlV Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlVv0 a=ssrc:2566107569 mslabel:Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlV a=ssrc:2566107569 label:Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlVv0 sdp-transform-1.4.0/test/compose.test.js000066400000000000000000000023761250237216000202610ustar00rootroot00000000000000var fs = require('fs') , main = require(process.env.SDP_TRANSFORM_COV ? '../lib-cov' : '../') , parse = main.parse , write = main.write; var verifyCompose = function (file, t) { fs.readFile(__dirname + '/' + file, function (err, sdp) { if (err) { t.ok(false, "failed to read file:" + err); return t.done(); } sdp += ''; var obj = parse(sdp); var sdp2 = write(obj); var obj2 = parse(sdp2); t.deepEqual(obj, obj2, "parse ∘ write ∘ parse === parse | " + file); // This only tests that (parse ∘ write) == Id on the image of the parse. // It also doesn't test if (write ∘ parse) is the identity: which it isnt. // Properties may get reordered slightly (up to RFC legality). // However: (write ∘ parse) should be the identity on the image of write // because our own ordering is deterministic. t.equal(sdp2, write(obj2), "write ∘ parse === Id on Im(write) for " + file); t.done(); }) }; exports.normalCompose = function (t) { verifyCompose('normal.sdp', t); }; exports.chromeCompose = function (t) { verifyCompose('chrome.sdp', t); }; exports.jssipCompose = function (t) { verifyCompose('jssip.sdp', t); }; exports.jsepCompose = function (t) { verifyCompose('jsep.sdp', t); }; sdp-transform-1.4.0/test/coverage.test.js000066400000000000000000000016741250237216000204070ustar00rootroot00000000000000var fs = require('fs') , main = require(process.env.SDP_TRANSFORM_COV ? '../lib-cov' : '../') , parse = main.parse , write = main.write; var verifyCoverage = function (file, t) { fs.readFile(__dirname + '/' + file, function (err, sdp) { if (err) { t.ok(false, "failed to read file:" + err); return t.done(); } sdp += ''; var obj = parse(sdp); obj.media.forEach(function (m) { t.ok(!m.invalid, "no invalids in " + file + " at m=" + m.type); if (Array.isArray(m.invalid)) { t.deepEqual(m.invalid, [], "no invalids in " + file + " at m=" + m.type); } }); t.done(); }) }; exports.normalCoverage = function (t) { verifyCoverage('normal.sdp', t); }; exports.chromeCoverage = function (t) { verifyCoverage('chrome.sdp', t); }; exports.jssipCoverage = function (t) { verifyCoverage('jssip.sdp', t); }; exports.jsepCoverage = function (t) { verifyCoverage('jsep.sdp', t); }; sdp-transform-1.4.0/test/icelite.sdp000066400000000000000000000010611250237216000174140ustar00rootroot00000000000000v=0 o=- 3622532974 3622532974 IN IP4 192.168.100.100 s=- c=IN IP4 192.168.100.100 t=0 0 a=ice-lite m=audio 10018 RTP/SAVPF 8 0 101 a=rtpmap:8 PCMA/8000 a=rtpmap:0 PCMU/8000 a=rtpmap:101 telephone-event/8000 a=fmtp:101 0-15 a=direction:both a=sendrecv a=rtcp-mux a=setup:actpass a=fingerprint:sha-256 CE:17:02:86:E2:E8:B0:EF:F9:F3:3F:82:8A:A6:F0:EF:30:73:1D:5D:B3:5A:60:D7:AC:FE:F0:E3:DF:D5:D9:7B a=ice-ufrag:nXET a=ice-pwd:d0iwx/Qam8JnuvL+wkcXee a=candidate:X 1 UDP 659136 192.168.100.100 10018 typ host a=candidate:X 2 UDP 659134 192.168.100.100 10019 typ hostsdp-transform-1.4.0/test/invalid.sdp000066400000000000000000000002531250237216000174260ustar00rootroot00000000000000v=0 o=- 3710604898417546434 2 IN IP4 127.0.0.1 s=- t=0 0 m=audio 1 RTP/AVP 0 c=IN IP4 0.0.0.0 a=rtcp:1 IN IP7 X a=rtpmap:0 PCMU/8000 a=goo:hithere f=invalid:yes sdp-transform-1.4.0/test/jsep.sdp000066400000000000000000000035021250237216000167410ustar00rootroot00000000000000v=0 o=- 4962303333179871722 1 IN IP4 0.0.0.0 s=- t=0 0 a=msid-semantic:WMS a=group:BUNDLE a1 v1 m=audio 56500 UDP/TLS/RTP/SAVPF 96 0 8 97 98 c=IN IP4 192.0.2.1 a=mid:a1 a=rtcp:56501 IN IP4 192.0.2.1 a=msid:47017fee-b6c1-4162-929c-a25110252400 f83006c5-a0ff-4e0a-9ed9-d3e6747be7d9 a=sendrecv a=rtpmap:96 opus/48000/2 a=rtpmap:0 PCMU/8000 a=rtpmap:8 PCMA/8000 a=rtpmap:97 telephone-event/8000 a=rtpmap:98 telephone-event/48000 a=maxptime:120 a=ice-ufrag:ETEn1v9DoTMB9J4r a=ice-pwd:OtSK0WpNtpUjkY4+86js7ZQl a=ice-options:trickle a=fingerprint:sha-256 19:E2:1C:3B:4B:9F:81:E6:B8:5C:F4:A5:A8:D8:73:04:BB:05:2F:70:9F:04:A9:0E:05:E9:26:33:E8:70:88:A2 a=setup:actpass a=rtcp-mux a=rtcp-rsize a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level a=extmap:2 urn:ietf:params:rtp-hdrext:sdes:mid a=ssrc:1732846380 cname:EocUG1f0fcg/yvY7 a=candidate:3348148302 1 udp 2113937151 192.0.2.1 56500 typ host a=candidate:3348148302 2 udp 2113937151 192.0.2.1 56501 typ host a=end-of-candidates m=video 56502 UDP/TLS/RTP/SAVPF 100 101 c=IN IP4 192.0.2.1 a=rtcp:56503 IN IP4 192.0.2.1 a=mid:v1 a=msid:61317484-2ed4-49d7-9eb7-1414322a7aae f30bdb4a-5db8-49b5-bcdc-e0c9a23172e0 a=sendrecv a=rtpmap:100 VP8/90000 a=rtpmap:101 rtx/90000 a=fmtp:101 apt=100 a=ice-ufrag:BGKkWnG5GmiUpdIV a=ice-pwd:mqyWsAjvtKwTGnvhPztQ9mIf a=ice-options:trickle a=fingerprint:sha-256 19:E2:1C:3B:4B:9F:81:E6:B8:5C:F4:A5:A8:D8:73:04:BB:05:2F:70:9F:04:A9:0E:05:E9:26:33:E8:70:88:A2 a=setup:actpass a=rtcp-mux a=rtcp-rsize a=extmap:3 urn:ietf:params:rtp-hdrext:sdes:mid a=rtcp-fb:100 ccm fir a=rtcp-fb:100 nack a=rtcp-fb:100 nack pli a=ssrc:1366781083 cname:EocUG1f0fcg/yvY7 a=ssrc:1366781084 cname:EocUG1f0fcg/yvY7 a=ssrc-group:FID 1366781083 1366781084 a=candidate:3348148302 1 udp 2113937151 192.0.2.1 56502 typ host a=candidate:3348148302 2 udp 2113937151 192.0.2.1 56503 typ host a=end-of-candidates sdp-transform-1.4.0/test/jssip.sdp000066400000000000000000000034441250237216000171350ustar00rootroot00000000000000v=0 o=- 1334496563563564720 2 IN IP4 127.0.0.1 s=- t=0 0 a=group:BUNDLE audio a=msid-semantic: WMS KOaPIn6F0Qm9PuOA6WHfjdfqWMt9sGl6uOqg m=audio 60017 RTP/SAVPF 111 103 104 0 8 106 105 13 126 c=IN IP4 193.84.77.194 a=rtcp:60017 IN IP4 193.84.77.194 a=candidate:1162875081 1 udp 2113937151 192.168.34.75 60017 typ host generation 0 a=candidate:1162875081 2 udp 2113937151 192.168.34.75 60017 typ host generation 0 a=candidate:3289912957 1 udp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 generation 0 a=candidate:3289912957 2 udp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 generation 0 a=candidate:198437945 1 tcp 1509957375 192.168.34.75 0 typ host generation 0 a=candidate:198437945 2 tcp 1509957375 192.168.34.75 0 typ host generation 0 a=ice-ufrag:5I2uVefP13X1wzOY a=ice-pwd:e46UjXntt0K/xTncQcDBQePn a=ice-options:google-ice a=fingerprint:sha-256 79:14:AB:AB:93:7F:07:E8:91:1A:11:16:36:D0:11:66:C4:4F:31:A0:74:46:65:58:70:E5:09:95:48:F4:4B:D9 a=setup:actpass a=mid:audio a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level a=sendrecv a=rtcp-mux a=crypto:0 AES_CM_128_HMAC_SHA1_32 inline:6JYKxLF+o2nhouDHr5J0oNb3CEGK3I/HHv9idGTY a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:ayId2M5kCitGTEEI9OjgEqatTA0IXGpQhFjmKOGk a=rtpmap:111 opus/48000/2 a=fmtp:111 minptime=10 a=rtpmap:103 ISAC/16000 a=rtpmap:104 ISAC/32000 a=rtpmap:0 PCMU/8000 a=rtpmap:8 PCMA/8000 a=rtpmap:106 CN/32000 a=rtpmap:105 CN/16000 a=rtpmap:13 CN/8000 a=rtpmap:126 telephone-event/8000 a=maxptime:60 a=ssrc:1399694169 cname:w7AkLB30C7pk/PFE a=ssrc:1399694169 msid:KOaPIn6F0Qm9PuOA6WHfjdfqWMt9sGl6uOqg 775dca64-4698-455b-8a02-89833bd24773 a=ssrc:1399694169 mslabel:KOaPIn6F0Qm9PuOA6WHfjdfqWMt9sGl6uOqg a=ssrc:1399694169 label:775dca64-4698-455b-8a02-89833bd24773 sdp-transform-1.4.0/test/normal.sdp000066400000000000000000000016131250237216000172710ustar00rootroot00000000000000v=0 o=- 20518 0 IN IP4 203.0.113.1 s= t=0 0 c=IN IP4 203.0.113.1 a=ice-ufrag:F7gI a=ice-pwd:x9cml/YzichV2+XlhiMu8g a=fingerprint:sha-1 42:89:c5:c6:55:9d:6e:c8:e8:83:55:2a:39:f9:b6:eb:e9:a3:a9:e7 m=audio 54400 RTP/SAVPF 0 96 a=rtpmap:0 PCMU/8000 a=rtpmap:96 opus/48000 a=extmap:1 URI-toffset a=extmap:2/recvonly URI-gps-string a=ptime:20 a=sendrecv a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host a=candidate:1 2 UDP 2113667326 203.0.113.1 54401 typ host m=video 55400 RTP/SAVPF 97 98 a=rtpmap:97 H264/90000 a=fmtp:97 profile-level-id=4d0028;packetization-mode=1 a=rtpmap:98 VP8/90000 a=rtcp-fb:* nack a=rtcp-fb:98 nack rpsi a=rtcp-fb:98 trr-int 100 a=crypto:1 AES_CM_128_HMAC_SHA1_32 inline:keNcG3HezSNID7LmfDa9J4lfdUL8W1F7TNJKcbuy|2^20|1:32 a=sendrecv a=candidate:0 1 UDP 2113667327 203.0.113.1 55400 typ host a=candidate:1 2 UDP 2113667326 203.0.113.1 55401 typ host sdp-transform-1.4.0/test/parse.test.js000066400000000000000000000272031250237216000177220ustar00rootroot00000000000000var fs = require('fs') , main = require(process.env.SDP_TRANSFORM_COV ? '../lib-cov' : '../') , parse = main.parse , write = main.write , parseFmtpConfig = main.parseFmtpConfig; exports.normalSdp = function (t) { fs.readFile(__dirname + '/normal.sdp', function (err, sdp) { if (err) { t.ok(false, "failed to read file:" + err); t.done(); return; } var session = parse(sdp+''); t.ok(session, "got session info"); var media = session.media; t.ok(media && media.length > 0, "got media"); //t.equal(session.identifier, '- 20518 0 IN IP4 203.0.113.1', 'identifier'); t.equal(session.origin.username, '-', 'origin username'); t.equal(session.origin.sessionId, 20518, 'origin sessionId'); t.equal(session.origin.sessionVersion, 0, 'origin sessionVersion'); t.equal(session.origin.netType, 'IN', 'origin netType'); t.equal(session.origin.ipVer, 4, 'origin ipVer'); t.equal(session.origin.address, '203.0.113.1', 'origin address'); t.equal(session.connection.ip, '203.0.113.1', 'session connect ip'); t.equal(session.connection.version, 4, 'session connect ip ver'); // global ICE and fingerprint t.equal(session.iceUfrag, "F7gI", "global ufrag"); t.equal(session.icePwd, "x9cml/YzichV2+XlhiMu8g", "global pwd"); var audio = media[0]; t.equal(audio.type, "audio", "audio type"); t.equal(audio.port, 54400, "audio port"); t.equal(audio.protocol, "RTP/SAVPF", "audio protocol"); t.equal(audio.direction, "sendrecv", "audio direction"); t.equal(audio.rtp[0].payload, 0, "audio rtp 0 payload"); t.equal(audio.rtp[0].codec, "PCMU", "audio rtp 0 codec"); t.equal(audio.rtp[0].rate, 8000, "audio rtp 0 rate"); t.equal(audio.rtp[1].payload, 96, "audio rtp 1 payload"); t.equal(audio.rtp[1].codec, "opus", "audio rtp 1 codec"); t.equal(audio.rtp[1].rate, 48000, "audio rtp 1 rate"); t.deepEqual(audio.ext[0], { value: "1", uri: "URI-toffset" }, "audio extension 0"); t.deepEqual(audio.ext[1], { value: "2/recvonly", uri: "URI-gps-string" }, "audio extension 1"); var video = media[1]; t.equal(video.type, "video", "video type"); t.equal(video.port, 55400, "video port"); t.equal(video.protocol, "RTP/SAVPF", "video protocol"); t.equal(video.direction, "sendrecv", "video direction"); t.equal(video.rtp[0].payload, 97, "video rtp 0 payload"); t.equal(video.rtp[0].codec, "H264", "video rtp 0 codec"); t.equal(video.rtp[0].rate, 90000, "video rtp 0 rate"); t.equal(video.fmtp[0].payload, 97, "video fmtp 0 payload"); var vidFmtp = parseFmtpConfig(video.fmtp[0].config); t.equal(vidFmtp['profile-level-id'], "4d0028", "video fmtp 0 profile-level-id"); t.equal(vidFmtp['packetization-mode'], 1, "video fmtp 0 packetization-mode"); t.equal(video.rtp[1].payload, 98, "video rtp 1 payload"); t.equal(video.rtp[1].codec, "VP8", "video rtp 1 codec"); t.equal(video.rtp[1].rate, 90000, "video rtp 1 rate"); t.equal(video.rtcpFb[0].payload, '*', "video rtcp-fb 0 payload"); t.equal(video.rtcpFb[0].type, 'nack', "video rtcp-fb 0 type"); t.equal(video.rtcpFb[1].payload, 98, "video rtcp-fb 0 payload"); t.equal(video.rtcpFb[1].type, 'nack', "video rtcp-fb 0 type"); t.equal(video.rtcpFb[1].subtype, 'rpsi', "video rtcp-fb 0 subtype"); t.equal(video.rtcpFbTrrInt[0].payload, 98, "video rtcp-fb trr-int 0 payload"); t.equal(video.rtcpFbTrrInt[0].value, 100, "video rtcp-fb trr-int 0 value"); t.equal(video.crypto[0].id, 1, "video crypto 0 id"); t.equal(video.crypto[0].suite, 'AES_CM_128_HMAC_SHA1_32', "video crypto 0 suite"); t.equal(video.crypto[0].config, 'inline:keNcG3HezSNID7LmfDa9J4lfdUL8W1F7TNJKcbuy|2^20|1:32', "video crypto 0 config"); // ICE candidates (same for both audio and video in this case) [audio.candidates, video.candidates].forEach(function (cs, i) { var str = (i === 0) ? "audio " : "video "; var port = (i === 0) ? 54400 : 55400; t.equal(cs.length, 2, str + "got 2 candidates"); t.equal(cs[0].foundation, 0, str + "ice candidate 0 foundation"); t.equal(cs[0].component, 1, str + "ice candidate 0 component"); t.equal(cs[0].transport, "UDP", str + "ice candidate 0 transport"); t.equal(cs[0].priority, 2113667327, str + "ice candidate 0 priority"); t.equal(cs[0].ip, "203.0.113.1", str + "ice candidate 0 ip"); t.equal(cs[0].port, port, str + "ice candidate 0 port"); t.equal(cs[0].type, "host", str + "ice candidate 0 type"); t.equal(cs[1].foundation, 1, str + "ice candidate 1 foundation"); t.equal(cs[1].component, 2, str + "ice candidate 1 component"); t.equal(cs[1].transport, "UDP", str + "ice candidate 1 transport"); t.equal(cs[1].priority, 2113667326, str + "ice candidate 1 priority"); t.equal(cs[1].ip, "203.0.113.1", str + "ice candidate 1 ip"); t.equal(cs[1].port, port+1, str + "ice candidate 1 port"); t.equal(cs[1].type, "host", str + "ice candidate 1 type"); }); t.equal(media.length, 2, "got 2 m-lines"); t.done(); }); }; exports.chromeSdp = function (t) { fs.readFile(__dirname + '/chrome.sdp', function (err, sdp) { if (err) { t.ok(false, "failed to read file:" + err); t.done(); return; } var session = parse(sdp+''); t.ok(session, "got session info"); var media = session.media; t.ok(media && media.length > 0, "got media"); t.equal(session.origin.sessionId, '3710604898417546434', 'origin sessionId'); t.ok(session.groups, "parsing session groups"); t.equal(session.groups.length, 1, "one grouping"); t.equal(session.groups[0].type, "BUNDLE", "grouping is BUNDLE"); t.equal(session.groups[0].mids, "audio video", "bundling audio video"); t.ok(session.msidSemantic, "have an msid semantic"); t.equal(session.msidSemantic.semantic, "WMS", "webrtc semantic"); t.equal(session.msidSemantic.token, "Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlV", "semantic token"); // verify a=rtcp:65179 IN IP4 193.84.77.194 t.equal(media[0].rtcp.port, 1, 'rtcp port'); t.equal(media[0].rtcp.netType, 'IN', 'rtcp netType'); t.equal(media[0].rtcp.ipVer, 4, 'rtcp ipVer'); t.equal(media[0].rtcp.address, '0.0.0.0', 'rtcp address'); // and verify it works without specifying the ip t.equal(media[1].rtcp.port, 12312, 'rtcp port'); t.equal(media[1].rtcp.netType, undefined, 'rtcp netType'); t.equal(media[1].rtcp.ipVer, undefined, 'rtcp ipVer'); t.equal(media[1].rtcp.address, undefined, 'rtcp address'); // verify a=rtpmap:126 telephone-event/8000 var lastRtp = media[0].rtp.length-1; t.equal(media[0].rtp[lastRtp].codec, 'telephone-event', 'dtmf codec'); t.equal(media[0].rtp[lastRtp].rate, 8000, 'dtmf rate'); t.equal(media[0].iceOptions, 'google-ice', "ice options parsed"); t.equal(media[0].maxptime, 60, 'maxptime parsed'); t.equal(media[0].rtcpMux, 'rtcp-mux', 'rtcp-mux present'); t.equal(media[0].rtp[0].codec, 'opus', 'audio rtp 0 codec'); t.equal(media[0].rtp[0].encoding, 2, 'audio rtp 0 encoding'); t.ok(media[0].ssrcs, "have ssrc lines"); t.equal(media[0].ssrcs.length, 4, "got 4 ssrc lines"); var ssrcs = media[0].ssrcs; t.deepEqual(ssrcs[0], { id: 2754920552, attribute: "cname", value: "t9YU8M1UxTF8Y1A1" }, "1st ssrc line"); t.deepEqual(ssrcs[1], { id: 2754920552, attribute: "msid", value: "Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlV Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlVa0" }, "2nd ssrc line"); t.deepEqual(ssrcs[2], { id: 2754920552, attribute: "mslabel", value: "Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlV" }, "3rd ssrc line"); t.deepEqual(ssrcs[3], { id: 2754920552, attribute: "label", value: "Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlVa0" }, "4th ssrc line"); t.done(); }); }; exports.iceliteSdp = function (t) { fs.readFile(__dirname + '/icelite.sdp', function (err, sdp) { if (err) { t.ok(false, "failed to read file:" + err); t.done(); return; } var session = parse(sdp+''); t.ok(session, "got session info"); t.equal(session.icelite, 'ice-lite', 'icelite parsed'); var rew = write(session); t.ok(rew.indexOf("a=ice-lite\r\n") >= 0, "got ice-lite"); t.ok(rew.indexOf("m=") > rew.indexOf("a=ice-lite"), 'session level icelite'); t.done(); }); }; exports.invalidSdp = function (t) { fs.readFile(__dirname + '/invalid.sdp', function (err, sdp) { if (err) { t.ok(false, "failed to read file:" + err); t.done(); return; } var session = parse(sdp+''); t.ok(session, "got session info"); var media = session.media; t.ok(media && media.length > 0, "got media"); // verify a=rtcp:65179 IN IP4 193.84.77.194 t.equal(media[0].rtcp.port, 1, 'rtcp port'); t.equal(media[0].rtcp.netType, 'IN', 'rtcp netType'); t.equal(media[0].rtcp.ipVer, 7, 'rtcp ipVer'); t.equal(media[0].rtcp.address, 'X', 'rtcp address'); t.equal(media[0].invalid.length, 1, 'found exactly 1 invalid line'); // f= lost t.equal(media[0].invalid[0].value, 'goo:hithere', 'copied verbatim'); t.done(); }); }; exports.jssipSdp = function (t) { fs.readFile(__dirname + '/jssip.sdp', function (err, sdp) { if (err) { t.ok(false, "failed to read file:" + err); t.done(); return; } var session = parse(sdp+''); t.ok(session, "got session info"); var media = session.media; t.ok(media && media.length > 0, "got media"); var audio = media[0]; var audCands = audio.candidates; t.equal(audCands.length, 6, '6 candidates'); // testing ice optionals: t.deepEqual(audCands[0], { foundation: 1162875081, component: 1, transport: 'udp', priority: 2113937151, ip: '192.168.34.75', port: 60017, type: 'host', generation: 0 }, "audio candidate 0" ); t.deepEqual(audCands[2], { foundation: 3289912957, component: 1, transport: 'udp', priority: 1845501695, ip: '193.84.77.194', port: 60017, type: 'srflx', raddr: '192.168.34.75', rport: 60017, generation: 0 }, "audio candidate 2 (raddr rport)" ); t.deepEqual(audCands[4], { foundation: 198437945, component: 1, transport: 'tcp', priority: 1509957375, ip: '192.168.34.75', port: 0, type: 'host', generation: 0 }, "audio candidate 4 (tcp)" ); t.done(); }); }; exports.jsepSdp = function (t) { fs.readFile(__dirname + '/jsep.sdp', function (err, sdp) { if (err) { t.ok(false, "failed to read file:" + err); t.done(); return; } var session = parse(sdp+''); t.ok(session, "got session info"); var media = session.media; t.ok(media && media.length === 2, "got media"); var video = media[1]; t.equal(video.ssrcGroups.length, 1, '1 ssrc grouping') t.deepEqual(video.ssrcGroups[0], { semantics: 'FID', ssrcs: '1366781083 1366781084' }, 'ssrc-group' ); t.equal(video.msid, '61317484-2ed4-49d7-9eb7-1414322a7aae f30bdb4a-5db8-49b5-bcdc-e0c9a23172e0' , 'msid' ); t.ok(video.rtcpRsize, 'rtcp-rsize present'); // video contains 'a=end-of-candidates' // we want to ensure this comes after the candidate lines // so this is the only place we actually test the writer in here t.ok(video.endOfCandidates, "have end of candidates marker"); var rewritten = write(session).split('\r\n'); var idx = rewritten.indexOf('a=end-of-candidates'); t.equal(rewritten[idx-1].slice(0, 11), 'a=candidate', 'marker after candidate'); t.done(); }); };