From 03a9c8aa1cf3815327352604a1695456a1a19d06 Mon Sep 17 00:00:00 2001 From: damencho Date: Fri, 22 Jan 2016 11:35:27 -0600 Subject: [PATCH] Updates library to fix start muted. --- libs/lib-jitsi-meet.js | 1750 ++++++++++++++++++++-------------------- 1 file changed, 889 insertions(+), 861 deletions(-) diff --git a/libs/lib-jitsi-meet.js b/libs/lib-jitsi-meet.js index 53ea2fffbe..4e10f53304 100644 --- a/libs/lib-jitsi-meet.js +++ b/libs/lib-jitsi-meet.js @@ -916,7 +916,7 @@ function setupListeners(conference) { module.exports = JitsiConference; }).call(this,"/JitsiConference.js") -},{"./JitsiConferenceErrors":2,"./JitsiConferenceEvents":3,"./JitsiParticipant":8,"./JitsiTrackEvents":10,"./modules/DTMF/JitsiDTMFManager":11,"./modules/RTC/RTC":16,"./modules/statistics/statistics":24,"./service/RTC/RTCEvents":75,"./service/authentication/AuthenticationEvents":77,"./service/xmpp/XMPPEvents":81,"events":43,"jitsi-meet-logger":47}],2:[function(require,module,exports){ +},{"./JitsiConferenceErrors":2,"./JitsiConferenceEvents":3,"./JitsiParticipant":8,"./JitsiTrackEvents":10,"./modules/DTMF/JitsiDTMFManager":11,"./modules/RTC/RTC":16,"./modules/statistics/statistics":24,"./service/RTC/RTCEvents":79,"./service/authentication/AuthenticationEvents":81,"./service/xmpp/XMPPEvents":85,"events":43,"jitsi-meet-logger":47}],2:[function(require,module,exports){ /** * Enumeration with the errors for the conference. * @type {{string: string}} @@ -1324,7 +1324,7 @@ window.Promise = window.Promise || require("es6-promise").Promise; module.exports = LibJitsiMeet; -},{"./JitsiConferenceErrors":2,"./JitsiConferenceEvents":3,"./JitsiConnection":4,"./JitsiConnectionErrors":5,"./JitsiConnectionEvents":6,"./JitsiTrackErrors":9,"./JitsiTrackEvents":10,"./modules/RTC/RTC":16,"./modules/statistics/statistics":24,"./service/RTC/Resolutions":76,"es6-promise":45,"jitsi-meet-logger":47}],8:[function(require,module,exports){ +},{"./JitsiConferenceErrors":2,"./JitsiConferenceEvents":3,"./JitsiConnection":4,"./JitsiConnectionErrors":5,"./JitsiConnectionEvents":6,"./JitsiTrackErrors":9,"./JitsiTrackEvents":10,"./modules/RTC/RTC":16,"./modules/statistics/statistics":24,"./service/RTC/Resolutions":80,"es6-promise":45,"jitsi-meet-logger":47}],8:[function(require,module,exports){ /* global Strophe */ /** @@ -1772,7 +1772,7 @@ module.exports = DataChannels; }).call(this,"/modules/RTC/DataChannels.js") -},{"../../service/RTC/RTCEvents":75,"jitsi-meet-logger":47}],13:[function(require,module,exports){ +},{"../../service/RTC/RTCEvents":79,"jitsi-meet-logger":47}],13:[function(require,module,exports){ var JitsiTrack = require("./JitsiTrack"); var RTCBrowserType = require("./RTCBrowserType"); var JitsiTrackEvents = require('../../JitsiTrackEvents'); @@ -1955,7 +1955,7 @@ function JitsiRemoteTrack(RTC, data, sid, ssrc) { JitsiTrack.call(this, RTC, data.stream, function () { this.eventEmitter.emit(JitsiTrackEvents.TRACK_STOPPED); - }.bind(this)); + }.bind(this), data.type); this.rtc = RTC; this.sid = sid; this.stream = data.stream; @@ -2029,6 +2029,10 @@ var RTC = require("./RTCUtils"); */ function implementOnEndedHandling(jitsiTrack) { var stream = jitsiTrack.getOriginalStream(); + + if(!stream) + return; + var originalStop = stream.stop; stream.stop = function () { originalStop.apply(stream); @@ -2074,8 +2078,11 @@ function addMediaStreamInactiveHandler(mediaStream, handler) { * @param stream the stream * @param streamInactiveHandler the function that will handle * onended/oninactive events of the stream. + * @param type optionally a type can be specified. + * This is the case where we are creating a dummy track with no stream + * Currently this happens when a remote side is starting with video muted */ -function JitsiTrack(rtc, stream, streamInactiveHandler) +function JitsiTrack(rtc, stream, streamInactiveHandler, type) { /** * Array with the HTML elements that are displaying the streams. @@ -2086,9 +2093,9 @@ function JitsiTrack(rtc, stream, streamInactiveHandler) this.stream = stream; this.eventEmitter = new EventEmitter(); this.audioLevel = -1; - this.type = (this.stream.getVideoTracks().length > 0)? + this.type = type || (this.stream.getVideoTracks().length > 0)? JitsiTrack.VIDEO : JitsiTrack.AUDIO; - if(this.type == "audio") { + if(this.type == JitsiTrack.AUDIO) { this._getTracks = function () { return this.stream.getAudioTracks(); }.bind(this); @@ -2317,12 +2324,25 @@ function RTC(room, options) { this.options = options || {}; room.addPresenceListener("videomuted", function (values, from) { if(self.remoteStreams[from]) { - self.remoteStreams[from][JitsiTrack.VIDEO].setMute(values.value == "true"); + // If there is no video track, but we receive it is muted, + // we need to create a dummy track which we will mute, so we can + // notify interested about the muting + if(!self.remoteStreams[from][JitsiTrack.VIDEO]) { + self.createRemoteStream( + {peerjid:room.roomjid + "/" + from, + videoType:"camera", + type:JitsiTrack.VIDEO}, + null, null); + } + + self.remoteStreams[from][JitsiTrack.VIDEO] + .setMute(values.value == "true"); } }); room.addPresenceListener("audiomuted", function (values, from) { if(self.remoteStreams[from]) { - self.remoteStreams[from][JitsiTrack.AUDIO].setMute(values.value == "true"); + self.remoteStreams[from][JitsiTrack.AUDIO] + .setMute(values.value == "true"); } }); } @@ -2523,7 +2543,7 @@ RTC.prototype.setAudioLevel = function (jid, audioLevel) { } module.exports = RTC; -},{"../../service/RTC/MediaStreamTypes":74,"../../service/RTC/RTCEvents.js":75,"../../service/desktopsharing/DesktopSharingEventTypes":78,"./DataChannels":12,"./JitsiLocalTrack.js":13,"./JitsiRemoteTrack.js":14,"./JitsiTrack":15,"./RTCBrowserType":17,"./RTCUtils.js":18,"events":43}],17:[function(require,module,exports){ +},{"../../service/RTC/MediaStreamTypes":78,"../../service/RTC/RTCEvents.js":79,"../../service/desktopsharing/DesktopSharingEventTypes":82,"./DataChannels":12,"./JitsiLocalTrack.js":13,"./JitsiRemoteTrack.js":14,"./JitsiTrack":15,"./RTCBrowserType":17,"./RTCUtils.js":18,"events":43}],17:[function(require,module,exports){ var currentBrowser; @@ -3516,7 +3536,7 @@ var RTCUtils = { module.exports = RTCUtils; }).call(this,"/modules/RTC/RTCUtils.js") -},{"../../JitsiTrackErrors":9,"../../service/RTC/RTCEvents":75,"../../service/RTC/Resolutions":76,"../xmpp/SDPUtil":31,"./RTCBrowserType":17,"./ScreenObtainer":19,"./adapter.screenshare":20,"events":43,"jitsi-meet-logger":47}],19:[function(require,module,exports){ +},{"../../JitsiTrackErrors":9,"../../service/RTC/RTCEvents":79,"../../service/RTC/Resolutions":80,"../xmpp/SDPUtil":31,"./RTCBrowserType":17,"./ScreenObtainer":19,"./adapter.screenshare":20,"events":43,"jitsi-meet-logger":47}],19:[function(require,module,exports){ (function (__filename){ /* global chrome, $, alert */ /* jshint -W003 */ @@ -3932,7 +3952,7 @@ function initFirefoxExtensionDetection(options) { module.exports = ScreenObtainer; }).call(this,"/modules/RTC/ScreenObtainer.js") -},{"../../JitsiTrackErrors":9,"../../service/desktopsharing/DesktopSharingEventTypes":78,"./RTCBrowserType":17,"./adapter.screenshare":20,"jitsi-meet-logger":47}],20:[function(require,module,exports){ +},{"../../JitsiTrackErrors":9,"../../service/desktopsharing/DesktopSharingEventTypes":82,"./RTCBrowserType":17,"./adapter.screenshare":20,"jitsi-meet-logger":47}],20:[function(require,module,exports){ (function (__filename){ /*! adapterjs - v0.12.3 - 2015-11-16 */ var console = require("jitsi-meet-logger").getLogger(__filename); @@ -6040,7 +6060,7 @@ StatsCollector.prototype.processAudioLevelReport = function () { }; }).call(this,"/modules/statistics/RTPStatsCollector.js") -},{"../../service/statistics/Events":79,"../RTC/RTCBrowserType":17,"jitsi-meet-logger":47}],24:[function(require,module,exports){ +},{"../../service/statistics/Events":83,"../RTC/RTCBrowserType":17,"jitsi-meet-logger":47}],24:[function(require,module,exports){ /* global require, APP */ var LocalStats = require("./LocalStatsCollector.js"); var RTPStats = require("./RTPStatsCollector.js"); @@ -6215,7 +6235,7 @@ Statistics.LOCAL_JID = require("../../service/statistics/constants").LOCAL_JID; module.exports = Statistics; -},{"../../service/statistics/Events":79,"../../service/statistics/constants":80,"./LocalStatsCollector.js":22,"./RTPStatsCollector.js":23,"events":43}],25:[function(require,module,exports){ +},{"../../service/statistics/Events":83,"../../service/statistics/constants":84,"./LocalStatsCollector.js":22,"./RTPStatsCollector.js":23,"events":43}],25:[function(require,module,exports){ /** /** * @const @@ -7103,7 +7123,7 @@ ChatRoom.prototype.onMute = function (iq) { module.exports = ChatRoom; }).call(this,"/modules/xmpp/ChatRoom.js") -},{"../../service/xmpp/XMPPEvents":81,"./moderator":33,"./recording":34,"events":43,"jitsi-meet-logger":47}],27:[function(require,module,exports){ +},{"../../service/xmpp/XMPPEvents":85,"./moderator":33,"./recording":34,"events":43,"jitsi-meet-logger":47}],27:[function(require,module,exports){ (function (__filename){ /* * JingleSession provides an API to manage a single Jingle session. We will @@ -8902,7 +8922,7 @@ JingleSessionPC.prototype.getIceConnectionState = function () { module.exports = JingleSessionPC; }).call(this,"/modules/xmpp/JingleSessionPC.js") -},{"../../service/xmpp/XMPPEvents":81,"../RTC/RTC":16,"../RTC/RTCBrowserType":17,"./JingleSession":27,"./SDP":29,"./SDPDiffer":30,"./SDPUtil":31,"./TraceablePeerConnection":32,"async":42,"jitsi-meet-logger":47,"sdp-transform":71}],29:[function(require,module,exports){ +},{"../../service/xmpp/XMPPEvents":85,"../RTC/RTC":16,"../RTC/RTCBrowserType":17,"./JingleSession":27,"./SDP":29,"./SDPDiffer":30,"./SDPUtil":31,"./TraceablePeerConnection":32,"async":42,"jitsi-meet-logger":47,"sdp-transform":75}],29:[function(require,module,exports){ (function (__filename){ /* jshint -W117 */ @@ -10586,7 +10606,7 @@ TraceablePeerConnection.prototype.getStats = function(callback, errback) { module.exports = TraceablePeerConnection; }).call(this,"/modules/xmpp/TraceablePeerConnection.js") -},{"../../service/xmpp/XMPPEvents":81,"../RTC/RTC":16,"../RTC/RTCBrowserType.js":17,"../util/RandomUtil":25,"jitsi-meet-logger":47,"sdp-interop":65,"sdp-simulcast":68,"sdp-transform":71}],33:[function(require,module,exports){ +},{"../../service/xmpp/XMPPEvents":85,"../RTC/RTC":16,"../RTC/RTCBrowserType.js":17,"../util/RandomUtil":25,"jitsi-meet-logger":47,"sdp-interop":65,"sdp-simulcast":72,"sdp-transform":75}],33:[function(require,module,exports){ (function (__filename){ /* global $, $iq, Promise, Strophe */ @@ -11047,7 +11067,7 @@ Moderator.prototype.logout = function (callback) { module.exports = Moderator; }).call(this,"/modules/xmpp/moderator.js") -},{"../../service/authentication/AuthenticationEvents":77,"../../service/xmpp/XMPPEvents":81,"../settings/Settings":21,"jitsi-meet-logger":47}],34:[function(require,module,exports){ +},{"../../service/authentication/AuthenticationEvents":81,"../../service/xmpp/XMPPEvents":85,"../settings/Settings":21,"jitsi-meet-logger":47}],34:[function(require,module,exports){ (function (__filename){ /* global $, $iq, config, connection, focusMucJid, messageHandler, Toolbar, Util, Promise */ @@ -11274,7 +11294,7 @@ Recording.prototype.getURL = function () { module.exports = Recording; }).call(this,"/modules/xmpp/recording.js") -},{"../../service/xmpp/XMPPEvents":81,"jitsi-meet-logger":47}],35:[function(require,module,exports){ +},{"../../service/xmpp/XMPPEvents":85,"jitsi-meet-logger":47}],35:[function(require,module,exports){ (function (__filename){ /* jshint -W117 */ /* a simple MUC connection plugin @@ -11683,7 +11703,7 @@ module.exports = function(XMPP, eventEmitter) { }).call(this,"/modules/xmpp/strophe.jingle.js") -},{"../../service/xmpp/XMPPEvents":81,"../RTC/RTCBrowserType":17,"./JingleSessionPC":28,"jitsi-meet-logger":47}],37:[function(require,module,exports){ +},{"../../service/xmpp/XMPPEvents":85,"../RTC/RTCBrowserType":17,"./JingleSessionPC":28,"jitsi-meet-logger":47}],37:[function(require,module,exports){ /* global Strophe */ module.exports = function () { @@ -11831,7 +11851,7 @@ module.exports = function (XMPP, eventEmitter) { }; }).call(this,"/modules/xmpp/strophe.ping.js") -},{"../../service/xmpp/XMPPEvents":81,"jitsi-meet-logger":47}],39:[function(require,module,exports){ +},{"../../service/xmpp/XMPPEvents":85,"jitsi-meet-logger":47}],39:[function(require,module,exports){ (function (__filename){ /* jshint -W117 */ var logger = require("jitsi-meet-logger").getLogger(__filename); @@ -12309,7 +12329,7 @@ XMPP.prototype.disconnect = function () { module.exports = XMPP; }).call(this,"/modules/xmpp/xmpp.js") -},{"../../JitsiConnectionErrors":5,"../../JitsiConnectionEvents":6,"../../service/RTC/RTCEvents":75,"../../service/xmpp/XMPPEvents":81,"../RTC/RTC":16,"./strophe.emuc":35,"./strophe.jingle":36,"./strophe.logger":37,"./strophe.ping":38,"./strophe.rayo":39,"./strophe.util":40,"events":43,"jitsi-meet-logger":47,"pako":48}],42:[function(require,module,exports){ +},{"../../JitsiConnectionErrors":5,"../../JitsiConnectionEvents":6,"../../service/RTC/RTCEvents":79,"../../service/xmpp/XMPPEvents":85,"../RTC/RTC":16,"./strophe.emuc":35,"./strophe.jingle":36,"./strophe.logger":37,"./strophe.ping":38,"./strophe.rayo":39,"./strophe.util":40,"events":43,"jitsi-meet-logger":47,"pako":48}],42:[function(require,module,exports){ (function (process){ /*! * async @@ -22359,984 +22379,992 @@ exports.parse = function(sdp) { }; -},{"sdp-transform":71}],68:[function(require,module,exports){ -/* Copyright @ 2015 Atlassian Pty Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -var transform = require('sdp-transform'); -var transformUtils = require('./transform-utils'); -var parseSsrcs = transformUtils.parseSsrcs; -var writeSsrcs = transformUtils.writeSsrcs; - -//region Constants - -var DEFAULT_NUM_OF_LAYERS = 3; - -//endregion - -//region Ctor - -function Simulcast(options) { - - this.options = options ? options : {}; - - if (!this.options.numOfLayers) { - this.options.numOfLayers = DEFAULT_NUM_OF_LAYERS; - } - - this.layers = []; -} - -//endregion - -//region Stateless private utility functions +},{"sdp-transform":69}],68:[function(require,module,exports){ +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:%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 + //a=candidate:229815620 1 tcp 1518280447 192.168.150.19 60017 typ host tcptype active generation 0 + //a=candidate:3289912957 2 tcp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 tcptype passive generation 0 + push:'candidates', + reg: /^candidate:(\S*) (\d*) (\S*) (\d*) (\S*) (\d*) typ (\S*)(?: raddr (\S*) rport (\d*))?(?: tcptype (\S*))?(?: generation (\d*))?/, + names: ['foundation', 'component', 'transport', 'priority', 'ip', 'port', 'type', 'raddr', 'rport', 'tcptype', 'generation'], + format: function (o) { + var str = "candidate:%s %d %s %d %s %d typ %s"; -/** - * Returns a random integer between min (included) and max (excluded) - * Using Math.round() gives a non-uniform distribution! - * @returns {number} - */ -function generateSSRC() { - var min = 0, max = 0xffffffff; - return Math.floor(Math.random() * (max - min)) + min; -}; + str += (o.raddr != null) ? " raddr %s rport %d" : "%v%v"; -function processVideo(session, action) { - if (session == null || !Array.isArray(session.media)) { - return; - } + // NB: candidate has three optional chunks, so %void middles one if it's missing + str += (o.tcptype != null) ? " tcptype %s" : "%v"; - session.media.forEach(function (mLine) { - if (mLine.type === 'video') { - action(mLine); + if (o.generation != null) { + str += " generation %d"; } - }); -} - -function validateDescription(desc) -{ - return desc && desc != null - && desc.type && desc.type != '' - && desc.sdp && desc.sdp != ''; -} - -function explodeRemoteSimulcast(mLine) { - - if (!mLine || !Array.isArray(mLine.ssrcGroups)) { - return; + 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"] } + ] +}; - var sources = parseSsrcs(mLine); - var order = []; - - // Find the SIM group and explode its sources. - var j = mLine.ssrcGroups.length; - while (j--) { - - if (mLine.ssrcGroups[j].semantics !== 'SIM') { - continue; - } - - var simulcastSsrcs = mLine.ssrcGroups[j].ssrcs.split(' '); +// 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"; + } + }); +}); - for (var i = 0; i < simulcastSsrcs.length; i++) { +},{}],69:[function(require,module,exports){ +var parser = require('./parser'); +var writer = require('./writer'); - var ssrc = simulcastSsrcs[i]; - order.push(ssrc); +exports.write = writer; +exports.parse = parser.parse; +exports.parseFmtpConfig = parser.parseFmtpConfig; +exports.parsePayloads = parser.parsePayloads; +exports.parseRemoteCandidates = parser.parseRemoteCandidates; - var parts = sources[ssrc].msid.split(' '); - sources[ssrc].msid = [parts[0], '/', i, ' ', parts[1], '/', i].join(''); - sources[ssrc].cname = [sources[ssrc].cname, '/', i].join(''); +},{"./parser":70,"./writer":71}],70:[function(require,module,exports){ +var toIntIfInt = function (v) { + return String(Number(v)) === v ? Number(v) : v; +}; - // Remove all the groups that this SSRC participates in. - mLine.ssrcGroups.forEach(function (relatedGroup) { - if (relatedGroup.semantics === 'SIM') { - return; - } +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 relatedSsrcs = relatedGroup.ssrcs.split(' '); - if (relatedSsrcs.indexOf(ssrc) === -1) { - return; - } +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 - // Nuke all the related SSRCs. - relatedSsrcs.forEach(function (relatedSSRC) { - sources[relatedSSRC].msid = sources[ssrc].msid; - sources[relatedSSRC].cname = sources[ssrc].cname; - if (relatedSSRC !== ssrc) { - order.push(relatedSSRC); - } - }); + attachProperties(content.match(obj.reg), keyLocation, obj.names, obj.name); - // Schedule the related group for nuking. - }) - } + if (obj.push) { + location[obj.push].push(keyLocation); + } +}; - mLine.ssrcs = writeSsrcs(sources, order); - mLine.ssrcGroups.splice(j, 1); - }; -} +var grammar = require('./grammar'); +var validLine = RegExp.prototype.test.bind(/^([a-z])=(.*)/); -function implodeRemoteSimulcast(mLine) { +exports.parse = function (sdp) { + var session = {} + , media = [] + , location = session; // points at where properties go under (one of the above) - if (!mLine || !Array.isArray(mLine.ssrcGroups)) { - console.info('Halt: There are no SSRC groups in the remote ' + - 'description.'); - return; + // 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 } - var sources = parseSsrcs(mLine); - - // Find the SIM group and nuke it. - mLine.ssrcGroups.forEach(function (simulcastGroup) { - if (simulcastGroup.semantics !== 'SIM') { - return; - } - - console.info("Imploding SIM group: " + simulcastGroup.ssrcs); - // Schedule the SIM group for nuking. - simulcastGroup.nuke = true; - - var simulcastSsrcs = simulcastGroup.ssrcs.split(' '); - - // Nuke all the higher layer SSRCs. - for (var i = 1; i < simulcastSsrcs.length; i++) { - - var ssrc = simulcastSsrcs[i]; - delete sources[ssrc]; + 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); + } + } + }); - // Remove all the groups that this SSRC participates in. - mLine.ssrcGroups.forEach(function (relatedGroup) { - if (relatedGroup.semantics === 'SIM') { - return; - } + session.media = media; // link it up + return session; +}; - var relatedSsrcs = relatedGroup.ssrcs.split(' '); - if (relatedSsrcs.indexOf(ssrc) === -1) { - return; - } +var fmtpReducer = function (acc, expr) { + var s = expr.split('='); + if (s.length === 2) { + acc[s[0]] = toIntIfInt(s[1]); + } + return acc; +}; - // Nuke all the related SSRCs. - relatedSsrcs.forEach(function (relatedSSRC) { - delete sources[relatedSSRC]; - }); +exports.parseFmtpConfig = function (str) { + return str.split(/\;\s?/).reduce(fmtpReducer, {}); +}; - // Schedule the related group for nuking. - relatedGroup.nuke = true; - }) - } +exports.parsePayloads = function (str) { + return str.split(' ').map(Number); +}; - return; +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; +}; - mLine.ssrcs = writeSsrcs(sources); - - // Nuke all the scheduled groups. - var i = mLine.ssrcGroups.length; - while (i--) { - if (mLine.ssrcGroups[i].nuke) { - mLine.ssrcGroups.splice(i, 1); - } - } -} +},{"./grammar":68}],71:[function(require,module,exports){ +var grammar = require('./grammar'); -function removeGoogConference(mLine) { - if (!mLine || !Array.isArray(mLine.invalid)) { - return; - } - - var i = mLine.invalid.length; - while (i--) { - if (mLine.invalid[i].value == 'x-google-flag:conference') { - mLine.invalid.splice(i, 1); - } - } -} - -function assertGoogConference(mLine) { - if (!mLine) { - return; - } - - if (!Array.isArray(mLine.invalid)) { - mLine.invalid = []; - } - - if (!mLine.invalid.some( - function (i) { return i.value === 'x-google-flag:conference' })) { - mLine.invalid.push({'value': 'x-google-flag:conference'}); +// 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 } -} - -//endregion - -//region "Private" functions - -/** - * - * @param mLine - * @private - */ -Simulcast.prototype._maybeInitializeLayers = function(mLine) { - - if (!mLine || mLine.type !== 'video') { - return; + 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 sources = parseSsrcs(mLine); - - if (Object.keys(sources).length === 0) { - - // no sources, disable simulcast. - if (this.layers.length !== 0) { - this.layers = []; - } +var makeLine = function (type, obj, location) { + var str = obj.format instanceof Function ? + (obj.format(obj.push ? location : location[obj.name])) : + obj.format; - return; + 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); +}; - // find the base layer (we'll reuse its msid and cname). - var baseLayerSSRC = Object.keys(sources)[0]; - var baseLayer = sources[baseLayerSSRC]; +// 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']; - // todo(gp) handle screen sharing. - // check if base CNAME has changed and reinitialise layers. - if (this.layers.length > 0 - && sources[baseLayerSSRC].cname !== this.layers[0].cname) { - this.layers = []; +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 = ""; } + }); - // (re)initialise layers - if (this.layers.length < 1) { + var outerOrder = opts.outerOrder || defaultOuterOrder; + var innerOrder = opts.innerOrder || defaultInnerOrder; + var sdp = []; - // first push the base layer. - this.layers.push({ - ssrc: baseLayerSSRC, - msid: baseLayer.msid, - cname: baseLayer.cname + // 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)); }); + } + }); + }); - var rtx = false; // RFC 4588 - if (Array.isArray(mLine.rtp)) { - rtx = mLine.rtp.some( - function (rtpmap) { return rtpmap.codec === 'rtx'; }); - } + // then for each media line, follow the innerOrder + session.media.forEach(function (mLine) { + sdp.push(makeLine('m', grammar.m[0], mLine)); - if (rtx) { - this.layers[0].rtx = generateSSRC(); + innerOrder.forEach(function (type) { + grammar[type].forEach(function (obj) { + if (obj.name in mLine && mLine[obj.name] != null) { + sdp.push(makeLine(type, obj, mLine)); } - - // now push additional layers. - for (var i = 1; i < Math.max(1, this.options.numOfLayers); i++) { - - var layer = { ssrc: generateSSRC() }; - if (rtx) { - layer.rtx = generateSSRC(); - } - - this.layers.push(layer); + 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'; }; -/** +},{"./grammar":68}],72:[function(require,module,exports){ +/* Copyright @ 2015 Atlassian Pty Ltd * - * @param mLine - * @private + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ -Simulcast.prototype._restoreSimulcastView = function(mLine) { - if (mLine && mLine.type === 'video' && this.layers.length !== 0) { - var sources = {}; - - var msid = this.layers[0].msid; - var cname = this.layers[0].cname; - var simulcastSsrcs = []; - var ssrcGroups = []; +var transform = require('sdp-transform'); +var transformUtils = require('./transform-utils'); +var parseSsrcs = transformUtils.parseSsrcs; +var writeSsrcs = transformUtils.writeSsrcs; - for (var i = 0; i < this.layers.length; i++) { - var layer = this.layers[i]; +//region Constants - sources[layer.ssrc] = { msid: msid, cname: cname }; - simulcastSsrcs.push(layer.ssrc); +var DEFAULT_NUM_OF_LAYERS = 3; - if (layer.rtx) { +//endregion - sources[layer.rtx] = { - msid: msid, - cname: cname - } +//region Ctor - ssrcGroups.push({ - semantics: 'FID', - ssrcs: [layer.ssrc, layer.rtx].join(' ') - }); - } - } +function Simulcast(options) { - ssrcGroups.push({ - semantics: 'SIM', - ssrcs: simulcastSsrcs.join(' ') - }); + this.options = options ? options : {}; - mLine.ssrcGroups = ssrcGroups; - mLine.ssrcs = writeSsrcs(sources); + if (!this.options.numOfLayers) { + this.options.numOfLayers = DEFAULT_NUM_OF_LAYERS; } + + this.layers = []; } //endregion -//region "Public" functions - -Simulcast.prototype.isSupported = function () { - return window.chrome; - - // TODO this needs improvements. For example I doubt that Chrome in Android - // has simulcast support. Also, only recent versions of Chromium have native - // simulcast support. -} +//region Stateless private utility functions /** - * - * @param desc - * @returns {RTCSessionDescription} + * Returns a random integer between min (included) and max (excluded) + * Using Math.round() gives a non-uniform distribution! + * @returns {number} */ -Simulcast.prototype.mungeRemoteDescription = function (desc) { +function generateSSRC() { + var min = 0, max = 0xffffffff; + return Math.floor(Math.random() * (max - min)) + min; +}; - if (!validateDescription(desc)) { - return desc; +function processVideo(session, action) { + if (session == null || !Array.isArray(session.media)) { + return; } - var session = transform.parse(desc.sdp); + session.media.forEach(function (mLine) { + if (mLine.type === 'video') { + action(mLine); + } + }); +} - var self = this; - processVideo(session, function (mLine) { +function validateDescription(desc) +{ + return desc && desc != null + && desc.type && desc.type != '' + && desc.sdp && desc.sdp != ''; +} - // Handle simulcast reception. - if (self.options.explodeRemoteSimulcast) { - explodeRemoteSimulcast(mLine); - } else { - implodeRemoteSimulcast(mLine); +function explodeRemoteSimulcast(mLine) { + + if (!mLine || !Array.isArray(mLine.ssrcGroups)) { + return; + } + + var sources = parseSsrcs(mLine); + var order = []; + + // Find the SIM group and explode its sources. + var j = mLine.ssrcGroups.length; + while (j--) { + + if (mLine.ssrcGroups[j].semantics !== 'SIM') { + continue; } - // If native simulcast is enabled, we must append the x-goog-conference - // attribute to the SDP. - if (self.layers.length < 1) { - removeGoogConference(mLine); - } else { - assertGoogConference(mLine); + var simulcastSsrcs = mLine.ssrcGroups[j].ssrcs.split(' '); + + for (var i = 0; i < simulcastSsrcs.length; i++) { + + var ssrc = simulcastSsrcs[i]; + order.push(ssrc); + + var parts = sources[ssrc].msid.split(' '); + sources[ssrc].msid = [parts[0], '/', i, ' ', parts[1], '/', i].join(''); + sources[ssrc].cname = [sources[ssrc].cname, '/', i].join(''); + + // Remove all the groups that this SSRC participates in. + mLine.ssrcGroups.forEach(function (relatedGroup) { + if (relatedGroup.semantics === 'SIM') { + return; + } + + var relatedSsrcs = relatedGroup.ssrcs.split(' '); + if (relatedSsrcs.indexOf(ssrc) === -1) { + return; + } + + // Nuke all the related SSRCs. + relatedSsrcs.forEach(function (relatedSSRC) { + sources[relatedSSRC].msid = sources[ssrc].msid; + sources[relatedSSRC].cname = sources[ssrc].cname; + if (relatedSSRC !== ssrc) { + order.push(relatedSSRC); + } + }); + + // Schedule the related group for nuking. + }) } - }); - return new RTCSessionDescription({ - type: desc.type, - sdp: transform.write(session) - }); -}; + mLine.ssrcs = writeSsrcs(sources, order); + mLine.ssrcGroups.splice(j, 1); + }; +} -/** - * - * @param desc - * @returns {RTCSessionDescription} - */ -Simulcast.prototype.mungeLocalDescription = function (desc) { +function implodeRemoteSimulcast(mLine) { - if (!validateDescription(desc) || !this.isSupported()) { - return desc; + if (!mLine || !Array.isArray(mLine.ssrcGroups)) { + console.info('Halt: There are no SSRC groups in the remote ' + + 'description.'); + return; } - var session = transform.parse(desc.sdp); + var sources = parseSsrcs(mLine); - var self = this; - processVideo(session, function (mLine) { - if (mLine.direction == 'recvonly' || mLine.direction == 'inactive') - { + // Find the SIM group and nuke it. + mLine.ssrcGroups.forEach(function (simulcastGroup) { + if (simulcastGroup.semantics !== 'SIM') { return; } - // Initialize native simulcast layers, if not already done. - self._maybeInitializeLayers(mLine); - // Update the SDP with the simulcast layers. - self._restoreSimulcastView(mLine); - }); + console.info("Imploding SIM group: " + simulcastGroup.ssrcs); + // Schedule the SIM group for nuking. + simulcastGroup.nuke = true; - return new RTCSessionDescription({ - type: desc.type, - sdp: transform.write(session) + var simulcastSsrcs = simulcastGroup.ssrcs.split(' '); + + // Nuke all the higher layer SSRCs. + for (var i = 1; i < simulcastSsrcs.length; i++) { + + var ssrc = simulcastSsrcs[i]; + delete sources[ssrc]; + + // Remove all the groups that this SSRC participates in. + mLine.ssrcGroups.forEach(function (relatedGroup) { + if (relatedGroup.semantics === 'SIM') { + return; + } + + var relatedSsrcs = relatedGroup.ssrcs.split(' '); + if (relatedSsrcs.indexOf(ssrc) === -1) { + return; + } + + // Nuke all the related SSRCs. + relatedSsrcs.forEach(function (relatedSSRC) { + delete sources[relatedSSRC]; + }); + + // Schedule the related group for nuking. + relatedGroup.nuke = true; + }) + } + + return; }); -}; + + mLine.ssrcs = writeSsrcs(sources); + + // Nuke all the scheduled groups. + var i = mLine.ssrcGroups.length; + while (i--) { + if (mLine.ssrcGroups[i].nuke) { + mLine.ssrcGroups.splice(i, 1); + } + } +} + +function removeGoogConference(mLine) { + if (!mLine || !Array.isArray(mLine.invalid)) { + return; + } + + var i = mLine.invalid.length; + while (i--) { + if (mLine.invalid[i].value == 'x-google-flag:conference') { + mLine.invalid.splice(i, 1); + } + } +} + +function assertGoogConference(mLine) { + if (!mLine) { + return; + } + + if (!Array.isArray(mLine.invalid)) { + mLine.invalid = []; + } + + if (!mLine.invalid.some( + function (i) { return i.value === 'x-google-flag:conference' })) { + mLine.invalid.push({'value': 'x-google-flag:conference'}); + } +} //endregion -module.exports = Simulcast; +//region "Private" functions -},{"./transform-utils":69,"sdp-transform":71}],69:[function(require,module,exports){ -/* Copyright @ 2015 Atlassian Pty Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 +/** * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * @param mLine + * @private */ +Simulcast.prototype._maybeInitializeLayers = function(mLine) { -exports.writeSsrcs = function(sources, order) { - var ssrcs = []; + if (!mLine || mLine.type !== 'video') { + return; + } - // expand sources to ssrcs - if (typeof sources !== 'undefined' && - Object.keys(sources).length !== 0) { + var sources = parseSsrcs(mLine); - if (Array.isArray(order)) { - for (var i = 0; i < order.length; i++) { - var ssrc = order[i]; - var source = sources[ssrc]; - Object.keys(source).forEach(function (attribute) { - ssrcs.push({ - id: ssrc, - attribute: attribute, - value: source[attribute] - }); - }); - } - } else { - Object.keys(sources).forEach(function (ssrc) { - var source = sources[ssrc]; - Object.keys(source).forEach(function (attribute) { - ssrcs.push({ - id: ssrc, - attribute: attribute, - value: source[attribute] - }); - }); - }); + if (Object.keys(sources).length === 0) { + + // no sources, disable simulcast. + if (this.layers.length !== 0) { + this.layers = []; + } + + return; } - } - return ssrcs; -}; + // find the base layer (we'll reuse its msid and cname). + var baseLayerSSRC = Object.keys(sources)[0]; + var baseLayer = sources[baseLayerSSRC]; -exports.parseSsrcs = function (mLine) { - var sources = {}; - // group sources attributes by ssrc. - if (typeof mLine.ssrcs !== 'undefined' && Array.isArray(mLine.ssrcs)) { - mLine.ssrcs.forEach(function (ssrc) { - if (!sources[ssrc.id]) - sources[ssrc.id] = {}; - sources[ssrc.id][ssrc.attribute] = ssrc.value; - }); - } - return sources; -}; + // todo(gp) handle screen sharing. + // check if base CNAME has changed and reinitialise layers. + if (this.layers.length > 0 + && sources[baseLayerSSRC].cname !== this.layers[0].cname) { + this.layers = []; + } -},{}],70:[function(require,module,exports){ -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:%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 - //a=candidate:229815620 1 tcp 1518280447 192.168.150.19 60017 typ host tcptype active generation 0 - //a=candidate:3289912957 2 tcp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 tcptype passive generation 0 - push:'candidates', - reg: /^candidate:(\S*) (\d*) (\S*) (\d*) (\S*) (\d*) typ (\S*)(?: raddr (\S*) rport (\d*))?(?: tcptype (\S*))?(?: generation (\d*))?/, - names: ['foundation', 'component', 'transport', 'priority', 'ip', 'port', 'type', 'raddr', 'rport', 'tcptype', 'generation'], - format: function (o) { - var str = "candidate:%s %d %s %d %s %d typ %s"; + // (re)initialise layers + if (this.layers.length < 1) { - str += (o.raddr != null) ? " raddr %s rport %d" : "%v%v"; + // first push the base layer. + this.layers.push({ + ssrc: baseLayerSSRC, + msid: baseLayer.msid, + cname: baseLayer.cname + }); - // NB: candidate has three optional chunks, so %void middles one if it's missing - str += (o.tcptype != null) ? " tcptype %s" : "%v"; + var rtx = false; // RFC 4588 + if (Array.isArray(mLine.rtp)) { + rtx = mLine.rtp.some( + function (rtpmap) { return rtpmap.codec === 'rtx'; }); + } - if (o.generation != null) { - str += " generation %d"; + if (rtx) { + this.layers[0].rtx = generateSSRC(); + } + + // now push additional layers. + for (var i = 1; i < Math.max(1, this.options.numOfLayers); i++) { + + var layer = { ssrc: generateSSRC() }; + if (rtx) { + layer.rtx = generateSSRC(); + } + + this.layers.push(layer); } - 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"; - } - }); -}); +/** + * + * @param mLine + * @private + */ +Simulcast.prototype._restoreSimulcastView = function(mLine) { + if (mLine && mLine.type === 'video' && this.layers.length !== 0) { -},{}],71:[function(require,module,exports){ -var parser = require('./parser'); -var writer = require('./writer'); + var sources = {}; -exports.write = writer; -exports.parse = parser.parse; -exports.parseFmtpConfig = parser.parseFmtpConfig; -exports.parsePayloads = parser.parsePayloads; -exports.parseRemoteCandidates = parser.parseRemoteCandidates; + var msid = this.layers[0].msid; + var cname = this.layers[0].cname; + var simulcastSsrcs = []; + var ssrcGroups = []; -},{"./parser":72,"./writer":73}],72:[function(require,module,exports){ -var toIntIfInt = function (v) { - return String(Number(v)) === v ? Number(v) : v; -}; + for (var i = 0; i < this.layers.length; i++) { + var layer = this.layers[i]; -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]); - } - } - } -}; + sources[layer.ssrc] = { msid: msid, cname: cname }; + simulcastSsrcs.push(layer.ssrc); -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 + if (layer.rtx) { - attachProperties(content.match(obj.reg), keyLocation, obj.names, obj.name); + sources[layer.rtx] = { + msid: msid, + cname: cname + } - if (obj.push) { - location[obj.push].push(keyLocation); - } -}; + ssrcGroups.push({ + semantics: 'FID', + ssrcs: [layer.ssrc, layer.rtx].join(' ') + }); + } + } -var grammar = require('./grammar'); -var validLine = RegExp.prototype.test.bind(/^([a-z])=(.*)/); + ssrcGroups.push({ + semantics: 'SIM', + ssrcs: simulcastSsrcs.join(' ') + }); -exports.parse = function (sdp) { - var session = {} - , media = [] - , location = session; // points at where properties go under (one of the above) + mLine.ssrcGroups = ssrcGroups; + mLine.ssrcs = writeSsrcs(sources); + } +} + +//endregion + +//region "Public" functions + +Simulcast.prototype.isSupported = function () { + return window.chrome; + + // TODO this needs improvements. For example I doubt that Chrome in Android + // has simulcast support. Also, only recent versions of Chromium have native + // simulcast support. +} - // 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 - } +/** + * + * @param desc + * @returns {RTCSessionDescription} + */ +Simulcast.prototype.mungeRemoteDescription = function (desc) { - 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); - } + if (!validateDescription(desc)) { + return desc; } - }); - session.media = media; // link it up - return session; -}; + var session = transform.parse(desc.sdp); -var fmtpReducer = function (acc, expr) { - var s = expr.split('='); - if (s.length === 2) { - acc[s[0]] = toIntIfInt(s[1]); - } - return acc; -}; + var self = this; + processVideo(session, function (mLine) { -exports.parseFmtpConfig = function (str) { - return str.split(/\;\s?/).reduce(fmtpReducer, {}); -}; + // Handle simulcast reception. + if (self.options.explodeRemoteSimulcast) { + explodeRemoteSimulcast(mLine); + } else { + implodeRemoteSimulcast(mLine); + } -exports.parsePayloads = function (str) { - return str.split(' ').map(Number); -}; + // If native simulcast is enabled, we must append the x-goog-conference + // attribute to the SDP. + if (self.layers.length < 1) { + removeGoogConference(mLine); + } else { + assertGoogConference(mLine); + } + }); -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 new RTCSessionDescription({ + type: desc.type, + sdp: transform.write(session) }); - } - return candidates; }; -},{"./grammar":70}],73:[function(require,module,exports){ -var grammar = require('./grammar'); +/** + * + * @param desc + * @returns {RTCSessionDescription} + */ +Simulcast.prototype.mungeLocalDescription = function (desc) { -// 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 ''; + if (!validateDescription(desc) || !this.isSupported()) { + return desc; } - }); - // 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 session = transform.parse(desc.sdp); - 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); + var self = this; + processVideo(session, function (mLine) { + if (mLine.direction == 'recvonly' || mLine.direction == 'inactive') + { + return; + } + // Initialize native simulcast layers, if not already done. + self._maybeInitializeLayers(mLine); + + // Update the SDP with the simulcast layers. + self._restoreSimulcastView(mLine); + }); + + return new RTCSessionDescription({ + type: desc.type, + sdp: transform.write(session) + }); }; -// 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']; +//endregion +module.exports = Simulcast; -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 = ""; - } - }); +},{"./transform-utils":73,"sdp-transform":75}],73:[function(require,module,exports){ +/* Copyright @ 2015 Atlassian Pty Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ - var outerOrder = opts.outerOrder || defaultOuterOrder; - var innerOrder = opts.innerOrder || defaultInnerOrder; - var sdp = []; +exports.writeSsrcs = function(sources, order) { + var ssrcs = []; - // 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)); + // expand sources to ssrcs + if (typeof sources !== 'undefined' && + Object.keys(sources).length !== 0) { + + if (Array.isArray(order)) { + for (var i = 0; i < order.length; i++) { + var ssrc = order[i]; + var source = sources[ssrc]; + Object.keys(source).forEach(function (attribute) { + ssrcs.push({ + id: ssrc, + attribute: attribute, + value: source[attribute] + }); }); } - }); - }); - - // 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)); + } else { + Object.keys(sources).forEach(function (ssrc) { + var source = sources[ssrc]; + Object.keys(source).forEach(function (attribute) { + ssrcs.push({ + id: ssrc, + attribute: attribute, + value: source[attribute] }); - } + }); }); - }); - }); + } + } - return sdp.join('\r\n') + '\r\n'; + return ssrcs; +}; + +exports.parseSsrcs = function (mLine) { + var sources = {}; + // group sources attributes by ssrc. + if (typeof mLine.ssrcs !== 'undefined' && Array.isArray(mLine.ssrcs)) { + mLine.ssrcs.forEach(function (ssrc) { + if (!sources[ssrc.id]) + sources[ssrc.id] = {}; + sources[ssrc.id][ssrc.attribute] = ssrc.value; + }); + } + return sources; }; -},{"./grammar":70}],74:[function(require,module,exports){ + +},{}],74:[function(require,module,exports){ +arguments[4][68][0].apply(exports,arguments) +},{"dup":68}],75:[function(require,module,exports){ +arguments[4][69][0].apply(exports,arguments) +},{"./parser":76,"./writer":77,"dup":69}],76:[function(require,module,exports){ +arguments[4][70][0].apply(exports,arguments) +},{"./grammar":74,"dup":70}],77:[function(require,module,exports){ +arguments[4][71][0].apply(exports,arguments) +},{"./grammar":74,"dup":71}],78:[function(require,module,exports){ var MediaStreamType = { VIDEO_TYPE: "Video", AUDIO_TYPE: "Audio" }; module.exports = MediaStreamType; -},{}],75:[function(require,module,exports){ +},{}],79:[function(require,module,exports){ var RTCEvents = { RTC_READY: "rtc.ready", DATA_CHANNEL_OPEN: "rtc.data_channel_open", @@ -23347,7 +23375,7 @@ var RTCEvents = { }; module.exports = RTCEvents; -},{}],76:[function(require,module,exports){ +},{}],80:[function(require,module,exports){ var Resolutions = { "1080": { width: 1920, @@ -23401,7 +23429,7 @@ var Resolutions = { } }; module.exports = Resolutions; -},{}],77:[function(require,module,exports){ +},{}],81:[function(require,module,exports){ var AuthenticationEvents = { /** * Event callback arguments: @@ -23415,7 +23443,7 @@ var AuthenticationEvents = { }; module.exports = AuthenticationEvents; -},{}],78:[function(require,module,exports){ +},{}],82:[function(require,module,exports){ var DesktopSharingEventTypes = { /** * An event which indicates that the jidesha extension for Firefox is @@ -23426,7 +23454,7 @@ var DesktopSharingEventTypes = { module.exports = DesktopSharingEventTypes; -},{}],79:[function(require,module,exports){ +},{}],83:[function(require,module,exports){ module.exports = { /** * An event carrying connection statistics. @@ -23442,12 +23470,12 @@ module.exports = { STOP: "statistics.stop" }; -},{}],80:[function(require,module,exports){ +},{}],84:[function(require,module,exports){ var Constants = { LOCAL_JID: 'local' }; module.exports = Constants; -},{}],81:[function(require,module,exports){ +},{}],85:[function(require,module,exports){ var XMPPEvents = { // Designates an event indicating that the connection to the XMPP server // failed.