From 0fd0f5b633f2a4f065058b7f168b909fe951078e Mon Sep 17 00:00:00 2001 From: isymchych Date: Fri, 25 Dec 2015 18:55:45 +0200 Subject: [PATCH 1/7] refactoring of Etherpad and Prezi --- app.js | 51 +- css/videolayout_default.css | 4 +- index.html | 19 + libs/lib-jitsi-meet.js | 1382 +++++++++++-------- modules/UI/UI.js | 60 +- modules/UI/audio_levels/AudioLevels.js | 54 +- modules/UI/etherpad/Etherpad.js | 214 +-- modules/UI/prezi/Prezi.js | 660 ++++----- modules/UI/prezi/PreziPlayer.js | 552 ++++---- modules/UI/side_pannels/SidePanelToggler.js | 22 +- modules/UI/toolbars/BottomToolbar.js | 14 +- modules/UI/toolbars/Toolbar.js | 34 +- modules/UI/util/UIUtil.js | 35 +- modules/UI/videolayout/LargeContainer.js | 24 + modules/UI/videolayout/LargeVideo.js | 895 ++++-------- modules/UI/videolayout/SmallVideo.js | 11 +- modules/UI/videolayout/VideoLayout.js | 299 ++-- service/UI/UIEvents.js | 4 +- 18 files changed, 2256 insertions(+), 2078 deletions(-) create mode 100644 modules/UI/videolayout/LargeContainer.js diff --git a/app.js b/app.js index 61bbf63bc..716b3ae5b 100644 --- a/app.js +++ b/app.js @@ -41,6 +41,9 @@ const Commands = { CONNECTION_QUALITY: "connectionQuality", EMAIL: "email", VIDEO_TYPE: "videoType" + ETHERPAD: "etherpad", + PREZI: "prezi", + STOP_PREZI: "stop-prezi" }; function buildRoomName () { @@ -218,16 +221,14 @@ function initConference(localTracks, connection) { room.on(ConferenceEvents.USER_JOINED, function (id, user) { - if (APP.conference.isLocalId(id)) { - return; - } - console.error('USER %s connnected', id); + console.error('USER %s connnected', id, user); // FIXME email??? APP.UI.addUser(id, user.getDisplayName()); }); room.on(ConferenceEvents.USER_LEFT, function (id, user) { - console.error('USER LEFT', id); + console.error('USER %s LEFT', id, user); APP.UI.removeUser(id, user.getDisplayName()); + APP.UI.stopPrezi(id); }); @@ -348,9 +349,12 @@ function initConference(localTracks, connection) { room.removeCommand(Commands.CONNECTION_QUALITY); }); // listen to remote stats - room.addCommandListener(Commands.CONNECTION_QUALITY, function (data) { - APP.connectionquality.updateRemoteStats(data.attributes.id, data.value); - }); + room.addCommandListener( + Commands.CONNECTION_QUALITY, + function ({value, attributes}) { + APP.connectionquality.updateRemoteStats(attributes.id, value); + } + ); APP.connectionquality.addListener( CQEvents.REMOTESTATS_UPDATED, function (id, percent, stats) { @@ -358,6 +362,37 @@ function initConference(localTracks, connection) { } ); + room.addCommandListener(Commands.ETHERPAD, function ({value}) { + APP.UI.initEtherpad(value); + }); + + + room.addCommandListener(Commands.PREZI, function ({value, attributes}) { + APP.UI.showPrezi(attributes.id, value, attributes.slide); + }); + room.addCommandListener(Commands.STOP_PREZI, function ({attributes}) { + APP.UI.stopPrezi(attributes.id); + }); + APP.UI.addListener(UIEvents.SHARE_PREZI, function (url, slide) { + console.log('Sharing Prezi %s slide %s', url, slide); + room.removeCommand(Commands.PREZI); + room.sendCommand(Commands.PREZI, { + value: url, + attributes: { + id: room.myUserId(), + slide + } + }); + }); + APP.UI.addListener(UIEvents.STOP_SHARING_PREZI, function () { + room.removeCommand(Commands.PREZI); + room.sendCommandOnce(Commands.STOP_PREZI, { + attributes: { + id: room.myUserId() + } + }); + }); + room.addCommandListener(Commands.VIDEO_TYPE, (data, from) => { APP.UI.onPeerVideoTypeChanged(from, data.value); }); diff --git a/css/videolayout_default.css b/css/videolayout_default.css index 02a71eafa..2935fcab9 100644 --- a/css/videolayout_default.css +++ b/css/videolayout_default.css @@ -34,7 +34,7 @@ } #remoteVideos .videocontainer { - display: inline-block; + display: none; background-color: black; background-size: contain; border-radius:8px; @@ -378,7 +378,7 @@ padding-right:2px; height:38px; width:auto; - background-color: rgba(0,0,0,0.8); + background-color: rgba(0,0,0,0.8); border: 1px solid rgba(256, 256, 256, 0.2); border-radius: 6px; pointer-events: auto; diff --git a/index.html b/index.html index 8790e2d8d..ecd1c37fc 100644 --- a/index.html +++ b/index.html @@ -140,6 +140,25 @@
+ +
+
+
+
+
+ + jitsi.org + +
+ + +
+
+ +
+ +
+
diff --git a/libs/lib-jitsi-meet.js b/libs/lib-jitsi-meet.js index fd5ab62f4..76532fb5a 100644 --- a/libs/lib-jitsi-meet.js +++ b/libs/lib-jitsi-meet.js @@ -403,7 +403,7 @@ JitsiConference.prototype.kickParticipant = function (id) { JitsiConference.prototype.onMemberJoined = function (jid, email, nick) { var id = Strophe.getResourceFromJid(jid); - if (id === 'focus') { + if (id === 'focus' || this.myUserId() === id) { return; } var participant = new JitsiParticipant(jid, this, nick); @@ -579,15 +579,6 @@ JitsiConference.prototype.toggleRecording = function (token, followEntity) { reject(new Error("The conference is not created yet!"))}); } -/** - * Returns true if the SIP calls are supported and false otherwise - */ -JitsiConference.prototype.isSIPCallingSupported = function () { - if(this.room) - return this.room.isSIPCallingSupported(); - return false; -} - /** * Dials a number. * @param number the number @@ -1068,13 +1059,6 @@ var LibJitsiMeet = { init: function (options) { return RTC.init(options || {}); }, - /** - * Returns whether the desktop sharing is enabled or not. - * @returns {boolean} - */ - isDesktopSharingEnabled: function () { - return RTC.isDesktopSharingEnabled(); - }, setLogLevel: function (level) { Logger.setLogLevel(level); }, @@ -1815,7 +1799,6 @@ module.exports = JitsiRemoteTrack; var RTCBrowserType = require("./RTCBrowserType"); var JitsiTrackEvents = require("../../JitsiTrackEvents"); var EventEmitter = require("events"); -var RTC = require("./RTCUtils"); /** * This implements 'onended' callback normally fired by WebRTC after the stream @@ -2006,21 +1989,13 @@ JitsiTrack.prototype.isScreenSharing = function(){ * Returns id of the track. * @returns {string} id of the track or null if this is fake track. */ -JitsiTrack.prototype._getId = function () { +JitsiTrack.prototype.getId = function () { var tracks = this.stream.getTracks(); if(!tracks || tracks.length === 0) return null; return tracks[0].id; }; -/** - * Returns id of the track. - * @returns {string} id of the track or null if this is fake track. - */ -JitsiTrack.prototype.getId = function () { - return RTC.getStreamID(this.stream); -}; - /** * Checks whether the MediaStream is avtive/not ended. * When there is no check for active we don't have information and so @@ -2272,14 +2247,6 @@ RTC.stopMediaStream = function (mediaStream) { RTCUtils.stopMediaStream(mediaStream); }; -/** - * Returns whether the desktop sharing is enabled or not. - * @returns {boolean} - */ -RTC.isDesktopSharingEnabled = function () { - return RTCUtils.isDesktopSharingEnabled(); -} - RTC.prototype.getVideoElementName = function () { return RTCBrowserType.isTemasysPluginUsed() ? 'object' : 'video'; }; @@ -3138,7 +3105,7 @@ var RTCUtils = { var deviceGUM = { "audio": GUM.bind(self, ["audio"]), "video": GUM.bind(self, ["video"]), - "desktop": screenObtainer.obtainStream.bind(screenObtainer) + "desktop": screenObtainer.obtainStream }; // With FF/IE we can't split the stream into audio and video because FF // doesn't support media stream constructors. So, we need to get the @@ -3244,13 +3211,6 @@ var RTCUtils = { if (mediaStream.stop) { mediaStream.stop(); } - }, - /** - * Returns whether the desktop sharing is enabled or not. - * @returns {boolean} - */ - isDesktopSharingEnabled: function () { - return screenObtainer.isSupported(); } }; @@ -6384,7 +6344,7 @@ ChatRoom.prototype.onPresence = function (pres) { ChatRoom.prototype.processNode = function (node, from) { if(this.presHandlers[node.tagName]) - this.presHandlers[node.tagName](node, Strophe.getResourceFromJid(from)); + this.presHandlers[node.tagName](node, from); }; ChatRoom.prototype.sendMessage = function (body, nickname) { @@ -6762,15 +6722,6 @@ ChatRoom.prototype.toggleRecording = function (token, followEntity) { reject(new Error("The conference is not created yet!"))}); } -/** - * Returns true if the SIP calls are supported and false otherwise - */ -ChatRoom.prototype.isSIPCallingSupported = function () { - if(this.moderator) - return this.moderator.isSipGatewayEnabled(); - return false; -} - /** * Dials a number. * @param number the number @@ -9137,11 +9088,11 @@ SDP.prototype.toJingle = function (elem, thecreator) { var msid = null; if(mline.media == "audio") { - msid = APP.RTC.localAudio._getId(); + msid = APP.RTC.localAudio.getId(); } else { - msid = APP.RTC.localVideo._getId(); + msid = APP.RTC.localVideo.getId(); } if(msid != null) { @@ -9537,6 +9488,7 @@ SDP.prototype.jingle2media = function (content) { module.exports = SDP; + }).call(this,"/modules/xmpp/SDP.js") },{"./SDPUtil":32,"jitsi-meet-logger":48}],31:[function(require,module,exports){ var SDPUtil = require("./SDPUtil"); @@ -10528,7 +10480,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, APP, config, messageHandler, roomName, sessionTerminated, Strophe, Util */ @@ -10570,7 +10522,7 @@ function Moderator(roomName, xmpp, emitter) { // Sip gateway can be enabled by configuring Jigasi host in config.js or // it will be enabled automatically if focus detects the component through // service discovery. - this.sipGatewayEnabled = this.xmppService.options.hosts && + this.sipGatewayEnabled = this.xmppService.options.hosts.call_control !== undefined; this.eventEmitter = emitter; @@ -22181,487 +22133,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"); @@ -22794,7 +22266,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; } @@ -22807,7 +22279,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; @@ -23081,7 +22553,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"); @@ -23147,15 +22619,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", diff --git a/modules/UI/UI.js b/modules/UI/UI.js index fcb4fae34..49a457439 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -2,7 +2,6 @@ /* jshint -W101 */ var UI = {}; -import AudioLevels from './audio_levels/AudioLevels'; import Chat from "./side_pannels/chat/Chat"; import Toolbar from "./toolbars/Toolbar"; import ToolbarToggler from "./toolbars/ToolbarToggler"; @@ -12,12 +11,12 @@ import Avatar from "./avatar/Avatar"; import PanelToggler from "./side_pannels/SidePanelToggler"; import UIUtil from "./util/UIUtil"; import UIEvents from "../../service/UI/UIEvents"; +import PreziManager from './prezi/Prezi'; +import EtherpadManager from './etherpad/Etherpad'; import VideoLayout from "./videolayout/VideoLayout"; import SettingsMenu from "./side_pannels/settings/SettingsMenu"; -var Prezi = require("./prezi/Prezi"); -var Etherpad = require("./etherpad/Etherpad"); var EventEmitter = require("events"); var Settings = require("./../settings/Settings"); UI.messageHandler = require("./util/MessageHandler"); @@ -32,6 +31,9 @@ var Feedback = require("./Feedback"); var eventEmitter = new EventEmitter(); UI.eventEmitter = eventEmitter; +let preziManager; +let etherpadManager; + function promptDisplayName() { let nickRequiredMsg = APP.translation.translateString("dialog.displayNameRequired"); let defaultNickMsg = APP.translation.translateString( @@ -77,12 +79,6 @@ function promptDisplayName() { ); } -function setupPrezi() { - $("#reloadPresentationLink").click(function() { - Prezi.reloadPresentation(); - }); -} - function setupChat() { Chat.init(eventEmitter); $("#toggle_smileys").click(function() { @@ -189,20 +185,18 @@ UI.initConference = function () { }; function registerListeners() { - UI.addListener(UIEvents.LARGEVIDEO_INIT, function () { - AudioLevels.init(); - }); - UI.addListener(UIEvents.EMAIL_CHANGED, function (email) { UI.setUserAvatar(APP.conference.localId, email); }); UI.addListener(UIEvents.PREZI_CLICKED, function () { - Prezi.openPreziDialog(); + preziManager.handlePreziButtonClicked(); }); UI.addListener(UIEvents.ETHERPAD_CLICKED, function () { - Etherpad.toggleEtherpad(0); + if (etherpadManager) { + etherpadManager.toggleEtherpad(); + } }); UI.addListener(UIEvents.FULLSCREEN_TOGGLE, toggleFullScreen); @@ -221,7 +215,7 @@ function registerListeners() { function bindEvents() { function onResize() { PanelToggler.resizeChat(); - VideoLayout.resizeLargeVideoContainer(); + VideoLayout.resizeLargeVideoContainer(PanelToggler.isVisible()); } // Resize and reposition videos in full screen mode. @@ -254,11 +248,17 @@ UI.start = function () { registerListeners(); VideoLayout.init(eventEmitter); + if (!interfaceConfig.filmStripOnly) { + VideoLayout.initLargeVideo(PanelToggler.isVisible()); + } + VideoLayout.resizeLargeVideoContainer(PanelToggler.isVisible()); + ContactList.init(eventEmitter); bindEvents(); - setupPrezi(); + preziManager = new PreziManager(eventEmitter); if (!interfaceConfig.filmStripOnly) { + $("#videospace").mousemove(function () { return ToolbarToggler.showToolbar(); }); @@ -350,9 +350,14 @@ UI.setSubject = function (subject) { Chat.setSubject(subject); }; -function initEtherpad(name) { - Etherpad.init(name); -} +UI.initEtherpad = function (name) { + if (etherpadManager) { + return; + } + console.log('Etherpad is enabled'); + etherpadManager = new EtherpadManager(config.etherpad_base, name); + Toolbar.showEtherpadButton(); +}; UI.addUser = function (id, displayName) { ContactList.addContact(id); @@ -443,7 +448,6 @@ UI.getSettings = function () { UI.toggleFilmStrip = function () { BottomToolbar.toggleFilmStrip(); - VideoLayout.updateLargeVideoSize(); }; UI.toggleChat = function () { @@ -592,9 +596,7 @@ UI.handleLastNEndpoints = function (ids) { }; UI.setAudioLevel = function (id, lvl) { - AudioLevels.updateAudioLevel( - id, lvl, VideoLayout.getLargeVideoId() - ); + VideoLayout.setAudioLevel(id, lvl); }; UI.updateDesktopSharingButtons = function () { @@ -750,4 +752,14 @@ UI.updateAuthInfo = function (isAuthEnabled, login) { } }; +UI.showPrezi = function (userId, url, slide) { + preziManager.showPrezi(userId, url, slide); +}; + +UI.stopPrezi = function (userId) { + if (preziManager.isSharing(userId)) { + preziManager.removePrezi(userId); + } +}; + module.exports = UI; diff --git a/modules/UI/audio_levels/AudioLevels.js b/modules/UI/audio_levels/AudioLevels.js index 2e99fe94d..01a4a31ab 100644 --- a/modules/UI/audio_levels/AudioLevels.js +++ b/modules/UI/audio_levels/AudioLevels.js @@ -112,33 +112,6 @@ function getVideoSpanId(id) { return videoSpanId; } -/** - * Indicates that the remote video has been resized. - */ -$(document).bind('remotevideo.resized', function (event, width, height) { - let resized = false; - - $('#remoteVideos>span>canvas').each(function() { - let canvas = $(this).get(0); - if (canvas.width !== width + interfaceConfig.CANVAS_EXTRA) { - canvas.width = width + interfaceConfig.CANVAS_EXTRA; - resized = true; - } - - if (canvas.height !== height + interfaceConfig.CANVAS_EXTRA) { - canvas.height = height + interfaceConfig.CANVAS_EXTRA; - resized = true; - } - }); - - if (resized) { - Object.keys(audioLevelCanvasCache).forEach(function (id) { - audioLevelCanvasCache[id].width = width + interfaceConfig.CANVAS_EXTRA; - audioLevelCanvasCache[id].height = height + interfaceConfig.CANVAS_EXTRA; - }); - } -}); - /** * The audio Levels plugin. */ @@ -248,6 +221,33 @@ const AudioLevels = { // Fill the shape. ASDrawContext.fill(); + }, + + /** + * Indicates that the remote video has been resized. + */ + onRemoteVideoResized (width, height) { + let resized = false; + + $('#remoteVideos>span>canvas').each(function() { + let canvas = $(this).get(0); + if (canvas.width !== width + interfaceConfig.CANVAS_EXTRA) { + canvas.width = width + interfaceConfig.CANVAS_EXTRA; + resized = true; + } + + if (canvas.height !== height + interfaceConfig.CANVAS_EXTRA) { + canvas.height = height + interfaceConfig.CANVAS_EXTRA; + resized = true; + } + }); + + if (resized) { + Object.keys(audioLevelCanvasCache).forEach(function (id) { + audioLevelCanvasCache[id].width = width + interfaceConfig.CANVAS_EXTRA; + audioLevelCanvasCache[id].height = height + interfaceConfig.CANVAS_EXTRA; + }); + } } }; diff --git a/modules/UI/etherpad/Etherpad.js b/modules/UI/etherpad/Etherpad.js index f0bf1cb4f..860e44615 100644 --- a/modules/UI/etherpad/Etherpad.js +++ b/modules/UI/etherpad/Etherpad.js @@ -1,61 +1,16 @@ -/* global $, config, - setLargeVideoVisible, Util */ +/* global $ */ -var VideoLayout = require("../videolayout/VideoLayout"); -var Prezi = require("../prezi/Prezi"); -var UIUtil = require("../util/UIUtil"); +import VideoLayout from "../videolayout/VideoLayout"; +import LargeContainer from '../videolayout/LargeContainer'; +import UIUtil from "../util/UIUtil"; +import SidePanelToggler from "../side_pannels/SidePanelToggler"; -var etherpadName = null; -var etherpadIFrame = null; -var domain = null; -var options = "?showControls=true&showChat=false&showLineNumbers=true" + - "&useMonospaceFont=false"; - - -/** - * Resizes the etherpad. - */ -function resize() { - if ($('#etherpad>iframe').length) { - var remoteVideos = $('#remoteVideos'); - var availableHeight - = window.innerHeight - remoteVideos.outerHeight(); - var availableWidth = UIUtil.getAvailableVideoWidth(); - - $('#etherpad>iframe').width(availableWidth); - $('#etherpad>iframe').height(availableHeight); - } -} - -/** - * Creates the Etherpad button and adds it to the toolbar. - */ -function enableEtherpadButton() { - if (!$('#toolbar_button_etherpad').is(":visible")) - $('#toolbar_button_etherpad').css({display: 'inline-block'}); -} - -/** - * Creates the IFrame for the etherpad. - */ -function createIFrame() { - etherpadIFrame = VideoLayout.createEtherpadIframe( - domain + etherpadName + options, function() { - - document.domain = document.domain; - bubbleIframeMouseMove(etherpadIFrame); - setTimeout(function() { - // the iframes inside of the etherpad are - // not yet loaded when the etherpad iframe is loaded - var outer = etherpadIFrame. - contentDocument.getElementsByName("ace_outer")[0]; - bubbleIframeMouseMove(outer); - var inner = outer. - contentDocument.getElementsByName("ace_inner")[0]; - bubbleIframeMouseMove(inner); - }, 2000); - }); -} +const options = $.param({ + showControns: true, + showChat: false, + showLineNumbers: true, + useMonospaceFont: false +}); function bubbleIframeMouseMove(iframe){ var existingOnMouseMove = iframe.contentWindow.onmousemove; @@ -71,8 +26,8 @@ function bubbleIframeMouseMove(iframe){ e.detail, e.screenX, e.screenY, - e.clientX + boundingClientRect.left, - e.clientY + boundingClientRect.top, + e.clientX + boundingClientRect.left, + e.clientY + boundingClientRect.top, e.ctrlKey, e.altKey, e.shiftKey, @@ -84,48 +39,123 @@ function bubbleIframeMouseMove(iframe){ }; } +const DEFAULT_WIDTH = 640; +const DEFAULT_HEIGHT = 480; -var Etherpad = { - /** - * Initializes the etherpad. - */ - init: function (name) { +const EtherpadContainerType = "etherpad"; - if (config.etherpad_base && !etherpadName && name) { +class Etherpad extends LargeContainer { + constructor (domain, name) { + super(); - domain = config.etherpad_base; + const iframe = document.createElement('iframe'); - etherpadName = name; + iframe.src = domain + name + '?' + options; + iframe.frameBorder = 0; + iframe.scrolling = "no"; + iframe.width = DEFAULT_WIDTH; + iframe.height = DEFAULT_HEIGHT; + iframe.setAttribute('style', 'visibility: hidden;'); - enableEtherpadButton(); + this.container.appendChild(iframe); - /** - * Resizes the etherpad, when the window is resized. - */ - $(window).resize(function () { - resize(); - }); - } - }, + iframe.onload = function() { + document.domain = document.domain; + bubbleIframeMouseMove(iframe); - /** - * Opens/hides the Etherpad. - */ - toggleEtherpad: function (isPresentation) { - if (!etherpadIFrame) - createIFrame(); + setTimeout(function() { + const doc = iframe.contentDocument; + // the iframes inside of the etherpad are + // not yet loaded when the etherpad iframe is loaded + const outer = doc.getElementsByName("ace_outer")[0]; + bubbleIframeMouseMove(outer); - if(VideoLayout.getLargeVideoState() === "etherpad") - { - VideoLayout.setLargeVideoState("video"); - } - else - { - VideoLayout.setLargeVideoState("etherpad"); - } - resize(); + const inner = doc.getElementsByName("ace_inner")[0]; + bubbleIframeMouseMove(inner); + }, 2000); + }; + + this.iframe = iframe; } -}; -module.exports = Etherpad; + get isOpen () { + return !!this.iframe; + } + + get container () { + return document.getElementById('etherpad'); + } + + resize (containerWidth, containerHeight, animate) { + let remoteVideos = $('#remoteVideos'); + + let height = containerHeight - remoteVideos.outerHeight(); + let width = containerWidth; + + $(this.iframe).width(width).height(height); + } + + show () { + const $iframe = $(this.iframe); + const $container = $(this.container); + + return new Promise(resolve => { + $iframe.fadeIn(300, function () { + document.body.style.background = '#eeeeee'; + $iframe.css({visibility: 'visible'}); + $container.css({zIndex: 2}); + resolve(); + }); + }); + } + + hide () { + const $iframe = $(this.iframe); + const $container = $(this.container); + + return new Promise(resolve => { + $iframe.fadeOut(300, function () { + $iframe.css({visibility: 'hidden'}); + $container.css({zIndex: 0}); + resolve(); + }); + }); + } +} + +export default class EtherpadManager { + constructor (domain, name) { + if (!domain || !name) { + throw new Error("missing domain or name"); + } + + this.domain = domain; + this.name = name; + this.etherpad = null; + } + + get isOpen () { + return !!this.etherpad; + } + + openEtherpad () { + this.etherpad = new Etherpad(this.domain, this.name); + VideoLayout.addLargeVideoContainer( + EtherpadContainerType, + this.etherpad + ); + } + + toggleEtherpad () { + if (!this.isOpen) { + this.openEtherpad(); + } + + let isVisible = VideoLayout.isLargeContainerTypeVisible( + EtherpadContainerType + ); + + VideoLayout.showLargeVideoContainer(EtherpadContainerType, !isVisible); + } +} diff --git a/modules/UI/prezi/Prezi.js b/modules/UI/prezi/Prezi.js index 4c2b6c61b..06ca6e24c 100644 --- a/modules/UI/prezi/Prezi.js +++ b/modules/UI/prezi/Prezi.js @@ -1,268 +1,21 @@ /* global $, APP */ /* jshint -W101 */ -import UIUtil from "../util/UIUtil"; + import VideoLayout from "../videolayout/VideoLayout"; +import LargeContainer from '../videolayout/LargeContainer'; +import PreziPlayer from './PreziPlayer'; +import UIUtil from '../util/UIUtil'; +import UIEvents from '../../../service/UI/UIEvents'; +import messageHandler from '../util/MessageHandler'; +import ToolbarToggler from "../toolbars/ToolbarToggler"; +import SidePanelToggler from "../side_pannels/SidePanelToggler"; -var messageHandler = require("../util/MessageHandler"); -var PreziPlayer = require("./PreziPlayer"); +const defaultPreziLink = "http://prezi.com/wz7vhjycl7e6/my-prezi"; +const alphanumRegex = /^[a-z0-9-_\/&\?=;]+$/i; +const aspectRatio = 16.0 / 9.0; -var preziPlayer = null; - - -/** - * Shows/hides a presentation. - */ -function setPresentationVisible(visible) { - - if (visible) { - VideoLayout.setLargeVideoState("prezi"); - } - else { - VideoLayout.setLargeVideoState("video"); - } -} - -var Prezi = { - - - /** - * Reloads the current presentation. - */ - reloadPresentation: function() { - var iframe = document.getElementById(preziPlayer.options.preziId); - iframe.src = iframe.src; - }, - - /** - * Returns true if the presentation is visible, false - - * otherwise. - */ - isPresentationVisible: function () { - return ($('#presentation>iframe') != null - && $('#presentation>iframe').css('opacity') == 1); - }, - - /** - * Opens the Prezi dialog, from which the user could choose a presentation - * to load. - */ - openPreziDialog: function() { - var myprezi = APP.xmpp.getPrezi(); - if (myprezi) { - messageHandler.openTwoButtonDialog("dialog.removePreziTitle", - null, - "dialog.removePreziMsg", - null, - false, - "dialog.Remove", - function(e,v,m,f) { - if(v) { - APP.xmpp.removePreziFromPresence(); - } - } - ); - } - else if (preziPlayer != null) { - messageHandler.openTwoButtonDialog("dialog.sharePreziTitle", - null, "dialog.sharePreziMsg", - null, - false, - "dialog.Ok", - function(e,v,m,f) { - $.prompt.close(); - } - ); - } - else { - var html = APP.translation.generateTranslationHTML( - "dialog.sharePreziTitle"); - var cancelButton = APP.translation.generateTranslationHTML( - "dialog.Cancel"); - var shareButton = APP.translation.generateTranslationHTML( - "dialog.Share"); - var backButton = APP.translation.generateTranslationHTML( - "dialog.Back"); - var buttons = []; - var buttons1 = []; - // Cancel button to both states - buttons.push({title: cancelButton, value: false}); - buttons1.push({title: cancelButton, value: false}); - // Share button - buttons.push({title: shareButton, value: true}); - // Back button - buttons1.push({title: backButton, value: true}); - var linkError = APP.translation.generateTranslationHTML( - "dialog.preziLinkError"); - var defaultUrl = APP.translation.translateString("defaultPreziLink", - {url: "http://prezi.com/wz7vhjycl7e6/my-prezi"}); - var openPreziState = { - state0: { - html: '

' + html + '

' + - '', - persistent: false, - buttons: buttons, - focus: ':input:first', - defaultButton: 0, - submit: function (e, v, m, f) { - e.preventDefault(); - if(v) - { - var preziUrl = f.preziUrl; - - if (preziUrl) - { - var urlValue - = encodeURI(UIUtil.escapeHtml(preziUrl)); - - if (urlValue.indexOf('http://prezi.com/') != 0 - && urlValue.indexOf('https://prezi.com/') != 0) - { - $.prompt.goToState('state1'); - return false; - } - else { - var presIdTmp = urlValue.substring( - urlValue.indexOf("prezi.com/") + 10); - if (!isAlphanumeric(presIdTmp) - || presIdTmp.indexOf('/') < 2) { - $.prompt.goToState('state1'); - return false; - } - else { - APP.xmpp.addToPresence("prezi", urlValue); - $.prompt.close(); - } - } - } - } - else - $.prompt.close(); - } - }, - state1: { - html: '

' + html + '

' + - linkError, - persistent: false, - buttons: buttons1, - focus: ':input:first', - defaultButton: 1, - submit: function (e, v, m, f) { - e.preventDefault(); - if (v === 0) - $.prompt.close(); - else - $.prompt.goToState('state0'); - } - } - }; - messageHandler.openDialogWithStates(openPreziState); - } - } - -}; - -/** - * A new presentation has been added. - * - * @param event the event indicating the add of a presentation - * @param jid the jid from which the presentation was added - * @param presUrl url of the presentation - * @param currentSlide the current slide to which we should move - */ -function presentationAdded(event, jid, presUrl, currentSlide) { - console.log("presentation added", presUrl); - - var presId = getPresentationId(presUrl); - - var elementId = 'participant_' - + Strophe.getResourceFromJid(jid) - + '_' + presId; - - VideoLayout.addPreziContainer(elementId); - - var controlsEnabled = false; - if (jid === APP.xmpp.myJid()) - controlsEnabled = true; - - setPresentationVisible(true); - VideoLayout.setLargeVideoHover( - function (event) { - if (Prezi.isPresentationVisible()) { - var reloadButtonRight = window.innerWidth - - $('#presentation>iframe').offset().left - - $('#presentation>iframe').width(); - - $('#reloadPresentation').css({ right: reloadButtonRight, - display:'inline-block'}); - } - }, - function (event) { - if (!Prezi.isPresentationVisible()) - $('#reloadPresentation').css({display:'none'}); - else { - var e = event.toElement || event.relatedTarget; - - if (e && e.id != 'reloadPresentation' && e.id != 'header') - $('#reloadPresentation').css({display:'none'}); - } - }); - - preziPlayer = new PreziPlayer( - 'presentation', - {preziId: presId, - width: getPresentationWidth(), - height: getPresentationHeihgt(), - controls: controlsEnabled, - debug: true - }); - - $('#presentation>iframe').attr('id', preziPlayer.options.preziId); - - preziPlayer.on(PreziPlayer.EVENT_STATUS, function(event) { - console.log("prezi status", event.value); - if (event.value == PreziPlayer.STATUS_CONTENT_READY) { - if (jid != APP.xmpp.myJid()) - preziPlayer.flyToStep(currentSlide); - } - }); - - preziPlayer.on(PreziPlayer.EVENT_CURRENT_STEP, function(event) { - console.log("event value", event.value); - APP.xmpp.addToPresence("preziSlide", event.value); - }); - - $("#" + elementId).css( 'background-image', - 'url(../images/avatarprezi.png)'); - $("#" + elementId).click( - function () { - setPresentationVisible(true); - } - ); -}; - -/** - * A presentation has been removed. - * - * @param event the event indicating the remove of a presentation - * @param jid the jid for which the presentation was removed - * @param the url of the presentation - */ -function presentationRemoved(event, jid, presUrl) { - console.log('presentation removed', presUrl); - var presId = getPresentationId(presUrl); - setPresentationVisible(false); - $('#participant_' - + Strophe.getResourceFromJid(jid) - + '_' + presId).remove(); - $('#presentation>iframe').remove(); - if (preziPlayer != null) { - preziPlayer.destroy(); - preziPlayer = null; - } -}; +const DEFAULT_WIDTH = 640; +const DEFAULT_HEIGHT = 480; /** * Indicates if the given string is an alphanumeric string. @@ -270,76 +23,355 @@ function presentationRemoved(event, jid, presUrl) { * purpose of checking URIs. */ function isAlphanumeric(unsafeText) { - var regex = /^[a-z0-9-_\/&\?=;]+$/i; - return regex.test(unsafeText); + return alphanumRegex.test(unsafeText); } /** * Returns the presentation id from the given url. */ -function getPresentationId (presUrl) { - var presIdTmp = presUrl.substring(presUrl.indexOf("prezi.com/") + 10); - return presIdTmp.substring(0, presIdTmp.indexOf('/')); +function getPresentationId (url) { + let presId = url.substring(url.indexOf("prezi.com/") + 10); + return presId.substring(0, presId.indexOf('/')); } -/** - * Returns the presentation width. - */ -function getPresentationWidth() { - var availableWidth = UIUtil.getAvailableVideoWidth(); - var availableHeight = getPresentationHeihgt(); - - var aspectRatio = 16.0 / 9.0; - if (availableHeight < availableWidth / aspectRatio) { - availableWidth = Math.floor(availableHeight * aspectRatio); +function isPreziLink(url) { + if (url.indexOf('http://prezi.com/') !== 0 && url.indexOf('https://prezi.com/') !== 0) { + return false; } - return availableWidth; -} -/** - * Returns the presentation height. - */ -function getPresentationHeihgt() { - var remoteVideos = $('#remoteVideos'); - return window.innerHeight - remoteVideos.outerHeight(); -} - -/** - * Resizes the presentation iframe. - */ -function resize() { - if ($('#presentation>iframe')) { - $('#presentation>iframe').width(getPresentationWidth()); - $('#presentation>iframe').height(getPresentationHeihgt()); + let presId = url.substring(url.indexOf("prezi.com/") + 10); + if (!isAlphanumeric(presId) || presId.indexOf('/') < 2) { + return false; } + + return true; } -/** - * Presentation has been removed. - */ -$(document).bind('presentationremoved.muc', presentationRemoved); +function notifyOtherIsSharingPrezi() { + messageHandler.openMessageDialog( + "dialog.sharePreziTitle", + "dialog.sharePreziMsg" + ); +} -/** - * Presentation has been added. - */ -$(document).bind('presentationadded.muc', presentationAdded); +function proposeToClosePrezi() { + return new Promise(function (resolve, reject) { + messageHandler.openTwoButtonDialog( + "dialog.removePreziTitle", + null, + "dialog.removePreziMsg", + null, + false, + "dialog.Remove", + function(e,v,m,f) { + if (v) { + resolve(); + } else { + reject(); + } + } + ); -/* - * Indicates presentation slide change. - */ -$(document).bind('gotoslide.muc', function (event, jid, presUrl, current) { - if (preziPlayer && preziPlayer.getCurrentStep() != current) { - preziPlayer.flyToStep(current); + }); +} - var animationStepsArray = preziPlayer.getAnimationCountOnSteps(); - for (var i = 0; i < parseInt(animationStepsArray[current]); i++) { - preziPlayer.flyToStep(current, i); +function requestPreziLink() { + const title = APP.translation.generateTranslationHTML("dialog.sharePreziTitle"); + const cancelButton = APP.translation.generateTranslationHTML("dialog.Cancel"); + const shareButton = APP.translation.generateTranslationHTML("dialog.Share"); + const backButton = APP.translation.generateTranslationHTML("dialog.Back"); + const linkError = APP.translation.generateTranslationHTML("dialog.preziLinkError"); + const i18nOptions = {url: defaultPreziLink}; + const defaultUrl = APP.translation.translateString( + "defaultPreziLink", i18nOptions + ); + + return new Promise(function (resolve, reject) { + let dialog = messageHandler.openDialogWithStates({ + state0: { + html: ` +

${title}

+ `, + persistent: false, + buttons: [ + {title: cancelButton, value: false}, + {title: shareButton, value: true} + ], + focus: ':input:first', + defaultButton: 1, + submit: function (e, v, m, f) { + e.preventDefault(); + if (!v) { + reject('cancelled'); + dialog.close(); + return; + } + + let preziUrl = f.preziUrl; + if (!preziUrl) { + return; + } + + let urlValue = encodeURI(UIUtil.escapeHtml(preziUrl)); + + if (!isPreziLink(urlValue)) { + dialog.goToState('state1'); + return false; + } + + resolve(urlValue); + dialog.close(); + } + }, + + state1: { + html: `

${title}

${linkError}`, + persistent: false, + buttons: [ + {title: cancelButton, value: false}, + {title: backButton, value: true} + ], + focus: ':input:first', + defaultButton: 1, + submit: function (e, v, m, f) { + e.preventDefault(); + if (v === 0) { + reject(); + dialog.close(); + } else { + dialog.goToState('state0'); + } + } + } + }); + + }); +} + +export const PreziContainerType = "prezi"; + +class PreziContainer extends LargeContainer { + + constructor ({preziId, isMy, slide, onSlideChanged}) { + super(); + this.reloadBtn = $('#reloadPresentation'); + + let preziPlayer = new PreziPlayer( + 'presentation', { + preziId, + width: DEFAULT_WIDTH, + height: DEFAULT_HEIGHT, + controls: isMy, + debug: true + } + ); + this.preziPlayer = preziPlayer; + this.$iframe = $(preziPlayer.iframe); + + this.$iframe.attr('id', preziId); + + preziPlayer.on(PreziPlayer.EVENT_STATUS, function({value}) { + console.log("prezi status", value); + if (value == PreziPlayer.STATUS_CONTENT_READY && !isMy) { + preziPlayer.flyToStep(slide); + } + }); + + preziPlayer.on(PreziPlayer.EVENT_CURRENT_STEP, function({value}) { + console.log("event value", value); + onSlideChanged(value); + }); + } + + goToSlide (slide) { + if (this.preziPlayer.getCurrentStep() === slide) { + return; + } + + this.preziPlayer.flyToStep(slide); + + let animationStepsArray = this.preziPlayer.getAnimationCountOnSteps(); + if (!animationStepsArray) { + return; + } + + for (var i = 0; i < parseInt(animationStepsArray[slide]); i += 1) { + this.preziPlayer.flyToStep(slide, i); } } -}); -$(window).resize(function () { - resize(); -}); + showReloadBtn (show) { + this.reloadBtn.css('display', show ? 'inline-block' : 'none'); + } -module.exports = Prezi; + show () { + return new Promise(resolve => { + this.$iframe.fadeIn(300, () => { + this.$iframe.css({opacity: 1}); + ToolbarToggler.dockToolbar(true); + resolve(); + }); + }); + } + + hide () { + return new Promise(resolve => { + this.$iframe.fadeOut(300, () => { + this.$iframe.css({opacity: 0}); + this.showReloadBtn(false); + ToolbarToggler.dockToolbar(false); + resolve(); + }); + }); + } + + onHoverIn () { + let rightOffset = window.innerWidth - this.$iframe.offset().left - this.$iframe.width(); + + this.showReloadBtn(true); + this.reloadBtn.css('right', rightOffset); + } + + onHoverOut (event) { + let e = event.toElement || event.relatedTarget; + + if (e && e.id != 'reloadPresentation' && e.id != 'header') { + this.showReloadBtn(false); + } + } + + resize (containerWidth, containerHeight) { + let remoteVideos = $('#remoteVideos'); + let height = containerHeight - remoteVideos.outerHeight(); + + let width = containerWidth; + + if (height < width / aspectRatio) { + width = Math.floor(height * aspectRatio); + } + + this.$iframe.width(width).height(height); + } + + close () { + this.showReloadBtn(false); + this.preziPlayer.destroy(); + this.$iframe.remove(); + } +} + +export default class PreziManager { + constructor (emitter) { + this.emitter = emitter; + + this.userId = null; + this.url = null; + this.prezi = null; + + $("#reloadPresentationLink").click(this.reloadPresentation.bind(this)); + } + + get isPresenting () { + return !!this.userId; + } + + get isMyPrezi () { + return this.userId === APP.conference.localId; + } + + isSharing (id) { + return this.userId === id; + } + + handlePreziButtonClicked () { + if (!this.isPresenting) { + requestPreziLink().then( + url => this.emitter.emit(UIEvents.SHARE_PREZI, url, 0), + err => console.error('PREZI CANCELED', err) + ); + return; + } + + if (this.isMyPrezi) { + proposeToClosePrezi().then(() => this.emitter.emit(UIEvents.STOP_SHARING_PREZI)); + } else { + notifyOtherIsSharingPrezi(); + } + } + + reloadPresentation () { + if (!this.prezi) { + return; + } + let iframe = this.prezi.$iframe[0]; + iframe.src = iframe.src; + } + + showPrezi (id, url, slide) { + if (!this.isPresenting) { + this.createPrezi(id, url, slide); + } + + if (this.userId === id && this.url === url) { + this.prezi.goToSlide(slide); + } else { + console.error(this.userId, id); + console.error(this.url, url); + throw new Error("unexpected presentation change"); + } + } + + createPrezi (id, url, slide) { + console.log("presentation added", url); + + this.userId = id; + this.url = url; + + let preziId = getPresentationId(url); + let elementId = `participant_${id}_${preziId}`; + + this.$thumb = $(VideoLayout.addRemoteVideoContainer(elementId)); + VideoLayout.resizeThumbnails(); + this.$thumb.css({ + 'background-image': 'url(../images/avatarprezi.png)' + }).click(() => VideoLayout.showLargeVideoContainer(PreziContainerType, true)); + + this.prezi = new PreziContainer({ + preziId, + isMy: this.isMyPrezi, + slide, + onSlideChanged: newSlide => { + if (this.isMyPrezi) { + this.emitter.emit(UIEvents.SHARE_PREZI, url, newSlide); + } + } + }); + + VideoLayout.addLargeVideoContainer(PreziContainerType, this.prezi); + VideoLayout.showLargeVideoContainer(PreziContainerType, true); + } + + removePrezi (id) { + if (this.userId !== id) { + throw new Error(`cannot close presentation from ${this.userId} instead of ${id}`); + } + + this.$thumb.remove(); + this.$thumb = null; + + // wait until Prezi is hidden, then remove it + VideoLayout.showLargeVideoContainer(PreziContainerType, false).then(() => { + console.log("presentation removed", this.url); + + VideoLayout.removeLargeVideoContainer(PreziContainerType); + + this.userId = null; + this.url = null; + this.prezi.close(); + this.prezi = null; + }); + } +} diff --git a/modules/UI/prezi/PreziPlayer.js b/modules/UI/prezi/PreziPlayer.js index 976f1db5c..b962057ac 100644 --- a/modules/UI/prezi/PreziPlayer.js +++ b/modules/UI/prezi/PreziPlayer.js @@ -1,298 +1,290 @@ -/* global PreziPlayer */ /* jshint -W101 */ -(function() { - "use strict"; - var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; - window.PreziPlayer = (function() { +var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; - PreziPlayer.API_VERSION = 1; - PreziPlayer.CURRENT_STEP = 'currentStep'; - PreziPlayer.CURRENT_ANIMATION_STEP = 'currentAnimationStep'; - PreziPlayer.CURRENT_OBJECT = 'currentObject'; - PreziPlayer.STATUS_LOADING = 'loading'; - PreziPlayer.STATUS_READY = 'ready'; - PreziPlayer.STATUS_CONTENT_READY = 'contentready'; - PreziPlayer.EVENT_CURRENT_STEP = "currentStepChange"; - PreziPlayer.EVENT_CURRENT_ANIMATION_STEP = "currentAnimationStepChange"; - PreziPlayer.EVENT_CURRENT_OBJECT = "currentObjectChange"; - PreziPlayer.EVENT_STATUS = "statusChange"; - PreziPlayer.EVENT_PLAYING = "isAutoPlayingChange"; - PreziPlayer.EVENT_IS_MOVING = "isMovingChange"; - PreziPlayer.domain = "https://prezi.com"; - PreziPlayer.path = "/player/"; - PreziPlayer.players = {}; - PreziPlayer.binded_methods = ['changesHandler']; +PreziPlayer.API_VERSION = 1; +PreziPlayer.CURRENT_STEP = 'currentStep'; +PreziPlayer.CURRENT_ANIMATION_STEP = 'currentAnimationStep'; +PreziPlayer.CURRENT_OBJECT = 'currentObject'; +PreziPlayer.STATUS_LOADING = 'loading'; +PreziPlayer.STATUS_READY = 'ready'; +PreziPlayer.STATUS_CONTENT_READY = 'contentready'; +PreziPlayer.EVENT_CURRENT_STEP = "currentStepChange"; +PreziPlayer.EVENT_CURRENT_ANIMATION_STEP = "currentAnimationStepChange"; +PreziPlayer.EVENT_CURRENT_OBJECT = "currentObjectChange"; +PreziPlayer.EVENT_STATUS = "statusChange"; +PreziPlayer.EVENT_PLAYING = "isAutoPlayingChange"; +PreziPlayer.EVENT_IS_MOVING = "isMovingChange"; +PreziPlayer.domain = "https://prezi.com"; +PreziPlayer.path = "/player/"; +PreziPlayer.players = {}; +PreziPlayer.binded_methods = ['changesHandler']; - PreziPlayer.createMultiplePlayers = function(optionArray){ - for(var i=0; i 0 && - obj.values.animationCountOnSteps && - obj.values.animationCountOnSteps[step] <= animation_step) { - animation_step = obj.values.animationCountOnSteps[step]; - } - // jump to animation steps by calling flyToNextStep() - function doAnimationSteps() { - if (obj.values.isMoving) { - setTimeout(doAnimationSteps, 100); // wait until the flight ends - return; - } - while (animation_step-- > 0) { - obj.flyToNextStep(); // do the animation steps - } - } - setTimeout(doAnimationSteps, 200); // 200ms is the internal "reporting" time - // jump to the step - return this.sendMessage({ - 'action': 'present', - 'data': ['moveToStep', step] - }); - }; - - PreziPlayer.prototype.toObject = /* toObject is DEPRECATED */ - PreziPlayer.prototype.flyToObject = function(objectId) { - return this.sendMessage({ - 'action': 'present', - 'data': ['moveToObject', objectId] - }); - }; - - PreziPlayer.prototype.play = function(defaultDelay) { - return this.sendMessage({ - 'action': 'present', - 'data': ['startAutoPlay', defaultDelay] - }); - }; - - PreziPlayer.prototype.stop = function() { - return this.sendMessage({ - 'action': 'present', - 'data': ['stopAutoPlay'] - }); - }; - - PreziPlayer.prototype.pause = function(defaultDelay) { - return this.sendMessage({ - 'action': 'present', - 'data': ['pauseAutoPlay', defaultDelay] - }); - }; - - PreziPlayer.prototype.getCurrentStep = function() { - return this.values.currentStep; - }; - - PreziPlayer.prototype.getCurrentAnimationStep = function() { - return this.values.currentAnimationStep; - }; - - PreziPlayer.prototype.getCurrentObject = function() { - return this.values.currentObject; - }; - - PreziPlayer.prototype.getStatus = function() { - return this.values.status; - }; - - PreziPlayer.prototype.isPlaying = function() { - return this.values.isAutoPlaying; - }; - - PreziPlayer.prototype.getStepCount = function() { - return this.values.stepCount; - }; - - PreziPlayer.prototype.getAnimationCountOnSteps = function() { - return this.values.animationCountOnSteps; - }; - - PreziPlayer.prototype.getTitle = function() { - return this.values.title; - }; - - PreziPlayer.prototype.setDimensions = function(dims) { - for (var parameter in dims) { - this.iframe[parameter] = dims[parameter]; - } - }; - - PreziPlayer.prototype.getDimensions = function() { - return { - width: parseInt(this.iframe.width, 10), - height: parseInt(this.iframe.height, 10) - }; - }; - - PreziPlayer.prototype.on = function(event, callback) { - this.callbacks.push({ - event: event, - callback: callback - }); - }; - - PreziPlayer.prototype.off = function(event, callback) { - var j, item; - if (event === undefined) { - this.callbacks = []; - } - j = this.callbacks.length; - while (j--) { +PreziPlayer.prototype.changesHandler = function(message) { + var key, value, j, item; + if (this.initPollInterval) { + clearInterval(this.initPollInterval); + this.initPollInterval = false; + } + for (key in message.data) { + if (message.data.hasOwnProperty(key)){ + value = message.data[key]; + this.values[key] = value; + for (j=0; j 0 && + obj.values.animationCountOnSteps && + obj.values.animationCountOnSteps[step] <= animation_step) { + animation_step = obj.values.animationCountOnSteps[step]; + } + // jump to animation steps by calling flyToNextStep() + function doAnimationSteps() { + if (obj.values.isMoving) { + setTimeout(doAnimationSteps, 100); // wait until the flight ends + return; + } + while (animation_step-- > 0) { + obj.flyToNextStep(); // do the animation steps + } + } + setTimeout(doAnimationSteps, 200); // 200ms is the internal "reporting" time + // jump to the step + return this.sendMessage({ + 'action': 'present', + 'data': ['moveToStep', step] + }); +}; + +PreziPlayer.prototype.toObject = /* toObject is DEPRECATED */ +PreziPlayer.prototype.flyToObject = function(objectId) { + return this.sendMessage({ + 'action': 'present', + 'data': ['moveToObject', objectId] + }); +}; + +PreziPlayer.prototype.play = function(defaultDelay) { + return this.sendMessage({ + 'action': 'present', + 'data': ['startAutoPlay', defaultDelay] + }); +}; + +PreziPlayer.prototype.stop = function() { + return this.sendMessage({ + 'action': 'present', + 'data': ['stopAutoPlay'] + }); +}; + +PreziPlayer.prototype.pause = function(defaultDelay) { + return this.sendMessage({ + 'action': 'present', + 'data': ['pauseAutoPlay', defaultDelay] + }); +}; + +PreziPlayer.prototype.getCurrentStep = function() { + return this.values.currentStep; +}; + +PreziPlayer.prototype.getCurrentAnimationStep = function() { + return this.values.currentAnimationStep; +}; + +PreziPlayer.prototype.getCurrentObject = function() { + return this.values.currentObject; +}; + +PreziPlayer.prototype.getStatus = function() { + return this.values.status; +}; + +PreziPlayer.prototype.isPlaying = function() { + return this.values.isAutoPlaying; +}; + +PreziPlayer.prototype.getStepCount = function() { + return this.values.stepCount; +}; + +PreziPlayer.prototype.getAnimationCountOnSteps = function() { + return this.values.animationCountOnSteps; +}; + +PreziPlayer.prototype.getTitle = function() { + return this.values.title; +}; + +PreziPlayer.prototype.setDimensions = function(dims) { + for (var parameter in dims) { + this.iframe[parameter] = dims[parameter]; + } +}; + +PreziPlayer.prototype.getDimensions = function() { + return { + width: parseInt(this.iframe.width, 10), + height: parseInt(this.iframe.height, 10) + }; +}; + +PreziPlayer.prototype.on = function(event, callback) { + this.callbacks.push({ + event: event, + callback: callback + }); +}; + +PreziPlayer.prototype.off = function(event, callback) { + var j, item; + if (event === undefined) { + this.callbacks = []; + } + j = this.callbacks.length; + while (j--) { + item = this.callbacks[j]; + if (item && item.event === event && (callback === undefined || item.callback === callback)){ + this.callbacks.splice(j, 1); + } + } +}; + +if (window.addEventListener) { + window.addEventListener('message', PreziPlayer.messageReceived, false); +} else { + window.attachEvent('onmessage', PreziPlayer.messageReceived); +} + +window.PreziPlayer = PreziPlayer; + +export default PreziPlayer; diff --git a/modules/UI/side_pannels/SidePanelToggler.js b/modules/UI/side_pannels/SidePanelToggler.js index 07a39e561..f3742a2cd 100644 --- a/modules/UI/side_pannels/SidePanelToggler.js +++ b/modules/UI/side_pannels/SidePanelToggler.js @@ -6,7 +6,6 @@ import SettingsMenu from "./settings/SettingsMenu"; import VideoLayout from "../videolayout/VideoLayout"; import ToolbarToggler from "../toolbars/ToolbarToggler"; import UIUtil from "../util/UIUtil"; -import LargeVideo from "../videolayout/LargeVideo"; const buttons = { '#chatspace': '#chatBottomButton', @@ -47,7 +46,7 @@ function toggle (object, selector, onOpenComplete, onOpen, onClose) { } else { // Undock the toolbar when the chat is shown and if we're in a // video mode. - if (LargeVideo.isLargeVideoVisible()) { + if (VideoLayout.isLargeVideoVisible()) { ToolbarToggler.dockToolbar(false); } @@ -62,7 +61,7 @@ function toggle (object, selector, onOpenComplete, onOpen, onClose) { } $("#toast-container").animate({ - right: (PanelToggler.getPanelSize()[0] + 5) + right: (UIUtil.getSidePanelSize()[0] + 5) }, { queue: false, duration: 500 @@ -116,7 +115,7 @@ var PanelToggler = { }, resizeChat () { - let [width, height] = this.getPanelSize(); + let [width, height] = UIUtil.getSidePanelSize(); Chat.resizeChat(width, height); }, @@ -156,21 +155,6 @@ var PanelToggler = { null); }, - /** - * Returns the size of the side panel. - */ - getPanelSize () { - var availableHeight = window.innerHeight; - var availableWidth = window.innerWidth; - - var panelWidth = 200; - if (availableWidth * 0.2 < 200) { - panelWidth = availableWidth * 0.2; - } - - return [panelWidth, availableHeight]; - }, - isVisible () { return (Chat.isVisible() || ContactList.isVisible() || diff --git a/modules/UI/toolbars/BottomToolbar.js b/modules/UI/toolbars/BottomToolbar.js index a937c3fbf..6d9ca1554 100644 --- a/modules/UI/toolbars/BottomToolbar.js +++ b/modules/UI/toolbars/BottomToolbar.js @@ -9,13 +9,6 @@ const defaultBottomToolbarButtons = { 'filmstrip': '#bottom_toolbar_film_strip' }; -$(document).bind("remotevideo.resized", function (event, width, height) { - let toolbar = $('#bottomToolbar'); - let bottom = (height - toolbar.outerHeight())/2 + 18; - - toolbar.css({bottom}); -}); - const BottomToolbar = { init (emitter) { UIUtil.hideDisabledButtons(defaultBottomToolbarButtons); @@ -42,6 +35,13 @@ const BottomToolbar = { toggleFilmStrip () { $("#remoteVideos").toggleClass("hidden"); + }, + + onRemoteVideoResized (width, height) { + let toolbar = $('#bottomToolbar'); + let bottom = (height - toolbar.outerHeight())/2 + 18; + + toolbar.css({bottom}); } }; diff --git a/modules/UI/toolbars/Toolbar.js b/modules/UI/toolbars/Toolbar.js index ee43423ad..730195ce1 100644 --- a/modules/UI/toolbars/Toolbar.js +++ b/modules/UI/toolbars/Toolbar.js @@ -182,17 +182,17 @@ const buttonHandlers = { } }; const defaultToolbarButtons = { - 'microphone': '#toolbar_button_mute', - 'camera': '#toolbar_button_camera', - 'desktop': '#toolbar_button_desktopsharing', - 'security': '#toolbar_button_security', - 'invite': '#toolbar_button_link', - 'chat': '#toolbar_button_chat', - 'prezi': '#toolbar_button_prezi', - 'etherpad': '#toolbar_button_etherpad', - 'fullscreen': '#toolbar_button_fullScreen', - 'settings': '#toolbar_button_settings', - 'hangup': '#toolbar_button_hangup' + 'microphone': '#toolbar_button_mute', + 'camera': '#toolbar_button_camera', + 'desktop': '#toolbar_button_desktopsharing', + 'security': '#toolbar_button_security', + 'invite': '#toolbar_button_link', + 'chat': '#toolbar_button_chat', + 'prezi': '#toolbar_button_prezi', + 'etherpad': '#toolbar_button_etherpad', + 'fullscreen': '#toolbar_button_fullScreen', + 'settings': '#toolbar_button_settings', + 'hangup': '#toolbar_button_hangup' }; function dialpadButtonClicked() { @@ -208,7 +208,7 @@ function showSipNumberInput () { messageHandler.openTwoButtonDialog( null, null, null, `

${sipMsg}

- `, + `, false, "dialog.Dial", function (e, v, m, f) { if (v && f.sipNumber) { @@ -250,7 +250,7 @@ const Toolbar = { * Disables and enables some of the buttons. */ setupButtonsFromConfig () { - if (UIUtil.isButtonEnabled('prezi')) { + if (!UIUtil.isButtonEnabled('prezi')) { $("#toolbar_button_prezi").css({display: "none"}); } }, @@ -283,6 +283,12 @@ const Toolbar = { } }, + showEtherpadButton () { + if (!$('#toolbar_button_etherpad').is(":visible")) { + $('#toolbar_button_etherpad').css({display: 'inline-block'}); + } + }, + // Shows or hides the 'recording' button. showRecordingButton (show) { if (UIUtil.isButtonEnabled('recording') && show) { @@ -304,7 +310,7 @@ const Toolbar = { // we have params to start automatically sharing checkAutoEnableDesktopSharing () { if (UIUtil.isButtonEnabled('desktop') - && config.autoEnableDesktopSharing) { + && config.autoEnableDesktopSharing) { APP.desktopsharing.toggleScreenSharing(); } }, diff --git a/modules/UI/util/UIUtil.js b/modules/UI/util/UIUtil.js index d1dc7a21c..2a1f084a0 100644 --- a/modules/UI/util/UIUtil.js +++ b/modules/UI/util/UIUtil.js @@ -1,19 +1,34 @@ /* global $, config, interfaceConfig */ -import PanelToggler from "../side_pannels/SidePanelToggler"; - /** * Created by hristo on 12/22/14. */ var UIUtil = { + + /** + * Returns the size of the side panel. + */ + getSidePanelSize () { + var availableHeight = window.innerHeight; + var availableWidth = window.innerWidth; + + var panelWidth = 200; + if (availableWidth * 0.2 < 200) { + panelWidth = availableWidth * 0.2; + } + + return [panelWidth, availableHeight]; + }, + /** * Returns the available video width. */ - getAvailableVideoWidth: function (isVisible) { - if(typeof isVisible === "undefined" || isVisible === null) - isVisible = PanelToggler.isVisible(); - var rightPanelWidth - = isVisible ? PanelToggler.getPanelSize()[0] : 0; + getAvailableVideoWidth: function (isSidePanelVisible) { + let rightPanelWidth = 0; + + if (isSidePanelVisible) { + rightPanelWidth = UIUtil.getSidePanelSize()[0]; + } return window.innerWidth - rightPanelWidth; }, @@ -118,6 +133,12 @@ import PanelToggler from "../side_pannels/SidePanelToggler"; redirect (url) { window.location.href = url; + }, + + isFullScreen () { + return document.fullScreen + || document.mozFullScreen + || document.webkitIsFullScreen; } }; diff --git a/modules/UI/videolayout/LargeContainer.js b/modules/UI/videolayout/LargeContainer.js new file mode 100644 index 000000000..b74ac50a4 --- /dev/null +++ b/modules/UI/videolayout/LargeContainer.js @@ -0,0 +1,24 @@ + +export default class LargeContainer { + + /** + * @returns Promise + */ + show () { + } + + /** + * @returns Promise + */ + hide () { + } + + resize (containerWidth, containerHeight, animate) { + } + + onHoverIn (e) { + } + + onHoverOut (e) { + } +} diff --git a/modules/UI/videolayout/LargeVideo.js b/modules/UI/videolayout/LargeVideo.js index 921398770..dd358411c 100644 --- a/modules/UI/videolayout/LargeVideo.js +++ b/modules/UI/videolayout/LargeVideo.js @@ -1,98 +1,20 @@ /* global $, APP, interfaceConfig */ /* jshint -W101 */ -import Avatar from "../avatar/Avatar"; -import ToolbarToggler from "../toolbars/ToolbarToggler"; + import UIUtil from "../util/UIUtil"; import UIEvents from "../../../service/UI/UIEvents"; +import LargeContainer from './LargeContainer'; -var RTCBrowserType = require("../../RTC/RTCBrowserType"); +const RTCBrowserType = require("../../RTC/RTCBrowserType"); -// FIXME: With Temasys we have to re-select everytime -//var video = $('#largeVideo'); +const avatarSize = interfaceConfig.ACTIVE_SPEAKER_AVATAR_SIZE; -var currentVideoWidth = null; -var currentVideoHeight = null; -// By default we use camera -var getVideoSize = getCameraVideoSize; -var getVideoPosition = getCameraVideoPosition; -/** - * The small video instance that is displayed in the large video - * @type {SmallVideo} - */ -var currentSmallVideo = null; -/** - * Indicates whether the large video is enabled. - * @type {boolean} - */ -var isEnabled = true; -/** - * Current large video state. - * Possible values - video, prezi or etherpad. - * @type {string} - */ -var state = "video"; - -/** - * Returns the html element associated with the passed state of large video - * @param state the state. - * @returns {JQuery|*|jQuery|HTMLElement} the container. - */ -function getContainerByState(state) { - var selector = null; - switch (state) { - case "video": - selector = "#largeVideoWrapper"; - break; - case "etherpad": - selector = "#etherpad>iframe"; - break; - case "prezi": - selector = "#presentation>iframe"; - break; - default: - return null; - } - return $(selector); -} - -/** - * Sets the size and position of the given video element. - * - * @param video the video element to position - * @param width the desired video width - * @param height the desired video height - * @param horizontalIndent the left and right indent - * @param verticalIndent the top and bottom indent - */ -function positionVideo(video, - width, - height, - horizontalIndent, - verticalIndent, - animate) { - if (animate) { - video.animate({ - width: width, - height: height, - top: verticalIndent, - bottom: verticalIndent, - left: horizontalIndent, - right: horizontalIndent - }, { - queue: false, - duration: 500 - }); +function getStreamId(stream) { + if (stream.isLocal()) { + return APP.conference.localId; } else { - video.width(width); - video.height(height); - video.css({ - top: verticalIndent, - bottom: verticalIndent, - left: horizontalIndent, - right: horizontalIndent - }); + return stream.getParticipantId(); } - } /** @@ -106,80 +28,28 @@ function getDesktopVideoSize(videoWidth, videoHeight, videoSpaceWidth, videoSpaceHeight) { - if (!videoWidth) - videoWidth = currentVideoWidth; - if (!videoHeight) - videoHeight = currentVideoHeight; - var aspectRatio = videoWidth / videoHeight; + let aspectRatio = videoWidth / videoHeight; - var availableWidth = Math.max(videoWidth, videoSpaceWidth); - var availableHeight = Math.max(videoHeight, videoSpaceHeight); + let availableWidth = Math.max(videoWidth, videoSpaceWidth); + let availableHeight = Math.max(videoHeight, videoSpaceHeight); - var filmstrip = $("#remoteVideos"); + let filmstrip = $("#remoteVideos"); if (!filmstrip.hasClass("hidden")) videoSpaceHeight -= filmstrip.outerHeight(); - if (availableWidth / aspectRatio >= videoSpaceHeight) - { + if (availableWidth / aspectRatio >= videoSpaceHeight) { availableHeight = videoSpaceHeight; availableWidth = availableHeight * aspectRatio; } - if (availableHeight * aspectRatio >= videoSpaceWidth) - { + if (availableHeight * aspectRatio >= videoSpaceWidth) { availableWidth = videoSpaceWidth; availableHeight = availableWidth / aspectRatio; } - return [availableWidth, availableHeight]; -} - - -/** - * Returns an array of the video horizontal and vertical indents, - * so that if fits its parent. - * - * @return an array with 2 elements, the horizontal indent and the vertical - * indent - */ -function getCameraVideoPosition(videoWidth, - videoHeight, - videoSpaceWidth, - videoSpaceHeight) { - // Parent height isn't completely calculated when we position the video in - // full screen mode and this is why we use the screen height in this case. - // Need to think it further at some point and implement it properly. - var isFullScreen = document.fullScreen || - document.mozFullScreen || - document.webkitIsFullScreen; - if (isFullScreen) - videoSpaceHeight = window.innerHeight; - - var horizontalIndent = (videoSpaceWidth - videoWidth) / 2; - var verticalIndent = (videoSpaceHeight - videoHeight) / 2; - - return [horizontalIndent, verticalIndent]; -} - -/** - * Returns an array of the video horizontal and vertical indents. - * Centers horizontally and top aligns vertically. - * - * @return an array with 2 elements, the horizontal indent and the vertical - * indent - */ -function getDesktopVideoPosition(videoWidth, - videoHeight, - videoSpaceWidth, - videoSpaceHeight) { - - var horizontalIndent = (videoSpaceWidth - videoWidth) / 2; - - var verticalIndent = 0;// Top aligned - - return [horizontalIndent, verticalIndent]; + return { availableWidth, availableHeight }; } @@ -199,15 +69,10 @@ function getCameraVideoSize(videoWidth, videoSpaceWidth, videoSpaceHeight) { - if (!videoWidth) - videoWidth = currentVideoWidth; - if (!videoHeight) - videoHeight = currentVideoHeight; + let aspectRatio = videoWidth / videoHeight; - var aspectRatio = videoWidth / videoHeight; - - var availableWidth = videoWidth; - var availableHeight = videoHeight; + let availableWidth = videoWidth; + let availableHeight = videoHeight; if (interfaceConfig.VIDEO_LAYOUT_FIT == 'height') { availableHeight = videoSpaceHeight; @@ -233,487 +98,341 @@ function getCameraVideoSize(videoWidth, } - return [availableWidth, availableHeight]; + return { availableWidth, availableHeight }; } /** - * Updates the src of the active speaker avatar + * Returns an array of the video horizontal and vertical indents, + * so that if fits its parent. + * + * @return an array with 2 elements, the horizontal indent and the vertical + * indent */ -function updateActiveSpeakerAvatarSrc() { - let avatar = $("#activeSpeakerAvatar"); - let id = currentSmallVideo.id; - let url = Avatar.getActiveSpeakerUrl(id); - if (id && avatar.attr('src') !== url) { - avatar.attr('src', url); - currentSmallVideo.showAvatar(); +function getCameraVideoPosition(videoWidth, + videoHeight, + videoSpaceWidth, + videoSpaceHeight) { + // Parent height isn't completely calculated when we position the video in + // full screen mode and this is why we use the screen height in this case. + // Need to think it further at some point and implement it properly. + if (UIUtil.isFullScreen()) { + videoSpaceHeight = window.innerHeight; } + + let horizontalIndent = (videoSpaceWidth - videoWidth) / 2; + let verticalIndent = (videoSpaceHeight - videoHeight) / 2; + + return { horizontalIndent, verticalIndent }; } /** - * Change the video source of the large video. - * @param isVisible + * Returns an array of the video horizontal and vertical indents. + * Centers horizontally and top aligns vertically. + * + * @return an array with 2 elements, the horizontal indent and the vertical + * indent */ -function changeVideo(isVisible) { +function getDesktopVideoPosition(videoWidth, + videoHeight, + videoSpaceWidth, + videoSpaceHeight) { - if (!currentSmallVideo) { - console.error("Unable to change large video - no 'currentSmallVideo'"); - return; - } + let horizontalIndent = (videoSpaceWidth - videoWidth) / 2; - updateActiveSpeakerAvatarSrc(); - let largeVideoElement = $('#largeVideo'); + let verticalIndent = 0;// Top aligned - currentSmallVideo.stream.attach(largeVideoElement); - - let flipX = currentSmallVideo.flipX; - - largeVideoElement.css({ - transform: flipX ? "scaleX(-1)" : "none" - }); - - LargeVideo.updateVideoSizeAndPosition(currentSmallVideo.getVideoType()); - - // Only if the large video is currently visible. - if (isVisible) { - LargeVideo.VideoLayout.largeVideoUpdated(currentSmallVideo); - - $('#largeVideoWrapper').fadeTo(300, 1); - } + return { horizontalIndent, verticalIndent }; } -/** - * Creates the html elements for the large video. - */ -function createLargeVideoHTML() -{ - var html = '
'; - html += '
' + - '
' + - '
' + - '
' + - '' + - ' jitsi.org' + - ''+ - '
' + - '' + - '' + - '
' + - '
' + - '' + - '
' + - ''; - html += '
'; - $(html).prependTo("#videospace"); +export const VideoContainerType = "video"; - if (interfaceConfig.SHOW_JITSI_WATERMARK) { - var leftWatermarkDiv - = $("#largeVideoContainer div[class='watermark leftwatermark']"); - - leftWatermarkDiv.css({display: 'block'}); - leftWatermarkDiv.parent().get(0).href - = interfaceConfig.JITSI_WATERMARK_LINK; +class VideoContainer extends LargeContainer { + // FIXME: With Temasys we have to re-select everytime + get $video () { + return $('#largeVideo'); } - if (interfaceConfig.SHOW_BRAND_WATERMARK) { - var rightWatermarkDiv - = $("#largeVideoContainer div[class='watermark rightwatermark']"); - - rightWatermarkDiv.css({display: 'block'}); - rightWatermarkDiv.parent().get(0).href - = interfaceConfig.BRAND_WATERMARK_LINK; - rightWatermarkDiv.get(0).style.backgroundImage - = "url(images/rightwatermark.png)"; + get id () { + if (this.stream) { + return getStreamId(this.stream); + } } - if (interfaceConfig.SHOW_POWERED_BY) { - $("#largeVideoContainer>a[class='poweredby']").css({display: 'block'}); + constructor (onPlay) { + super(); + this.stream = null; + + this.$avatar = $('#activeSpeaker'); + this.$wrapper = $('#largeVideoWrapper'); + + if (!RTCBrowserType.isIExplorer()) { + this.$video.volume = 0; + } + + this.$video.on('play', onPlay); } - if (!RTCBrowserType.isIExplorer()) { - $('#largeVideo').volume = 0; - } -} - -var LargeVideo = { - - init: function (VideoLayout, emitter) { - if(!isEnabled) - return; - createLargeVideoHTML(); - - this.VideoLayout = VideoLayout; - this.eventEmitter = emitter; - this.eventEmitter.emit(UIEvents.LARGEVIDEO_INIT); - var self = this; - // Listen for large video size updates - var largeVideo = $('#largeVideo')[0]; - var onplaying = function (arg1, arg2, arg3) { - // re-select - if (RTCBrowserType.isTemasysPluginUsed()) - largeVideo = $('#largeVideo')[0]; - currentVideoWidth = largeVideo.videoWidth; - currentVideoHeight = largeVideo.videoHeight; - self.position(currentVideoWidth, currentVideoHeight); + getStreamSize () { + let video = this.$video[0]; + return { + width: video.videoWidth, + height: video.videoHeight }; - largeVideo.onplaying = onplaying; - }, - /** - * Indicates if the large video is currently visible. - * - * @return true if visible, false - otherwise - */ - isLargeVideoVisible: function() { - return $('#largeVideoWrapper').is(':visible'); - }, - /** - * Returns true if the user is currently displayed on large video. - */ - isCurrentlyOnLarge: function (id) { - return id && id === this.getId(); - }, - /** - * Updates the large video with the given new video source. - */ - updateLargeVideo: function (id, forceUpdate) { - if(!isEnabled) { - return; - } - let newSmallVideo = this.VideoLayout.getSmallVideo(id); - console.info(`hover in ${id} , video: `, newSmallVideo); + } - if (!newSmallVideo) { - console.error("Small video not found for: " + id); - return; - } - - if (!LargeVideo.isCurrentlyOnLarge(id) || forceUpdate) { - $('#activeSpeaker').css('visibility', 'hidden'); - - let oldId = this.getId(); - - currentSmallVideo = newSmallVideo; - - if (oldId !== id) { - // we want the notification to trigger even if id is undefined, - // or null. - this.eventEmitter.emit(UIEvents.SELECTED_ENDPOINT, id); - } - // We are doing fadeOut/fadeIn animations on parent div which wraps - // largeVideo, because when Temasys plugin is in use it replaces - //