summaryrefslogtreecommitdiffstats
path: root/comm/chat/protocols/matrix/lib/sdp-transform
diff options
context:
space:
mode:
Diffstat (limited to 'comm/chat/protocols/matrix/lib/sdp-transform')
-rw-r--r--comm/chat/protocols/matrix/lib/sdp-transform/LICENSE22
-rw-r--r--comm/chat/protocols/matrix/lib/sdp-transform/grammar.js494
-rw-r--r--comm/chat/protocols/matrix/lib/sdp-transform/index.js11
-rw-r--r--comm/chat/protocols/matrix/lib/sdp-transform/parser.js124
-rw-r--r--comm/chat/protocols/matrix/lib/sdp-transform/writer.js114
5 files changed, 765 insertions, 0 deletions
diff --git a/comm/chat/protocols/matrix/lib/sdp-transform/LICENSE b/comm/chat/protocols/matrix/lib/sdp-transform/LICENSE
new file mode 100644
index 0000000000..5c2338f892
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/sdp-transform/LICENSE
@@ -0,0 +1,22 @@
+(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.
diff --git a/comm/chat/protocols/matrix/lib/sdp-transform/grammar.js b/comm/chat/protocols/matrix/lib/sdp-transform/grammar.js
new file mode 100644
index 0000000000..d8178e86f9
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/sdp-transform/grammar.js
@@ -0,0 +1,494 @@
+var 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\-.]*)(?:\s*\/(\d*)(?:\s*\/(\S*))?)?/,
+ names: ['payload', 'codec', 'rate', 'encoding'],
+ format: function (o) {
+ return (o.encoding)
+ ? 'rtpmap:%d %s/%s/%s'
+ : o.rate
+ ? 'rtpmap:%d %s/%s'
+ : 'rtpmap:%d %s';
+ }
+ },
+ {
+ // a=fmtp:108 profile-level-id=24;object=23;bitrate=64000
+ // a=fmtp:111 minptime=10; useinbandfec=1
+ 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:%s 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
+ // a=extmap:3 urn:ietf:params:rtp-hdrext:encrypt urn:ietf:params:rtp-hdrext:smpte-tc 25@600/24
+ push: 'ext',
+ reg: /^extmap:(\d+)(?:\/(\w+))?(?: (urn:ietf:params:rtp-hdrext:encrypt))? (\S*)(?: (\S*))?/,
+ names: ['value', 'direction', 'encrypt-uri', 'uri', 'config'],
+ format: function (o) {
+ return (
+ 'extmap:%d' +
+ (o.direction ? '/%s' : '%v') +
+ (o['encrypt-uri'] ? ' %s' : '%v') +
+ ' %s' +
+ (o.config ? ' %s' : '')
+ );
+ }
+ },
+ {
+ // a=extmap-allow-mixed
+ name: 'extmapAllowMixed',
+ reg: /^(extmap-allow-mixed)/
+ },
+ {
+ // 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=connection:new
+ name: 'connectionType',
+ reg: /^connection:(new|existing)/,
+ format: 'connection:%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*(?:\.\d*)*)/,
+ format: 'ptime:%d'
+ },
+ {
+ // a=maxptime:60
+ name: 'maxptime',
+ reg: /^maxptime:(\d*(?:\.\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 network-id 3 network-cost 10
+ // a=candidate:3289912957 2 udp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 generation 0 network-id 3 network-cost 10
+ // a=candidate:229815620 1 tcp 1518280447 192.168.150.19 60017 typ host tcptype active generation 0 network-id 3 network-cost 10
+ // a=candidate:3289912957 2 tcp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 tcptype passive generation 0 network-id 3 network-cost 10
+ push:'candidates',
+ reg: /^candidate:(\S*) (\d*) (\S*) (\d*) (\S*) (\d*) typ (\S*)(?: raddr (\S*) rport (\d*))?(?: tcptype (\S*))?(?: generation (\d*))?(?: network-id (\d*))?(?: network-cost (\d*))?/,
+ names: ['foundation', 'component', 'transport', 'priority', 'ip', 'port', 'type', 'raddr', 'rport', 'tcptype', 'generation', 'network-id', 'network-cost'],
+ format: function (o) {
+ var str = 'candidate:%s %d %s %d %s %d typ %s';
+
+ str += (o.raddr != null) ? ' raddr %s rport %d' : '%v%v';
+
+ // NB: candidate has three optional chunks, so %void middles one if it's missing
+ str += (o.tcptype != null) ? ' tcptype %s' : '%v';
+
+ if (o.generation != null) {
+ str += ' generation %d';
+ }
+
+ str += (o['network-id'] != null) ? ' network-id %d' : '%v';
+ str += (o['network-cost'] != null) ? ' network-cost %d' : '%v';
+ 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: function (o) {
+ var str = 'ssrc:%d';
+ if (o.attribute != null) {
+ str += ' %s';
+ if (o.value != null) {
+ str += ':%s';
+ }
+ }
+ return str;
+ }
+ },
+ {
+ // a=ssrc-group:FEC 1 2
+ // a=ssrc-group:FEC-FR 3004364195 1080772241
+ push: 'ssrcGroups',
+ // token-char = %x21 / %x23-27 / %x2A-2B / %x2D-2E / %x30-39 / %x41-5A / %x5E-7E
+ reg: /^ssrc-group:([\x21\x23\x24\x25\x26\x27\x2A\x2B\x2D\x2E\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)/
+ },
+ {
+ // a=sctpmap:5000 webrtc-datachannel 1024
+ name: 'sctpmap',
+ reg: /^sctpmap:([\w_/]*) (\S*)(?: (\S*))?/,
+ names: ['sctpmapNumber', 'app', 'maxMessageSize'],
+ format: function (o) {
+ return (o.maxMessageSize != null)
+ ? 'sctpmap:%s %s %s'
+ : 'sctpmap:%s %s';
+ }
+ },
+ {
+ // a=x-google-flag:conference
+ name: 'xGoogleFlag',
+ reg: /^x-google-flag:([^\s]*)/,
+ format: 'x-google-flag:%s'
+ },
+ {
+ // a=rid:1 send max-width=1280;max-height=720;max-fps=30;depend=0
+ push: 'rids',
+ reg: /^rid:([\d\w]+) (\w+)(?: ([\S| ]*))?/,
+ names: ['id', 'direction', 'params'],
+ format: function (o) {
+ return (o.params) ? 'rid:%s %s %s' : 'rid:%s %s';
+ }
+ },
+ {
+ // a=imageattr:97 send [x=800,y=640,sar=1.1,q=0.6] [x=480,y=320] recv [x=330,y=250]
+ // a=imageattr:* send [x=800,y=640] recv *
+ // a=imageattr:100 recv [x=320,y=240]
+ push: 'imageattrs',
+ reg: new RegExp(
+ // a=imageattr:97
+ '^imageattr:(\\d+|\\*)' +
+ // send [x=800,y=640,sar=1.1,q=0.6] [x=480,y=320]
+ '[\\s\\t]+(send|recv)[\\s\\t]+(\\*|\\[\\S+\\](?:[\\s\\t]+\\[\\S+\\])*)' +
+ // recv [x=330,y=250]
+ '(?:[\\s\\t]+(recv|send)[\\s\\t]+(\\*|\\[\\S+\\](?:[\\s\\t]+\\[\\S+\\])*))?'
+ ),
+ names: ['pt', 'dir1', 'attrs1', 'dir2', 'attrs2'],
+ format: function (o) {
+ return 'imageattr:%s %s %s' + (o.dir2 ? ' %s %s' : '');
+ }
+ },
+ {
+ // a=simulcast:send 1,2,3;~4,~5 recv 6;~7,~8
+ // a=simulcast:recv 1;4,5 send 6;7
+ name: 'simulcast',
+ reg: new RegExp(
+ // a=simulcast:
+ '^simulcast:' +
+ // send 1,2,3;~4,~5
+ '(send|recv) ([a-zA-Z0-9\\-_~;,]+)' +
+ // space + recv 6;~7,~8
+ '(?:\\s?(send|recv) ([a-zA-Z0-9\\-_~;,]+))?' +
+ // end
+ '$'
+ ),
+ names: ['dir1', 'list1', 'dir2', 'list2'],
+ format: function (o) {
+ return 'simulcast:%s %s' + (o.dir2 ? ' %s %s' : '');
+ }
+ },
+ {
+ // old simulcast draft 03 (implemented by Firefox)
+ // https://tools.ietf.org/html/draft-ietf-mmusic-sdp-simulcast-03
+ // a=simulcast: recv pt=97;98 send pt=97
+ // a=simulcast: send rid=5;6;7 paused=6,7
+ name: 'simulcast_03',
+ reg: /^simulcast:[\s\t]+([\S+\s\t]+)$/,
+ names: ['value'],
+ format: 'simulcast: %s'
+ },
+ {
+ // a=framerate:25
+ // a=framerate:29.97
+ name: 'framerate',
+ reg: /^framerate:(\d+(?:$|\.\d+))/,
+ format: 'framerate:%s'
+ },
+ {
+ // RFC4570
+ // a=source-filter: incl IN IP4 239.5.2.31 10.1.15.5
+ name: 'sourceFilter',
+ reg: /^source-filter: *(excl|incl) (\S*) (IP4|IP6|\*) (\S*) (.*)/,
+ names: ['filterMode', 'netType', 'addressTypes', 'destAddress', 'srcList'],
+ format: 'source-filter: %s %s %s %s %s'
+ },
+ {
+ // a=bundle-only
+ name: 'bundleOnly',
+ reg: /^(bundle-only)/
+ },
+ {
+ // a=label:1
+ name: 'label',
+ reg: /^label:(.+)/,
+ format: 'label:%s'
+ },
+ {
+ // RFC version 26 for SCTP over DTLS
+ // https://tools.ietf.org/html/draft-ietf-mmusic-sctp-sdp-26#section-5
+ name: 'sctpPort',
+ reg: /^sctp-port:(\d+)$/,
+ format: 'sctp-port:%s'
+ },
+ {
+ // RFC version 26 for SCTP over DTLS
+ // https://tools.ietf.org/html/draft-ietf-mmusic-sctp-sdp-26#section-6
+ name: 'maxMessageSize',
+ reg: /^max-message-size:(\d+)$/,
+ format: 'max-message-size:%s'
+ },
+ {
+ // RFC7273
+ // a=ts-refclk:ptp=IEEE1588-2008:39-A7-94-FF-FE-07-CB-D0:37
+ push:'tsRefClocks',
+ reg: /^ts-refclk:([^\s=]*)(?:=(\S*))?/,
+ names: ['clksrc', 'clksrcExt'],
+ format: function (o) {
+ return 'ts-refclk:%s' + (o.clksrcExt != null ? '=%s' : '');
+ }
+ },
+ {
+ // RFC7273
+ // a=mediaclk:direct=963214424
+ name:'mediaClk',
+ reg: /^mediaclk:(?:id=(\S*))? *([^\s=]*)(?:=(\S*))?(?: *rate=(\d+)\/(\d+))?/,
+ names: ['id', 'mediaClockName', 'mediaClockValue', 'rateNumerator', 'rateDenominator'],
+ format: function (o) {
+ var str = 'mediaclk:';
+ str += (o.id != null ? 'id=%s %s' : '%v%s');
+ str += (o.mediaClockValue != null ? '=%s' : '');
+ str += (o.rateNumerator != null ? ' rate=%s' : '');
+ str += (o.rateDenominator != null ? '/%s' : '');
+ return str;
+ }
+ },
+ {
+ // a=keywds:keywords
+ name: 'keywords',
+ reg: /^keywds:(.+)$/,
+ format: 'keywds:%s'
+ },
+ {
+ // a=content:main
+ name: 'content',
+ reg: /^content:(.+)/,
+ format: 'content:%s'
+ },
+ // BFCP https://tools.ietf.org/html/rfc4583
+ {
+ // a=floorctrl:c-s
+ name: 'bfcpFloorCtrl',
+ reg: /^floorctrl:(c-only|s-only|c-s)/,
+ format: 'floorctrl:%s'
+ },
+ {
+ // a=confid:1
+ name: 'bfcpConfId',
+ reg: /^confid:(\d+)/,
+ format: 'confid:%s'
+ },
+ {
+ // a=userid:1
+ name: 'bfcpUserId',
+ reg: /^userid:(\d+)/,
+ format: 'userid:%s'
+ },
+ {
+ // a=floorid:1
+ name: 'bfcpFloorId',
+ reg: /^floorid:(.+) (?:m-stream|mstrm):(.+)/,
+ names: ['id', 'mStream'],
+ format: 'floorid:%s mstrm:%s'
+ },
+ {
+ // any a= that we don't understand is kept 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';
+ }
+ });
+});
diff --git a/comm/chat/protocols/matrix/lib/sdp-transform/index.js b/comm/chat/protocols/matrix/lib/sdp-transform/index.js
new file mode 100644
index 0000000000..0a27894f89
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/sdp-transform/index.js
@@ -0,0 +1,11 @@
+var parser = require('./parser');
+var writer = require('./writer');
+
+exports.write = writer;
+exports.parse = parser.parse;
+exports.parseParams = parser.parseParams;
+exports.parseFmtpConfig = parser.parseFmtpConfig; // Alias of parseParams().
+exports.parsePayloads = parser.parsePayloads;
+exports.parseRemoteCandidates = parser.parseRemoteCandidates;
+exports.parseImageAttributes = parser.parseImageAttributes;
+exports.parseSimulcastStreamList = parser.parseSimulcastStreamList;
diff --git a/comm/chat/protocols/matrix/lib/sdp-transform/parser.js b/comm/chat/protocols/matrix/lib/sdp-transform/parser.js
new file mode 100644
index 0000000000..ac863971a7
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/sdp-transform/parser.js
@@ -0,0 +1,124 @@
+var 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 paramReducer = function (acc, expr) {
+ var s = expr.split(/=(.+)/, 2);
+ if (s.length === 2) {
+ acc[s[0]] = toIntIfInt(s[1]);
+ } else if (s.length === 1 && expr.length > 1) {
+ acc[s[0]] = undefined;
+ }
+ return acc;
+};
+
+exports.parseParams = function (str) {
+ return str.split(/;\s?/).reduce(paramReducer, {});
+};
+
+// For backward compatibility - alias will be removed in 3.0.0
+exports.parseFmtpConfig = exports.parseParams;
+
+exports.parsePayloads = function (str) {
+ return str.toString().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;
+};
+
+exports.parseImageAttributes = function (str) {
+ return str.split(' ').map(function (item) {
+ return item.substring(1, item.length-1).split(',').reduce(paramReducer, {});
+ });
+};
+
+exports.parseSimulcastStreamList = function (str) {
+ return str.split(';').map(function (stream) {
+ return stream.split(',').map(function (format) {
+ var scid, paused = false;
+
+ if (format[0] !== '~') {
+ scid = toIntIfInt(format);
+ } else {
+ scid = toIntIfInt(format.substring(1, format.length));
+ paused = true;
+ }
+
+ return {
+ scid: scid,
+ paused: paused
+ };
+ });
+ });
+};
diff --git a/comm/chat/protocols/matrix/lib/sdp-transform/writer.js b/comm/chat/protocols/matrix/lib/sdp-transform/writer.js
new file mode 100644
index 0000000000..decdf480a8
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/sdp-transform/writer.js
@@ -0,0 +1,114 @@
+var 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';
+};