From 0262917aa67aa81f7aa8254edd96a1714d7ed5f9 Mon Sep 17 00:00:00 2001 From: isymchych Date: Thu, 31 Dec 2015 17:23:23 +0200 Subject: [PATCH] implementation of "start muted" --- .gitattributes | 1 + app.js | 2 +- conference.js | 23 +- libs/lib-jitsi-meet.js | 1457 +++++++++++------ modules/UI/UI.js | 4 + .../UI/side_pannels/settings/SettingsMenu.js | 100 +- modules/UI/videolayout/VideoLayout.js | 4 +- modules/desktopsharing/desktopsharing.js | 2 +- service/xmpp/XMPPEvents.js | 2 - 9 files changed, 1032 insertions(+), 563 deletions(-) diff --git a/.gitattributes b/.gitattributes index ce82d6ffe..f7380cdf0 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ *.bundle.js -text -diff +lib-jitsi-meet.js -text -diff diff --git a/app.js b/app.js index a7a2f61d7..7ec3a8345 100644 --- a/app.js +++ b/app.js @@ -1,4 +1,4 @@ -/* global $, JitsiMeetJS, config, interfaceConfig */ +/* global $, JitsiMeetJS, config */ /* application specific logic */ import "babel-polyfill"; diff --git a/conference.js b/conference.js index 83185a45f..3c0813c51 100644 --- a/conference.js +++ b/conference.js @@ -15,8 +15,7 @@ const ConnectionErrors = JitsiMeetJS.errors.connection; const ConferenceEvents = JitsiMeetJS.events.conference; const ConferenceErrors = JitsiMeetJS.errors.conference; -let room, connection, localTracks, localAudio, localVideo; -let roomLocker = createRoomLocker(room); +let room, connection, localTracks, localAudio, localVideo, roomLocker; const Commands = { CONNECTION_QUALITY: "stats", @@ -223,7 +222,13 @@ export default { return room.isSIPCallingSupported(); }, get membersCount () { - return room.getParticipants().length; // FIXME maybe +1? + return room.getParticipants().length + 1; + }, + get startAudioMuted () { + return room && room.isStartAudioMuted(); + }, + get startVideoMuted () { + return room && room.isStartVideoMuted(); }, // used by torture currently isJoined () { @@ -241,6 +246,7 @@ export default { _createRoom () { room = connection.initJitsiConference(APP.conference.roomName, this._getConferenceOptions()); + roomLocker = createRoomLocker(room); this._room = room; // FIXME do not use this this.localId = room.myUserId(); @@ -508,7 +514,16 @@ export default { APP.UI.addListener(UIEvents.START_MUTED_CHANGED, (startAudioMuted, startVideoMuted) => { - // FIXME start muted + room.setStartMuted(startAudioMuted, startVideoMuted); + } + ); + room.on( + ConferenceEvents.START_MUTED, + function (startAudioMuted, startVideoMuted, initiallyMuted) { + APP.UI.onStartMutedChanged(); + if (initiallyMuted) { + APP.UI.notifyInitiallyMuted(); + } } ); diff --git a/libs/lib-jitsi-meet.js b/libs/lib-jitsi-meet.js index 9685739e6..85068add1 100644 --- a/libs/lib-jitsi-meet.js +++ b/libs/lib-jitsi-meet.js @@ -46,6 +46,9 @@ function JitsiConference(options) { this.somebodySupportsDTMF = false; this.authEnabled = false; this.authIdentity; + this.startAudioMuted = false; + this.startVideoMuted = false; + this.tracks = []; } /** @@ -252,6 +255,16 @@ JitsiConference.prototype.setDisplayName = function(name) { JitsiConference.prototype.addTrack = function (track) { this.room.addStream(track.getOriginalStream(), function () { this.rtc.addLocalStream(track); + if (this.startAudioMuted && track.isAudioTrack()) { + track.mute(); + } + if (this.startVideoMuted && track.isVideoTrack()) { + track.mute(); + } + if (track.startMuted) { + track.mute(); + } + this.tracks.push(track); var muteHandler = this._fireMuteChangeEvent.bind(this, track); var stopHandler = this.removeTrack.bind(this, track); var audioLevelHandler = this._fireAudioLevelChangeEvent.bind(this); @@ -293,6 +306,10 @@ JitsiConference.prototype._fireMuteChangeEvent = function (track) { * @param track the JitsiLocalTrack object. */ JitsiConference.prototype.removeTrack = function (track) { + var pos = this.tracks.indexOf(track); + if (pos > -1) { + this.tracks.splice(pos, 1); + } if(!this.room){ if(this.rtc) this.rtc.removeLocalStream(track); @@ -413,12 +430,13 @@ JitsiConference.prototype.muteParticipant = function (id) { this.room.muteParticipant(participant.getJid(), true); }; -JitsiConference.prototype.onMemberJoined = function (jid, nick) { +JitsiConference.prototype.onMemberJoined = function (jid, nick, role) { var id = Strophe.getResourceFromJid(jid); if (id === 'focus' || this.myUserId() === id) { return; } var participant = new JitsiParticipant(jid, this, nick); + participant._role = role; this.participants[id] = participant; this.eventEmitter.emit(JitsiConferenceEvents.USER_JOINED, id, participant); this.xmpp.connection.disco.info( @@ -652,6 +670,41 @@ JitsiConference.prototype.getConnectionState = function () { return null; } +/** + * Make all new participants mute their audio/video on join. + * @param {boolean} audioMuted if audio should be muted. + * @param {boolean} videoMuted if video should be muted. + */ +JitsiConference.prototype.setStartMuted = function (audioMuted, videoMuted) { + if (!this.isModerator()) { + return; + } + + this.room.removeFromPresence("startmuted"); + this.room.addToPresence("startmuted", { + attributes: { + audio: audioMuted, + video: videoMuted, + xmlns: 'http://jitsi.org/jitmeet/start-muted' + } + }); + this.room.sendPresence(); +}; + +/** + * Check if audio is muted on join. + */ +JitsiConference.prototype.isStartAudioMuted = function () { + return this.startAudioMuted; +}; + +/** + * Check if video is muted on join. + */ +JitsiConference.prototype.isStartVideoMuted = function () { + return this.startVideoMuted; +}; + /** * Setups the listeners needed for the conference. * @param conference the conference @@ -693,6 +746,9 @@ function setupListeners(conference) { conference.room.addListener(XMPPEvents.AUTHENTICATION_REQUIRED, function () { conference.eventEmitter.emit(JitsiConferenceEvents.CONFERENCE_FAILED, JitsiConferenceErrors.AUTHENTICATION_REQUIRED); }); + conference.room.addListener(XMPPEvents.BRIDGE_DOWN, function () { + conference.eventEmitter.emit(JitsiConferenceEvents.CONFERENCE_FAILED, JitsiConferenceErrors.VIDEOBRIDGE_NOT_AVAILABLE); + }); // FIXME // conference.room.addListener(XMPPEvents.MUC_JOINED, function () { // conference.eventEmitter.emit(JitsiConferenceEvents.CONFERENCE_LEFT); @@ -764,6 +820,70 @@ function setupListeners(conference) { conference.eventEmitter.emit(JitsiConferenceErrors.PASSWORD_REQUIRED); }); + conference.xmpp.addListener(XMPPEvents.START_MUTED_FROM_FOCUS, function (audioMuted, videoMuted) { + conference.startAudioMuted = audioMuted; + conference.startVideoMuted = videoMuted; + + // mute existing local tracks because this is initial mute from Jicofo + conference.tracks.forEach(function (track) { + if (conference.startAudioMuted && track.isAudioTrack()) { + track.mute(); + } + if (conference.startVideoMuted && track.isVideoTrack()) { + track.mute(); + } + }); + + var initiallyMuted = audioMuted || videoMuted; + + conference.eventEmitter.emit( + JitsiConferenceEvents.START_MUTED, + conference.startAudioMuted, + conference.startVideoMuted, + initiallyMuted + ); + }); + + conference.room.addPresenceListener("startmuted", function (data, from) { + var isModerator = false; + if (conference.myUserId() === from && conference.isModerator()) { + isModerator = true; + } else { + var participant = conference.getParticipantById(from); + if (participant && participant.isModerator()) { + isModerator = true; + } + } + + if (!isModerator) { + return; + } + + var startAudioMuted = data.attributes.audio === 'true'; + var startVideoMuted = data.attributes.video === 'true'; + + var updated = false; + + if (startAudioMuted !== conference.startAudioMuted) { + conference.startAudioMuted = startAudioMuted; + updated = true; + } + + if (startVideoMuted !== conference.startVideoMuted) { + conference.startVideoMuted = startVideoMuted; + updated = true; + } + + if (updated) { + conference.eventEmitter.emit( + JitsiConferenceEvents.START_MUTED, + conference.startAudioMuted, + conference.startVideoMuted, + false + ); + } + }); + if(conference.statistics) { //FIXME: Maybe remove event should not be associated with the conference. conference.statistics.addAudioLevelListener(function (ssrc, level) { @@ -914,6 +1034,10 @@ var JitsiConferenceEvents = { * You are kicked from the conference. */ KICKED: "conferenece.kicked", + /** + * Indicates that start muted settings changed. + */ + START_MUTED: "conference.start_muted", /** * Indicates that DTMF support changed. */ @@ -1663,6 +1787,9 @@ JitsiLocalTrack.prototype.constructor = JitsiLocalTrack; * @param mute {boolean} if true the track will be muted. Otherwise the track will be unmuted. */ JitsiLocalTrack.prototype._setMute = function (mute) { + if (this.isMuted() === mute) { + return; + } if(!this.rtc) { this.startMuted = mute; return; @@ -6407,7 +6534,7 @@ ChatRoom.prototype.onPresence = function (pres) { } else { this.eventEmitter.emit( - XMPPEvents.MUC_MEMBER_JOINED, from, member.nick); + XMPPEvents.MUC_MEMBER_JOINED, from, member.nick, member.role); } } else { // Presence update for existing participant @@ -10667,7 +10794,7 @@ TraceablePeerConnection.prototype.getStats = function(callback, errback) { module.exports = TraceablePeerConnection; }).call(this,"/modules/xmpp/TraceablePeerConnection.js") -},{"../../service/xmpp/XMPPEvents":87,"../RTC/RTC":16,"../RTC/RTCBrowserType.js":17,"./LocalSSRCReplacement":29,"jitsi-meet-logger":48,"sdp-interop":66,"sdp-simulcast":73,"sdp-transform":76}],34:[function(require,module,exports){ +},{"../../service/xmpp/XMPPEvents":87,"../RTC/RTC":16,"../RTC/RTCBrowserType.js":17,"./LocalSSRCReplacement":29,"jitsi-meet-logger":48,"sdp-interop":66,"sdp-simulcast":69,"sdp-transform":76}],34:[function(require,module,exports){ (function (__filename){ /* global $, $iq, Promise, Strophe */ @@ -22456,487 +22583,7 @@ exports.parse = function(sdp) { }; -},{"sdp-transform":70}],69:[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"; - - 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"; - } - 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"; - } - }); -}); - -},{}],70:[function(require,module,exports){ -var 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; - -},{"./parser":71,"./writer":72}],71:[function(require,module,exports){ -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 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(/\;\s?/).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; -}; - -},{"./grammar":69}],72:[function(require,module,exports){ -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'; -}; - -},{"./grammar":69}],73:[function(require,module,exports){ +},{"sdp-transform":76}],69:[function(require,module,exports){ /* Copyright @ 2015 Atlassian Pty Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23069,7 +22716,7 @@ function explodeRemoteSimulcast(mLine) { function implodeRemoteSimulcast(mLine) { if (!mLine || !Array.isArray(mLine.ssrcGroups)) { - console.info('Halt: There are no SSRC groups in the remote ' + + console.debug('Halt: There are no SSRC groups in the remote ' + 'description.'); return; } @@ -23082,7 +22729,7 @@ function implodeRemoteSimulcast(mLine) { return; } - console.info("Imploding SIM group: " + simulcastGroup.ssrcs); + console.debug("Imploding SIM group: " + simulcastGroup.ssrcs); // Schedule the SIM group for nuking. simulcastGroup.nuke = true; @@ -23356,7 +23003,7 @@ Simulcast.prototype.mungeLocalDescription = function (desc) { module.exports = Simulcast; -},{"./transform-utils":74,"sdp-transform":76}],74:[function(require,module,exports){ +},{"./transform-utils":70,"sdp-transform":72}],70:[function(require,module,exports){ /* Copyright @ 2015 Atlassian Pty Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23422,15 +23069,827 @@ exports.parseSsrcs = function (mLine) { }; -},{}],75:[function(require,module,exports){ -arguments[4][69][0].apply(exports,arguments) -},{"dup":69}],76:[function(require,module,exports){ -arguments[4][70][0].apply(exports,arguments) -},{"./parser":77,"./writer":78,"dup":70}],77:[function(require,module,exports){ -arguments[4][71][0].apply(exports,arguments) -},{"./grammar":75,"dup":71}],78:[function(require,module,exports){ +},{}],71:[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\-]*)\/(\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"; + } + }); +}); + +},{}],72:[function(require,module,exports){ +var 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; + +},{"./parser":73,"./writer":74}],73:[function(require,module,exports){ +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 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; +}; + +},{"./grammar":71}],74:[function(require,module,exports){ +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'; +}; + +},{"./grammar":71}],75:[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\-]*)\/(\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 + //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 + 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"; + } + }); +}); + +},{}],76:[function(require,module,exports){ arguments[4][72][0].apply(exports,arguments) -},{"./grammar":75,"dup":72}],79:[function(require,module,exports){ +},{"./parser":77,"./writer":78,"dup":72}],77:[function(require,module,exports){ +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 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(/\;\s?/).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; +}; + +},{"./grammar":75}],78:[function(require,module,exports){ +arguments[4][74][0].apply(exports,arguments) +},{"./grammar":75,"dup":74}],79:[function(require,module,exports){ var MediaStreamType = { VIDEO_TYPE: "Video", @@ -23564,9 +24023,6 @@ var XMPPEvents = { // Designates an event indicating that the focus has asked us to mute our // audio. AUDIO_MUTED_BY_FOCUS: "xmpp.audio_muted_by_focus", - // Designates an event indicating that a moderator in the room changed the - // "start muted" settings for the conference. - START_MUTED_SETTING_CHANGED: "xmpp.start_muted_setting_changed", // Designates an event indicating that we should join the conference with // audio and/or video muted. START_MUTED_FROM_FOCUS: "xmpp.start_muted_from_focus", @@ -23630,11 +24086,6 @@ module.exports = AuthenticationEvents; },{}],84:[function(require,module,exports){ var DesktopSharingEventTypes = { - INIT: "ds.init", - - SWITCHING_DONE: "ds.switching_done", - - NEW_STREAM_CREATED: "ds.new_stream_created", /** * An event which indicates that the jidesha extension for Firefox is * needed to proceed with screen sharing, and that it is not installed. diff --git a/modules/UI/UI.js b/modules/UI/UI.js index 6bc4e010a..e98a738da 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -781,4 +781,8 @@ UI.stopPrezi = function (userId) { } }; +UI.onStartMutedChanged = function () { + SettingsMenu.onStartMutedChanged(); +}; + module.exports = UI; diff --git a/modules/UI/side_pannels/settings/SettingsMenu.js b/modules/UI/side_pannels/settings/SettingsMenu.js index 881cb10c5..77413b844 100644 --- a/modules/UI/side_pannels/settings/SettingsMenu.js +++ b/modules/UI/side_pannels/settings/SettingsMenu.js @@ -2,6 +2,7 @@ import UIUtil from "../../util/UIUtil"; import UIEvents from "../../../../service/UI/UIEvents"; import languages from "../../../../service/translation/languages"; +import Settings from '../../../settings/Settings'; function generateLanguagesSelectBox() { var currentLang = APP.translation.getCurrentLanguage(); @@ -21,32 +22,53 @@ function generateLanguagesSelectBox() { } -const SettingsMenu = { +export default { + init (emitter) { + function update() { + let displayName = UIUtil.escapeHtml($('#setDisplayName').val()); - init: function (emitter) { - this.emitter = emitter; - - var startMutedSelector = $("#startMutedOptions"); - startMutedSelector.before(generateLanguagesSelectBox()); - APP.translation.translateElement($("#languages_selectbox")); - $('#settingsmenu>input').keyup(function(event){ - if(event.keyCode === 13) {//enter - SettingsMenu.update(); + if (displayName && Settings.getDisplayName() !== displayName) { + emitter.emit(UIEvents.NICKNAME_CHANGED, displayName); } - }); - if (APP.conference.isModerator) { - startMutedSelector.css("display", "block"); - } else { - startMutedSelector.css("display", "none"); + let language = $("#languages_selectbox").val(); + if (language !== Settings.getLanguage()) { + emitter.emit(UIEvents.LANG_CHANGED, language); + } + + let email = UIUtil.escapeHtml($('#setEmail').val()); + if (email !== Settings.getEmail()) { + emitter.emit(UIEvents.EMAIL_CHANGED, email); + } + + let startAudioMuted = $("#startAudioMuted").is(":checked"); + let startVideoMuted = $("#startVideoMuted").is(":checked"); + if (startAudioMuted !== APP.conference.startAudioMuted + || startVideoMuted !== APP.conference.startVideoMuted) { + emitter.emit( + UIEvents.START_MUTED_CHANGED, + startAudioMuted, + startVideoMuted + ); + } } - $("#updateSettings").click(function () { - SettingsMenu.update(); + let startMutedBlock = $("#startMutedOptions"); + startMutedBlock.before(generateLanguagesSelectBox()); + APP.translation.translateElement($("#languages_selectbox")); + + this.onRoleChanged(); + this.onStartMutedChanged(); + + $("#updateSettings").click(update); + $('#settingsmenu>input').keyup(function(event){ + if (event.keyCode === 13) {//enter + update(); + } }); }, - onRoleChanged: function () { + onRoleChanged () { if(APP.conference.isModerator) { $("#startMutedOptions").css("display", "block"); } @@ -55,46 +77,22 @@ const SettingsMenu = { } }, - setStartMuted: function (audio, video) { - $("#startAudioMuted").attr("checked", audio); - $("#startVideoMuted").attr("checked", video); + onStartMutedChanged () { + $("#startAudioMuted").attr("checked", APP.conference.startAudioMuted); + $("#startVideoMuted").attr("checked", APP.conference.startVideoMuted); }, - update: function() { - // FIXME check if this values really changed: - // compare them with Settings etc. - var newDisplayName = - UIUtil.escapeHtml($('#setDisplayName').get(0).value); - - if (newDisplayName) { - this.emitter.emit(UIEvents.NICKNAME_CHANGED, newDisplayName); - } - - var language = $("#languages_selectbox").val(); - this.emitter.emit(UIEvents.LANG_CHANGED, language); - - var newEmail = UIUtil.escapeHtml($('#setEmail').get(0).value); - this.emitter.emit(UIEvents.EMAIL_CHANGED, newEmail); - - var startAudioMuted = ($("#startAudioMuted").is(":checked")); - var startVideoMuted = ($("#startVideoMuted").is(":checked")); - this.emitter.emit( - UIEvents.START_MUTED_CHANGED, startAudioMuted, startVideoMuted - ); - }, - - isVisible: function() { + isVisible () { return $('#settingsmenu').is(':visible'); }, - onDisplayNameChange: function(id, newDisplayName) { + onDisplayNameChange (id, newDisplayName) { if(id === 'localVideoContainer' || APP.conference.isLocalId(id)) { - $('#setDisplayName').get(0).value = newDisplayName; + $('#setDisplayName').val(newDisplayName); } }, - changeAvatar: function (thumbUrl) { - $('#avatar').get(0).src = thumbUrl; + + changeAvatar (thumbUrl) { + $('#avatar').attr('src', thumbUrl); } }; - -export default SettingsMenu; diff --git a/modules/UI/videolayout/VideoLayout.js b/modules/UI/videolayout/VideoLayout.js index b8db333ad..fa592e122 100644 --- a/modules/UI/videolayout/VideoLayout.js +++ b/modules/UI/videolayout/VideoLayout.js @@ -157,7 +157,9 @@ var VideoLayout = { let {thumbWidth, thumbHeight} = this.calculateThumbnailSize(); AudioLevels.updateAudioLevelCanvas(null, thumbWidth, thumbHeight); - localVideoThumbnail.changeVideo(stream); + if (!stream.isMuted()) { + localVideoThumbnail.changeVideo(stream); + } /* force update if we're currently being displayed */ if (this.isCurrentlyOnLarge(localId)) { diff --git a/modules/desktopsharing/desktopsharing.js b/modules/desktopsharing/desktopsharing.js index a4f646d8f..9ebae1732 100644 --- a/modules/desktopsharing/desktopsharing.js +++ b/modules/desktopsharing/desktopsharing.js @@ -97,7 +97,7 @@ module.exports = { } else { type = "video"; } - APP.createLocalTracks(type).then(function (tracks) { + APP.conference.createLocalTracks(type).then(function (tracks) { if (!tracks.length) { if (type === 'desktop') { getDesktopStreamFailed(); diff --git a/service/xmpp/XMPPEvents.js b/service/xmpp/XMPPEvents.js index 718fb3581..ab3febb5b 100644 --- a/service/xmpp/XMPPEvents.js +++ b/service/xmpp/XMPPEvents.js @@ -86,8 +86,6 @@ var XMPPEvents = { JINGLE_FATAL_ERROR: 'xmpp.jingle_fatal_error', PROMPT_FOR_LOGIN: 'xmpp.prompt_for_login', FOCUS_DISCONNECTED: 'xmpp.focus_disconnected', - ROOM_JOIN_ERROR: 'xmpp.room_join_error', - ROOM_CONNECT_ERROR: 'xmpp.room_connect_error', // xmpp is connected and obtained user media READY_TO_JOIN: 'xmpp.ready_to_join' };