From 8b528b582f82a4e4c6840f622d8aa844fcea5df1 Mon Sep 17 00:00:00 2001 From: tsareg Date: Fri, 17 Jun 2016 15:31:25 +0300 Subject: [PATCH 01/10] Fixing various edge-cases when two gUM error dialogs might be shown and other possible bugs --- modules/UI/UI.js | 8 +++++++- modules/devices/mediaDeviceHelper.js | 10 ++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/modules/UI/UI.js b/modules/UI/UI.js index 300e00a7e..c3d6f97a7 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -39,6 +39,8 @@ let sharedVideoManager; let followMeHandler; +let deviceErrorDialog; + const TrackErrors = JitsiMeetJS.errors.track; const JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP = { @@ -1267,7 +1269,11 @@ UI.showDeviceErrorDialog = function (micError, cameraError) { message = `${message}${doNotShowWarningAgainSection}`; - messageHandler.openDialog( + // To make sure we don't have multiple error dialogs open at the same time, + // we will just close the previous one if we are going to show a new one. + deviceErrorDialog && deviceErrorDialog.close(); + + deviceErrorDialog = messageHandler.openDialog( titleMsg, message, false, diff --git a/modules/devices/mediaDeviceHelper.js b/modules/devices/mediaDeviceHelper.js index 8b68ad838..8043c9354 100644 --- a/modules/devices/mediaDeviceHelper.js +++ b/modules/devices/mediaDeviceHelper.js @@ -194,15 +194,17 @@ export default { return createLocalTracks( ['audio', 'video'], cameraDeviceId, micDeviceId) // If we fail to do this, try to create them separately. - .catch(() => Promise.all( - [createAudioTrack(false), createVideoTrack(false)])) - .then((audioTracks, videoTracks) => { + .catch(() => Promise.all([ + createAudioTrack(false).then(([stream]) => stream), + createVideoTrack(false).then(([stream]) => stream) + ])) + .then(tracks => { if (audioTrackError || videoTrackError) { APP.UI.showDeviceErrorDialog( audioTrackError, videoTrackError); } - return (audioTracks || []).concat(videoTracks || []); + return tracks.filter(t => typeof t !== 'undefined'); }); } else if (videoRequested && !audioRequested) { return createVideoTrack(); From 02f176c75ae82e6432386ccea05379b46aed4452 Mon Sep 17 00:00:00 2001 From: hristoterezov Date: Fri, 17 Jun 2016 15:35:40 -0500 Subject: [PATCH 02/10] Changes the implementation of the iframe API to use postis --- .gitignore | 1 + Makefile | 13 +- app.js | 5 +- conference.js | 13 +- doc/api.md | 37 ++- doc/debian/jitsi-meet/jitsi-meet.example | 5 + external_api.js | 378 ----------------------- modules/API/API.js | 275 +++++++---------- modules/API/external/external_api.js | 359 +++++++++++++++++++++ modules/TokenData/TokenData.js | 3 +- 10 files changed, 517 insertions(+), 572 deletions(-) delete mode 100644 external_api.js create mode 100644 modules/API/external/external_api.js diff --git a/.gitignore b/.gitignore index 69e79e789..31625a013 100644 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,6 @@ deploy-local.sh libs/app.bundle.* libs/lib-jitsi-meet* libs/external_connect.js +libs/external_api.* all.css .remote-sync.json diff --git a/Makefile b/Makefile index 51dc88acf..f0cb09367 100644 --- a/Makefile +++ b/Makefile @@ -8,8 +8,9 @@ DEPLOY_DIR = libs BROWSERIFY_FLAGS = -d OUTPUT_DIR = . LIBJITSIMEET_DIR = node_modules/lib-jitsi-meet/ +IFRAME_API_DIR = ./modules/API/external -all: update-deps compile uglify deploy clean +all: update-deps compile compile-iframe-api uglify uglify-iframe-api deploy clean update-deps: $(NPM) install @@ -17,8 +18,11 @@ update-deps: compile: $(BROWSERIFY) $(BROWSERIFY_FLAGS) -e app.js -s APP | $(EXORCIST) $(OUTPUT_DIR)/app.bundle.js.map > $(OUTPUT_DIR)/app.bundle.js +compile-iframe-api: + $(BROWSERIFY) $(BROWSERIFY_FLAGS) -e $(IFRAME_API_DIR)/external_api.js -s JitsiMeetExternalAPI | $(EXORCIST) $(OUTPUT_DIR)/external_api.js.map > $(OUTPUT_DIR)/external_api.js + clean: - rm -f $(OUTPUT_DIR)/app.bundle.* + rm -f $(OUTPUT_DIR)/app.bundle.* $(OUTPUT_DIR)/external_api.* deploy: deploy-init deploy-appbundle deploy-lib-jitsi-meet deploy-css deploy-local @@ -28,6 +32,8 @@ deploy-init: deploy-appbundle: cp $(OUTPUT_DIR)/app.bundle.min.js $(OUTPUT_DIR)/app.bundle.min.map \ $(OUTPUT_DIR)/app.bundle.js $(OUTPUT_DIR)/app.bundle.js.map \ + $(OUTPUT_DIR)/external_api.js.map $(OUTPUT_DIR)/external_api.js \ + $(OUTPUT_DIR)/external_api.min.map $(OUTPUT_DIR)/external_api.min.js \ $(DEPLOY_DIR) deploy-lib-jitsi-meet: @@ -46,6 +52,9 @@ deploy-local: uglify: $(UGLIFYJS) -p relative $(OUTPUT_DIR)/app.bundle.js -o $(OUTPUT_DIR)/app.bundle.min.js --source-map $(OUTPUT_DIR)/app.bundle.min.map --in-source-map $(OUTPUT_DIR)/app.bundle.js.map +uglify-iframe-api: + $(UGLIFYJS) -p relative $(OUTPUT_DIR)/external_api.js -o $(OUTPUT_DIR)/external_api.min.js --source-map $(OUTPUT_DIR)/external_api.min.map --in-source-map $(OUTPUT_DIR)/external_api.js.map + source-package: mkdir -p source_package/jitsi-meet/css && \ diff --git a/app.js b/app.js index 2b592ff25..155d941c7 100644 --- a/app.js +++ b/app.js @@ -116,10 +116,7 @@ function init() { APP.keyboardshortcut.init(); }).catch(function (err) { APP.UI.hideRingOverLay(); - APP.API.sendPostisMessage({ - method: 'video-conference-left', - params: {roomName: APP.conference.roomName} - }); + APP.API.notifyConferenceLeft(APP.conference.roomName); console.error(err); }); } diff --git a/conference.js b/conference.js index eb4bbe04b..a610defe7 100644 --- a/conference.js +++ b/conference.js @@ -155,10 +155,7 @@ function maybeRedirectToWelcomePage() { function disconnectAndShowFeedback(requestFeedback) { APP.UI.hideRingOverLay(); connection.disconnect(); - APP.API.sendPostisMessage({ - method: 'video-conference-left', - params: {roomName: APP.conference.roomName} - }); + APP.API.notifyConferenceLeft(APP.conference.roomName); if (requestFeedback) { return APP.UI.requestFeedback(); } else { @@ -465,9 +462,6 @@ export default { this._createRoom(tracks); this.isDesktopSharingEnabled = JitsiMeetJS.isDesktopSharingEnabled(); - if(this.isDesktopSharingEnabled) - APP.API.addPostisMessageListener('toggle-share-screen', - () => this.toggleScreenSharing()); // if user didn't give access to mic or camera or doesn't have // them at all, we disable corresponding toolbar buttons @@ -908,10 +902,7 @@ export default { // add local streams when joined to the conference room.on(ConferenceEvents.CONFERENCE_JOINED, () => { APP.UI.mucJoined(); - APP.API.sendPostisMessage({ - method: 'video-conference-joined', - params: {roomName: APP.conference.roomName} - }); + APP.API.notifyConferenceJoined(APP.conference.roomName); }); room.on( diff --git a/doc/api.md b/doc/api.md index 10d0ee61d..91aba6749 100644 --- a/doc/api.md +++ b/doc/api.md @@ -20,13 +20,13 @@ The next step for embedding Jitsi Meet is to create the Jitsi Meet API object var height = 700; var api = new JitsiMeetExternalAPI(domain, room, width, height); -``` +``` You can paste that lines in your html code where you want to be placed the Jitsi Meet conference or you can specify the parent HTML element for the Jitsi Meet conference in the JitsiMeetExternalAPI constructor. ```javascript var api = new JitsiMeetExternalAPI(domain, room, width, height, htmlElement); -``` +``` If you don't specify room the user will enter in new conference with random room name. You can overwrite options set in config.js and interface_config.js. For example, to enable the film-strip-only interface mode and disable simulcast, you can use: @@ -34,24 +34,24 @@ You can overwrite options set in config.js and interface_config.js. For example, var configOverwrite = {enableSimulcast: false}; var interfaceConfigOverwrite = {filmStripOnly: true}; var api = new JitsiMeetExternalAPI(domain, room, width, height, htmlElement, true, configOverwrite, interfaceConfigOverwrite); -``` +``` Controlling embedded Jitsi Meet Conference ========= You can control the embedded Jitsi Meet conference using the JitsiMeetExternalAPI object. -You can send command to Jitsi Meet conference using ```executeCommand```. +You can send command to Jitsi Meet conference using ```executeCommand```. ``` api.executeCommand(command, arguments) ``` The ```command``` parameter is String object with the name of the command. -The ```arguments``` parameter is array with the arguments required by the command. +The ```arguments``` parameter is array with the arguments required by the command. If no arguments are required by the command this parameter can be omitted or you can pass empty array. Currently we support the following commands: -* **displayName** - sets the display name of the local participant. This command requires one argument - +* **displayName** - sets the display name of the local participant. This command requires one argument - the new display name to be set ``` api.executeCommand('displayName', ['New Nickname']); @@ -77,7 +77,12 @@ api.executeCommand('toggleChat', []) api.executeCommand('toggleContactList', []) ``` -You can also execute multiple commands using the method ```executeCommands```. +* **toggleShareScreen** - starts / stops the screen sharing. No arguments are required. +``` +api.executeCommand('toggleShareScreen', []) +``` + +You can also execute multiple commands using the method ```executeCommands```. ``` api.executeCommands(commands) ``` @@ -136,9 +141,23 @@ The listener will receive object with the following structure: jid: jid //the jid of the participant } ``` +* **video-conference-joined** - event notifications fired when the local user has joined the video conference. +The listener will receive object with the following structure: +``` +{ +roomName: room //the room name of the conference +} +``` +* **video-conference-left** - event notifications fired when the local user has left the video conference. +The listener will receive object with the following structure: +``` +{ +roomName: room //the room name of the conference +} +``` You can also add multiple event listeners by using ```addEventListeners```. -This method requires one argument of type Object. The object argument must +This method requires one argument of type Object. The object argument must have keys with the names of the events and values the listeners of the events. ``` @@ -173,4 +192,4 @@ You can remove the embedded Jitsi Meet Conference with the following code: api.dispose() ``` -It is a good practice to remove the conference before the page is unloaded. +It is a good practice to remove the conference before the page is unloaded. diff --git a/doc/debian/jitsi-meet/jitsi-meet.example b/doc/debian/jitsi-meet/jitsi-meet.example index 2d1c950bd..1f92fc06c 100644 --- a/doc/debian/jitsi-meet/jitsi-meet.example +++ b/doc/debian/jitsi-meet/jitsi-meet.example @@ -33,6 +33,11 @@ server { ssi on; } + # Backward compatibility + location ~ /external_api.* { + root /usr/share/jitsi-meet/libs; + } + # BOSH location /http-bind { proxy_pass http://localhost:5280/http-bind; diff --git a/external_api.js b/external_api.js deleted file mode 100644 index 2a858dbd8..000000000 --- a/external_api.js +++ /dev/null @@ -1,378 +0,0 @@ -/** - * Implements API class that embeds Jitsi Meet in external applications. - */ -var JitsiMeetExternalAPI = (function() -{ - /** - * The minimum width for the Jitsi Meet frame - * @type {number} - */ - var MIN_WIDTH = 790; - - /** - * The minimum height for the Jitsi Meet frame - * @type {number} - */ - var MIN_HEIGHT = 300; - - /** - * Constructs new API instance. Creates iframe element that loads - * Jitsi Meet. - * @param domain the domain name of the server that hosts the conference - * @param room_name the name of the room to join - * @param width width of the iframe - * @param height height of the iframe - * @param parent_node the node that will contain the iframe - * @param filmStripOnly if the value is true only the small videos will be - * visible. - * @param noSsl if the value is true https won't be used - * @constructor - */ - function JitsiMeetExternalAPI(domain, room_name, width, height, parentNode, - configOverwrite, interfaceConfigOverwrite, noSsl) { - if (!width || width < MIN_WIDTH) - width = MIN_WIDTH; - if (!height || height < MIN_HEIGHT) - height = MIN_HEIGHT; - - this.parentNode = null; - if (parentNode) { - this.parentNode = parentNode; - } else { - var scriptTag = document.scripts[document.scripts.length - 1]; - this.parentNode = scriptTag.parentNode; - } - - this.iframeHolder = - this.parentNode.appendChild(document.createElement("div")); - this.iframeHolder.id = "jitsiConference" + JitsiMeetExternalAPI.id; - if(width) - this.iframeHolder.style.width = width + "px"; - if(height) - this.iframeHolder.style.height = height + "px"; - this.frameName = "jitsiConferenceFrame" + JitsiMeetExternalAPI.id; - this.url = (noSsl) ? "http" : "https" +"://" + domain + "/"; - if(room_name) - this.url += room_name; - this.url += "#external=true"; - - var key; - if (configOverwrite) { - for (key in configOverwrite) { - if (!configOverwrite.hasOwnProperty(key) || - typeof key !== 'string') - continue; - this.url += "&config." + key + "=" + configOverwrite[key]; - } - } - - if (interfaceConfigOverwrite) { - for (key in interfaceConfigOverwrite) { - if (!interfaceConfigOverwrite.hasOwnProperty(key) || - typeof key !== 'string') - continue; - this.url += "&interfaceConfig." + key + "=" + - interfaceConfigOverwrite[key]; - } - } - - JitsiMeetExternalAPI.id++; - - this.frame = document.createElement("iframe"); - this.frame.src = this.url; - this.frame.name = this.frameName; - this.frame.id = this.frameName; - this.frame.width = "100%"; - this.frame.height = "100%"; - this.frame.setAttribute("allowFullScreen","true"); - this.frame = this.iframeHolder.appendChild(this.frame); - - - this.frameLoaded = false; - this.initialCommands = []; - this.eventHandlers = {}; - this.initListeners(); - } - - /** - * Last id of api object - * @type {number} - */ - JitsiMeetExternalAPI.id = 0; - - /** - * Sends the passed object to Jitsi Meet - * @param object the object to be sent - */ - JitsiMeetExternalAPI.prototype.sendMessage = function(object) { - if (this.frameLoaded) { - this.frame.contentWindow.postMessage( - JSON.stringify(object), this.frame.src); - } - else { - this.initialCommands.push(object); - } - - }; - - /** - * Executes command. The available commands are: - * displayName - sets the display name of the local participant to the value - * passed in the arguments array. - * toggleAudio - mutes / unmutes audio with no arguments - * toggleVideo - mutes / unmutes video with no arguments - * filmStrip - hides / shows the film strip with no arguments - * If the command doesn't require any arguments the parameter should be set - * to empty array or it may be omitted. - * @param name the name of the command - * @param arguments array of arguments - */ - JitsiMeetExternalAPI.prototype.executeCommand = function(name, - argumentsList) { - var argumentsArray = argumentsList; - if (!argumentsArray) - argumentsArray = []; - var object = {type: "command", action: "execute"}; - object[name] = argumentsArray; - this.sendMessage(object); - }; - - /** - * Executes commands. The available commands are: - * displayName - sets the display name of the local participant to the value - * passed in the arguments array. - * toggleAudio - mutes / unmutes audio with no arguments - * toggleVideo - mutes / unmutes video with no arguments - * filmStrip - hides / shows the film strip with no arguments - * @param object the object with commands to be executed. The keys of the - * object are the commands that will be executed and the values are the - * arguments for the command. - */ - JitsiMeetExternalAPI.prototype.executeCommands = function (object) { - object.type = "command"; - object.action = "execute"; - this.sendMessage(object); - }; - - /** - * Adds event listeners to Meet Jitsi. The object key should be the name of - * the event and value - the listener. - * Currently we support the following - * events: - * incomingMessage - receives event notifications about incoming - * messages. The listener will receive object with the following structure: - * {{ - * "from": from,//JID of the user that sent the message - * "nick": nick,//the nickname of the user that sent the message - * "message": txt//the text of the message - * }} - * outgoingMessage - receives event notifications about outgoing - * messages. The listener will receive object with the following structure: - * {{ - * "message": txt//the text of the message - * }} - * displayNameChanged - receives event notifications about display name - * change. The listener will receive object with the following structure: - * {{ - * jid: jid,//the JID of the participant that changed his display name - * displayname: displayName //the new display name - * }} - * participantJoined - receives event notifications about new participant. - * The listener will receive object with the following structure: - * {{ - * jid: jid //the jid of the participant - * }} - * participantLeft - receives event notifications about the participant that - * left the room. - * The listener will receive object with the following structure: - * {{ - * jid: jid //the jid of the participant - * }} - * @param object - */ - JitsiMeetExternalAPI.prototype.addEventListeners - = function (object) { - - var message = {type: "event", action: "add", events: []}; - for(var i in object) - { - message.events.push(i); - this.eventHandlers[i] = object[i]; - } - this.sendMessage(message); - }; - - /** - * Adds event listeners to Meet Jitsi. Currently we support the following - * events: - * incomingMessage - receives event notifications about incoming - * messages. The listener will receive object with the following structure: - * {{ - * "from": from,//JID of the user that sent the message - * "nick": nick,//the nickname of the user that sent the message - * "message": txt//the text of the message - * }} - * outgoingMessage - receives event notifications about outgoing - * messages. The listener will receive object with the following structure: - * {{ - * "message": txt//the text of the message - * }} - * displayNameChanged - receives event notifications about display name - * change. The listener will receive object with the following structure: - * {{ - * jid: jid,//the JID of the participant that changed his display name - * displayname: displayName //the new display name - * }} - * participantJoined - receives event notifications about new participant. - * The listener will receive object with the following structure: - * {{ - * jid: jid //the jid of the participant - * }} - * participantLeft - receives event notifications about participant the that - * left the room. - * The listener will receive object with the following structure: - * {{ - * jid: jid //the jid of the participant - * }} - * @param event the name of the event - * @param listener the listener - */ - JitsiMeetExternalAPI.prototype.addEventListener - = function (event, listener) { - - var message = {type: "event", action: "add", events: [event]}; - this.eventHandlers[event] = listener; - this.sendMessage(message); - }; - - /** - * Removes event listener. - * @param event the name of the event. - */ - JitsiMeetExternalAPI.prototype.removeEventListener - = function (event) { - if(!this.eventHandlers[event]) - { - console.error("The event " + event + " is not registered."); - return; - } - var message = {type: "event", action: "remove", events: [event]}; - delete this.eventHandlers[event]; - this.sendMessage(message); - }; - - /** - * Removes event listeners. - * @param events array with the names of the events. - */ - JitsiMeetExternalAPI.prototype.removeEventListeners - = function (events) { - var eventsArray = []; - for(var i = 0; i < events.length; i++) - { - var event = events[i]; - if(!this.eventHandlers[event]) - { - console.error("The event " + event + " is not registered."); - continue; - } - delete this.eventHandlers[event]; - eventsArray.push(event); - } - - if(eventsArray.length > 0) - { - this.sendMessage( - {type: "event", action: "remove", events: eventsArray}); - } - - }; - - /** - * Processes message events sent from Jitsi Meet - * @param event the event - */ - JitsiMeetExternalAPI.prototype.processMessage = function(event) { - var message; - try { - message = JSON.parse(event.data); - } catch (e) {} - - if(!message.type) { - console.error("Message without type is received."); - return; - } - switch (message.type) { - case "system": - if(message.loaded) { - this.onFrameLoaded(); - } - break; - case "event": - if(message.action != "result" || - !message.event || !this.eventHandlers[message.event]) { - console.warn("The received event cannot be parsed."); - return; - } - this.eventHandlers[message.event](message.result); - break; - default : - console.error("Unknown message type."); - return; - } - }; - - /** - * That method is called when the Jitsi Meet is loaded. Executes saved - * commands that are send before the frame was loaded. - */ - JitsiMeetExternalAPI.prototype.onFrameLoaded = function () { - this.frameLoaded = true; - for (var i = 0; i < this.initialCommands.length; i++) { - this.sendMessage(this.initialCommands[i]); - } - this.initialCommands = null; - }; - - /** - * Setups the listener for message events from Jitsi Meet. - */ - JitsiMeetExternalAPI.prototype.initListeners = function () { - var self = this; - this.eventListener = function (event) { - self.processMessage(event); - }; - if (window.addEventListener) { - window.addEventListener('message', - this.eventListener, false); - } - else { - window.attachEvent('onmessage', this.eventListener); - } - }; - - /** - * Removes the listeners and removes the Jitsi Meet frame. - */ - JitsiMeetExternalAPI.prototype.dispose = function () { - if (window.removeEventListener) { - window.removeEventListener('message', - this.eventListener, false); - } - else { - window.detachEvent('onmessage', - this.eventListener); - } - var frame = document.getElementById(this.frameName); - if(frame) - frame.src = 'about:blank'; - var self = this; - window.setTimeout(function () { - self.iframeHolder.removeChild(self.frame); - self.iframeHolder.parentNode.removeChild(self.iframeHolder); - }, 10); - }; - - return JitsiMeetExternalAPI; - -})(); diff --git a/modules/API/API.js b/modules/API/API.js index bb6e5bbac..91c4ca622 100644 --- a/modules/API/API.js +++ b/modules/API/API.js @@ -1,11 +1,11 @@ -/* global APP */ +/* global APP, getConfigParamsFromUrl */ /** * Implements API class that communicates with external api class * and provides interface to access Jitsi Meet features by external * applications that embed Jitsi Meet */ - import postis from 'postis'; +import postisInit from 'postis'; /** * List of the available commands. @@ -20,36 +20,41 @@ */ let commands = {}; +let hashParams = getConfigParamsFromUrl(); + +/** + * JitsiMeetExternalAPI id - unique for a webpage. + */ +let jitsi_meet_external_api_id = hashParams.jitsi_meet_external_api_id; + /** * Object that will execute sendMessage */ let target = window.opener ? window.opener : window.parent; /** - * Array of functions that are going to receive the objects passed to this - * window + * Postis instance. Used to communicate with the external application. */ -let messageListeners = []; +let postis; /** - * Current status (enabled/disabled) of Postis. + * Current status (enabled/disabled) of API. */ -let enablePostis = false; - -/** - * Current status (enabled/disabled) of Post Message API. - */ -let enablePostMessage = false; +let enabled = false; function initCommands() { commands = { - displayName: APP.UI.inputDisplayNameHandler, - toggleAudio: APP.conference.toggleAudioMuted, - toggleVideo: APP.conference.toggleVideoMuted, - toggleFilmStrip: APP.UI.toggleFilmStrip, - toggleChat: APP.UI.toggleChat, - toggleContactList: APP.UI.toggleContactList + "display-name": APP.UI.inputDisplayNameHandler, + "toggle-audio": APP.conference.toggleAudioMuted, + "toggle-video": APP.conference.toggleVideoMuted, + "toggle-film-strip": APP.UI.toggleFilmStrip, + "toggle-chat": APP.UI.toggleChat, + "toggle-contact-list": APP.UI.toggleContactList, + "toggle-share-screen": APP.conference.toggleScreenSharing }; + Object.keys(commands).forEach(function (key) { + postis.listen(key, commands[key]); + }); } @@ -57,95 +62,34 @@ function initCommands() { * Maps the supported events and their status * (true it the event is enabled and false if it is disabled) * @type {{ - * incomingMessage: boolean, - * outgoingMessage: boolean, - * displayNameChange: boolean, - * participantJoined: boolean, - * participantLeft: boolean + * incoming-message: boolean, + * outgoing-message: boolean, + * display-name-change: boolean, + * participant-left: boolean, + * participant-joined: boolean, + * video-conference-left: boolean, + * video-conference-joined: boolean * }} */ const events = { - incomingMessage: false, - outgoingMessage:false, - displayNameChange: false, - participantJoined: false, - participantLeft: false + "incoming-message": false, + "outgoing-message":false, + "display-name-change": false, + "participant-joined": false, + "participant-left": false, + "video-conference-joined": false, + "video-conference-left": false }; -/** - * Processes commands from external application. - * @param message the object with the command - */ -function processCommand(message) { - if (message.action != "execute") { - console.error("Unknown action of the message"); - return; - } - for (var key in message) { - if(commands[key]) - commands[key].apply(null, message[key]); - } -} - -/** - * Processes events objects from external applications - * @param event the event - */ -function processEvent(event) { - if (!event.action) { - console.error("Event with no action is received."); - return; - } - - var i = 0; - switch(event.action) { - case "add": - for (; i < event.events.length; i++) { - events[event.events[i]] = true; - } - break; - case "remove": - for (; i < event.events.length; i++) { - events[event.events[i]] = false; - } - break; - default: - console.error("Unknown action for event."); - } -} - -/** - * Processes a message event from the external application - * @param event the message event - */ -function processMessage(event) { - var message; - try { - message = JSON.parse(event.data); - } catch (e) { - console.error("Cannot parse data", event.data); - return; - } - - switch (message.type) { - case "command": - processCommand(message); - break; - case "event": - processEvent(message); - break; - default: - console.warn("Unknown message type"); - } -} - /** * Sends message to the external application. - * @param object {object} the object that will be sent as JSON string + * @param message {object} + * @param method {string} + * @param params {object} the object that will be sent as JSON string */ -function sendMessage(object) { - if(enablePostMessage) - target.postMessage(JSON.stringify(object), "*"); +function sendMessage(message) { + if(enabled) + postis.send(message); } /** @@ -153,8 +97,7 @@ function sendMessage(object) { * @returns {boolean} */ function isEnabled () { - let hash = location.hash; - return !!(hash && hash.indexOf("external=true") > -1 && window.postMessage); + return (typeof jitsi_meet_external_api_id === "number"); } /** @@ -173,13 +116,25 @@ function isEventEnabled (name) { * @param object data associated with the event */ function triggerEvent (name, object) { - if (isEventEnabled(name) && enablePostMessage) { - sendMessage({ - type: "event", - action: "result", - event: name, - result: object - }); + if(isEventEnabled(name)) + sendMessage({method: name, params: object}); +} + +/** + * Handles system messages. (for example: enable/disable events) + * @param message {object} the message + */ +function onSystemMessage(message) { + switch (message.type) { + case "eventStatus": + if(!message.name || !message.value) { + console.warn("Unknown system message format", message); + break; + } + events[message.name] = message.value; + break; + default: + console.warn("Unknown system message type", message); } } @@ -190,33 +145,27 @@ export default { * It also sends a message to the external application that APIConnector * is initialized. * @param options {object} - * @param enablePostis {boolean} if true the postis npm - * package for comminication with the parent window will be enabled. - * @param enablePostMessage {boolean} if true the postMessageAPI for - * comminication with the parent window will be enabled. + * @param forceEnable {boolean} if true the module will be enabled. + * @param enabledEvents {array} array of events that should be enabled. */ - init: function (options = {}) { - options.enablePostMessage = options.enablePostMessage || isEnabled(); - if (!options.enablePostis && - !options.enablePostMessage) { + init (options = {}) { + if(!isEnabled() && !options.forceEnable) return; - } - enablePostis = options.enablePostis; - enablePostMessage = options.enablePostMessage; - if(enablePostMessage) { - initCommands(); - if (window.addEventListener) { - window.addEventListener('message', processMessage, false); - } else { - window.attachEvent('onmessage', processMessage); - } - sendMessage({type: "system", loaded: true}); - } - - if(enablePostis) { - this.postis = postis({window: target}); - } + enabled = true; + if(options.enabledEvents) + options.enabledEvents.forEach(function (eventName) { + events[eventName] = true; + }); + let postisOptions = { + window: target + }; + if(typeof jitsi_meet_external_api_id === "number") + postisOptions.scope + = "jitsi_meet_external_api_" + jitsi_meet_external_api_id; + postis = postisInit(postisOptions); + postis.listen("jitsiSystemMessage", onSystemMessage); + initCommands(); }, /** @@ -224,28 +173,7 @@ export default { * @param {string} body message body */ notifySendingChatMessage (body) { - triggerEvent("outgoingMessage", {"message": body}); - }, - - /** - * Sends message to the external application. - * @param options {object} - * @param method {string} - * @param params {object} the object that will be sent as JSON string - */ - sendPostisMessage(options) { - if(enablePostis) - this.postis.send(options); - }, - - /** - * Adds listener for Postis messages. - * @param method {string} postis mehtod - * @param listener {function} - */ - addPostisMessageListener (method, listener) { - if(enablePostis) - this.postis.listen(method, listener); + triggerEvent("outgoing-message", {"message": body}); }, /** @@ -262,7 +190,7 @@ export default { } triggerEvent( - "incomingMessage", + "incoming-message", {"from": id, "nick": nick, "message": body, "stamp": ts} ); }, @@ -273,7 +201,7 @@ export default { * @param {string} id user id */ notifyUserJoined (id) { - triggerEvent("participantJoined", {id}); + triggerEvent("participant-joined", {id}); }, /** @@ -282,7 +210,7 @@ export default { * @param {string} id user id */ notifyUserLeft (id) { - triggerEvent("participantLeft", {id}); + triggerEvent("participant-left", {id}); }, /** @@ -292,21 +220,34 @@ export default { * @param {string} displayName user nickname */ notifyDisplayNameChanged (id, displayName) { - triggerEvent("displayNameChange", {id, displayname: displayName}); + triggerEvent("display-name-change", {id, displayname: displayName}); + }, + + /** + * Notify external application (if API is enabled) that + * user changed their nickname. + * @param {string} id user id + * @param {string} displayName user nickname + */ + notifyConferenceJoined (room) { + triggerEvent("video-conference-joined", {roomName: room}); + }, + + /** + * Notify external application (if API is enabled) that + * user changed their nickname. + * @param {string} id user id + * @param {string} displayName user nickname + */ + notifyConferenceLeft (room) { + triggerEvent("video-conference-left", {roomName: room}); }, /** * Removes the listeners. */ dispose: function () { - if (enablePostMessage) { - if (window.removeEventListener) { - window.removeEventListener("message", processMessage, false); - } else { - window.detachEvent('onmessage', processMessage); - } - } - if(enablePostis) - this.postis.destroy(); + if(enabled) + postis.destroy(); } }; diff --git a/modules/API/external/external_api.js b/modules/API/external/external_api.js new file mode 100644 index 000000000..02a920d22 --- /dev/null +++ b/modules/API/external/external_api.js @@ -0,0 +1,359 @@ +/** + * Implements API class that embeds Jitsi Meet in external applications. + */ + +var postisInit = require("postis"); + +/** + * The minimum width for the Jitsi Meet frame + * @type {number} + */ +var MIN_WIDTH = 790; + +/** + * The minimum height for the Jitsi Meet frame + * @type {number} + */ +var MIN_HEIGHT = 300; + +/** + * Last id of api object + * @type {number} + */ +var id = 0; + +/** + * Maps the names of the commands expected by the API with the name of the + * commands expected by jitsi-meet + */ +var commands = { + "displayName": "display-name", + "toggleAudio": "toggle-audio", + "toggleVideo": "toggle-video", + "toggleFilmStrip": "toggle-film-strip", + "toggleChat": "toggle-chat", + "toggleContactList": "toggle-contact-list", + "toggleShareScreen": "toggle-share-screen" +}; + +/** + * Maps the names of the events expected by the API with the name of the + * events expected by jitsi-meet + */ +var events = { + "incomingMessage": "incoming-message", + "outgoingMessage": "outgoing-message", + "displayNameChange": "display-name-change", + "participantJoined": "participant-joined", + "participantLeft": "participant-left", + "videoConferenceJoined": "video-conference-joined", + "videoConferenceLeft": "video-conference-left" +}; + +/** + * Sends the passed object to Jitsi Meet + * @param postis {Postis object} the postis instance that is going to be used + * to send the message + * @param object the object to be sent + * - method {sting} + * - params {object} + */ +function sendMessage(postis, object) { + postis.send(object); +} + +/** + * Sends message for event enable/disable status change. + * @param postis {Postis object} the postis instance that is going to be used. + * @param event {string} the name of the event + * @param status {boolean} true - enabled; false - disabled; + */ +function changeEventStatus(postis, event, status) { + if(!(event in events)) { + console.error("Not supported event name."); + return; + } + sendMessage(postis, { + method: "jitsiSystemMessage", + params: {type: "eventStatus", name: events[event], value: status} + }); +} + +/** + * Constructs new API instance. Creates iframe element that loads + * Jitsi Meet. + * @param domain the domain name of the server that hosts the conference + * @param room_name the name of the room to join + * @param width width of the iframe + * @param height height of the iframe + * @param parent_node the node that will contain the iframe + * @param filmStripOnly if the value is true only the small videos will be + * visible. + * @param noSsl if the value is true https won't be used + * @constructor + */ +function JitsiMeetExternalAPI(domain, room_name, width, height, parentNode, + configOverwrite, interfaceConfigOverwrite, noSsl) { + if (!width || width < MIN_WIDTH) + width = MIN_WIDTH; + if (!height || height < MIN_HEIGHT) + height = MIN_HEIGHT; + + this.parentNode = null; + if (parentNode) { + this.parentNode = parentNode; + } else { + var scriptTag = document.scripts[document.scripts.length - 1]; + this.parentNode = scriptTag.parentNode; + } + + this.iframeHolder = + this.parentNode.appendChild(document.createElement("div")); + this.iframeHolder.id = "jitsiConference" + id; + if(width) + this.iframeHolder.style.width = width + "px"; + if(height) + this.iframeHolder.style.height = height + "px"; + this.frameName = "jitsiConferenceFrame" + id; + this.url = (noSsl) ? "http" : "https" +"://" + domain + "/"; + if(room_name) + this.url += room_name; + this.url += "#jitsi_meet_external_api_id=" + id; + + var key; + if (configOverwrite) { + for (key in configOverwrite) { + if (!configOverwrite.hasOwnProperty(key) || + typeof key !== 'string') + continue; + this.url += "&config." + key + "=" + configOverwrite[key]; + } + } + + if (interfaceConfigOverwrite) { + for (key in interfaceConfigOverwrite) { + if (!interfaceConfigOverwrite.hasOwnProperty(key) || + typeof key !== 'string') + continue; + this.url += "&interfaceConfig." + key + "=" + + interfaceConfigOverwrite[key]; + } + } + + this.frame = document.createElement("iframe"); + this.frame.src = this.url; + this.frame.name = this.frameName; + this.frame.id = this.frameName; + this.frame.width = "100%"; + this.frame.height = "100%"; + this.frame.setAttribute("allowFullScreen","true"); + this.frame = this.iframeHolder.appendChild(this.frame); + this.postis = postisInit({ + window: this.frame.contentWindow, + scope: "jitsi_meet_external_api_" + id + }); + + this.eventHandlers = {}; + + id++; +} + +/** + * Executes command. The available commands are: + * displayName - sets the display name of the local participant to the value + * passed in the arguments array. + * toggleAudio - mutes / unmutes audio with no arguments + * toggleVideo - mutes / unmutes video with no arguments + * filmStrip - hides / shows the film strip with no arguments + * If the command doesn't require any arguments the parameter should be set + * to empty array or it may be omitted. + * @param name the name of the command + * @param arguments array of arguments + */ +JitsiMeetExternalAPI.prototype.executeCommand = function(name, argumentsList) { + if(!(name in commands)) { + console.error("Not supported command name."); + return; + } + var argumentsArray = argumentsList; + if (!argumentsArray) + argumentsArray = []; + sendMessage(this.postis, {method: commands[name], params: argumentsArray}); +}; + +/** + * Executes commands. The available commands are: + * displayName - sets the display name of the local participant to the value + * passed in the arguments array. + * toggleAudio - mutes / unmutes audio. no arguments + * toggleVideo - mutes / unmutes video. no arguments + * filmStrip - hides / shows the film strip. no arguments + * toggleChat - hides / shows chat. no arguments. + * toggleContactList - hides / shows contact list. no arguments. + * toggleShareScreen - starts / stops screen sharing. no arguments. + * @param object the object with commands to be executed. The keys of the + * object are the commands that will be executed and the values are the + * arguments for the command. + */ +JitsiMeetExternalAPI.prototype.executeCommands = function(object) { + for(var key in object) + this.executeCommand(key, object[key]); +}; + +/** + * Adds event listeners to Meet Jitsi. The object key should be the name of + * the event and value - the listener. + * Currently we support the following + * events: + * incomingMessage - receives event notifications about incoming + * messages. The listener will receive object with the following structure: + * {{ + * "from": from,//JID of the user that sent the message + * "nick": nick,//the nickname of the user that sent the message + * "message": txt//the text of the message + * }} + * outgoingMessage - receives event notifications about outgoing + * messages. The listener will receive object with the following structure: + * {{ + * "message": txt//the text of the message + * }} + * displayNameChanged - receives event notifications about display name + * change. The listener will receive object with the following structure: + * {{ + * jid: jid,//the JID of the participant that changed his display name + * displayname: displayName //the new display name + * }} + * participantJoined - receives event notifications about new participant. + * The listener will receive object with the following structure: + * {{ + * jid: jid //the jid of the participant + * }} + * participantLeft - receives event notifications about the participant that + * left the room. + * The listener will receive object with the following structure: + * {{ + * jid: jid //the jid of the participant + * }} + * video-conference-joined - receives event notifications about the local user + * has successfully joined the video conference. + * The listener will receive object with the following structure: + * {{ + * roomName: room //the room name of the conference + * }} + * video-conference-left - receives event notifications about the local user + * has left the video conference. + * The listener will receive object with the following structure: + * {{ + * roomName: room //the room name of the conference + * }} + * @param object + */ +JitsiMeetExternalAPI.prototype.addEventListeners = function(object) { + for(var i in object) + this.addEventListener(i, object[i]); +}; + +/** + * Adds event listeners to Meet Jitsi. Currently we support the following + * events: + * incomingMessage - receives event notifications about incoming + * messages. The listener will receive object with the following structure: + * {{ + * "from": from,//JID of the user that sent the message + * "nick": nick,//the nickname of the user that sent the message + * "message": txt//the text of the message + * }} + * outgoingMessage - receives event notifications about outgoing + * messages. The listener will receive object with the following structure: + * {{ + * "message": txt//the text of the message + * }} + * displayNameChanged - receives event notifications about display name + * change. The listener will receive object with the following structure: + * {{ + * jid: jid,//the JID of the participant that changed his display name + * displayname: displayName //the new display name + * }} + * participantJoined - receives event notifications about new participant. + * The listener will receive object with the following structure: + * {{ + * jid: jid //the jid of the participant + * }} + * participantLeft - receives event notifications about participant the that + * left the room. + * The listener will receive object with the following structure: + * {{ + * jid: jid //the jid of the participant + * }} + * video-conference-joined - receives event notifications fired when the local + * user has joined the video conference. + * The listener will receive object with the following structure: + * {{ + * roomName: room //the room name of the conference + * }} + * video-conference-left - receives event notifications fired when the local + * user has joined the video conference. + * The listener will receive object with the following structure: + * {{ + * roomName: room //the room name of the conference + * }} + * @param event the name of the event + * @param listener the listener + */ +JitsiMeetExternalAPI.prototype.addEventListener = function(event, listener) { + if(!(event in events)) { + console.error("Not supported event name."); + return; + } + // We cannot remove listeners from postis that's why we are handling the + // callback that way. + if(!(event in this.eventHandlers)) + this.postis.listen(events[event], function(data) { + if((event in this.eventHandlers) && + typeof this.eventHandlers[event] === "function") + this.eventHandlers[event].call(null, data); + }.bind(this)); + this.eventHandlers[event] = listener; + changeEventStatus(this.postis, event, true); +}; + +/** + * Removes event listener. + * @param event the name of the event. + */ +JitsiMeetExternalAPI.prototype.removeEventListener = function(event) { + if(!(event in this.eventHandlers)) + { + console.error("The event " + event + " is not registered."); + return; + } + delete this.eventHandlers[event]; + changeEventStatus(this.postis, event, false); +}; + +/** + * Removes event listeners. + * @param events array with the names of the events. + */ +JitsiMeetExternalAPI.prototype.removeEventListeners = function(events) { + var eventsArray = []; + for(var i = 0; i < events.length; i++) + this.removeEventListener(events[i]); +}; + +/** + * Removes the listeners and removes the Jitsi Meet frame. + */ +JitsiMeetExternalAPI.prototype.dispose = function() { + this.postis.dispose(); + var frame = document.getElementById(this.frameName); + if(frame) + frame.src = 'about:blank'; + var self = this; + window.setTimeout(function () { + self.iframeHolder.removeChild(self.frame); + self.iframeHolder.parentNode.removeChild(self.iframeHolder); + }, 10); +}; + +module.exports = JitsiMeetExternalAPI; diff --git a/modules/TokenData/TokenData.js b/modules/TokenData/TokenData.js index 3a7ac29d6..64aacf7a1 100644 --- a/modules/TokenData/TokenData.js +++ b/modules/TokenData/TokenData.js @@ -72,7 +72,8 @@ class TokenData{ //External API settings this.externalAPISettings = { - enablePostis: true + forceEnable: true, + enabledEvents: ["video-conference-joined", "video-conference-left"] }; this._decode(); // Use JWT param as token if there is not other token set and if the From 21c2469dd6c9607ab149a7cee313807881d36e05 Mon Sep 17 00:00:00 2001 From: hristoterezov Date: Mon, 20 Jun 2016 13:23:00 -0500 Subject: [PATCH 03/10] Removes unnecessary whitespaces from Makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index f0cb09367..393f72703 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ OUTPUT_DIR = . LIBJITSIMEET_DIR = node_modules/lib-jitsi-meet/ IFRAME_API_DIR = ./modules/API/external -all: update-deps compile compile-iframe-api uglify uglify-iframe-api deploy clean +all: update-deps compile compile-iframe-api uglify uglify-iframe-api deploy clean update-deps: $(NPM) install From 4b6ac38058eba70bd1fb1b2824243f9238432c89 Mon Sep 17 00:00:00 2001 From: Boris Grozev Date: Fri, 10 Jun 2016 12:43:29 -0500 Subject: [PATCH 04/10] Fixes a failure to show the dominant speaker indicator for the local participant. --- modules/UI/videolayout/VideoLayout.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/UI/videolayout/VideoLayout.js b/modules/UI/videolayout/VideoLayout.js index 5f2a785b4..bb72899a8 100644 --- a/modules/UI/videolayout/VideoLayout.js +++ b/modules/UI/videolayout/VideoLayout.js @@ -577,9 +577,9 @@ var VideoLayout = { if(oldSpeakerRemoteVideo) { oldSpeakerRemoteVideo.updateDominantSpeakerIndicator(false); - localVideoThumbnail.updateDominantSpeakerIndicator(true); currentDominantSpeaker = null; } + localVideoThumbnail.updateDominantSpeakerIndicator(true); return; } From 2d2e27b8d06836c4ad8bdb863de2c5f5a615219c Mon Sep 17 00:00:00 2001 From: Boris Grozev Date: Mon, 20 Jun 2016 16:13:17 -0500 Subject: [PATCH 05/10] Implements "raised hand". --- conference.js | 50 ++++++++++++++ css/videolayout_default.css | 4 +- lang/main.json | 4 +- modules/UI/UI.js | 20 +++++- modules/UI/util/MessageHandler.js | 15 +++-- modules/UI/videolayout/SmallVideo.js | 70 ++++++++++++++------ modules/UI/videolayout/VideoLayout.js | 22 ++++-- modules/keyboardshortcut/keyboardshortcut.js | 7 ++ 8 files changed, 158 insertions(+), 34 deletions(-) diff --git a/conference.js b/conference.js index eb4bbe04b..3c5915098 100644 --- a/conference.js +++ b/conference.js @@ -394,6 +394,14 @@ export default { videoMuted: false, isSharingScreen: false, isDesktopSharingEnabled: false, + /* + * Whether the local "raisedHand" flag is on. + */ + isHandRaised: false, + /* + * Whether the local participant is the dominant speaker in the conference. + */ + isDominantSpeaker: false, /** * Open new connection and join to the conference. * @param {object} options @@ -1018,6 +1026,16 @@ export default { APP.UI.handleLastNEndpoints(ids, enteringIds); }); room.on(ConferenceEvents.DOMINANT_SPEAKER_CHANGED, (id) => { + if (this.isLocalId(id)) { + this.isDominantSpeaker = true; + this.setRaisedHand(false); + } else { + this.isDominantSpeaker = false; + var participant = room.getParticipantById(id); + if (participant) { + APP.UI.setRaisedHandStatus(participant, false); + } + } APP.UI.markDominantSpeaker(id); }); @@ -1040,6 +1058,13 @@ export default { APP.UI.changeDisplayName(id, displayName); }); + room.on(ConferenceEvents.PARTICIPANT_PROPERTY_CHANGED, + (participant, name, oldValue, newValue) => { + if (name === "raisedHand") { + APP.UI.setRaisedHandStatus(participant, newValue); + } + }); + room.on(ConferenceEvents.RECORDER_STATE_CHANGED, (status, error) => { console.log("Received recorder status change: ", status, error); APP.UI.updateRecordingState(status); @@ -1432,5 +1457,30 @@ export default { mediaDeviceHelper.setCurrentMediaDevices(devices); APP.UI.onAvailableDevicesChanged(devices); }); + }, + + /** + * Toggles the local "raised hand" status, if the current state allows + * toggling. + */ + maybeToggleRaisedHand() { + // If we are the dominant speaker, we don't enable "raise hand". + if (this.isHandRaised || !this.isDominantSpeaker) { + this.setRaisedHand(!this.isHandRaised); + } + }, + + /** + * Sets the local "raised hand" status to a particular value. + */ + setRaisedHand(raisedHand) { + if (raisedHand !== this.isHandRaised) + { + this.isHandRaised = raisedHand; + // Advertise the updated status + room.setLocalParticipantProperty("raisedHand", raisedHand); + // Update the view + APP.UI.setLocalRaisedHandStatus(raisedHand); + } } }; \ No newline at end of file diff --git a/css/videolayout_default.css b/css/videolayout_default.css index 9d16b77e2..62dcdcbd6 100644 --- a/css/videolayout_default.css +++ b/css/videolayout_default.css @@ -310,7 +310,7 @@ z-index: 3; } -.videocontainer>span.dominantspeakerindicator { +.videocontainer>span.indicator { bottom: 0px; left: 0px; width: 25px; @@ -327,7 +327,7 @@ border: 0px; } -#speakerindicatoricon { +#indicatoricon { padding-top: 5px; } diff --git a/lang/main.json b/lang/main.json index e947c3f72..a35e4e57c 100644 --- a/lang/main.json +++ b/lang/main.json @@ -8,6 +8,7 @@ "participant": "Participant", "me": "me", "speaker": "Speaker", + "raisedHand": "Would like to speak", "defaultNickname": "ex. Jane Pink", "defaultLink": "e.g. __url__", "calling": "Calling __name__ ...", @@ -154,7 +155,8 @@ "grantedTo": "Moderator rights granted to __to__!", "grantedToUnknown": "Moderator rights granted to $t(somebody)!", "muted": "You have started the conversation muted.", - "mutedTitle": "You're muted!" + "mutedTitle": "You're muted!", + "raisedHand": "Would like to speak." }, "dialog": { "kickMessage": "Ouch! You have been kicked out of the meet!", diff --git a/modules/UI/UI.js b/modules/UI/UI.js index 300e00a7e..95baabf45 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -255,7 +255,25 @@ UI.changeDisplayName = function (id, displayName) { }; /** - * Intitialize conference UI. + * Sets the "raised hand" status for a participant. + */ +UI.setRaisedHandStatus = (participant, raisedHandStatus) => { + VideoLayout.setRaisedHandStatus(participant.getId(), raisedHandStatus); + if (raisedHandStatus) { + messageHandler.notify(participant.getDisplayName(), 'notify.somebody', + 'connected', 'notify.raisedHand'); + } +}; + +/** + * Sets the local "raised hand" status. + */ +UI.setLocalRaisedHandStatus = (raisedHandStatus) => { + VideoLayout.setRaisedHandStatus(APP.conference.localId, raisedHandStatus); +}; + +/** + * Initialize conference UI. */ UI.initConference = function () { let id = APP.conference.localId; diff --git a/modules/UI/util/MessageHandler.js b/modules/UI/util/MessageHandler.js index fdf274481..b450361f2 100644 --- a/modules/UI/util/MessageHandler.js +++ b/modules/UI/util/MessageHandler.js @@ -215,16 +215,19 @@ var messageHandler = { }, /** - * Displayes notification. - * @param displayName display name of the participant that is associated with the notification. - * @param displayNameKey the key from the language file for the display name. + * Displays a notification. + * @param displayName the display name of the participant that is + * associated with the notification. + * @param displayNameKey the key from the language file for the display + * name. Only used if displayName i not provided. * @param cls css class for the notification - * @param messageKey the key from the language file for the text of the message. + * @param messageKey the key from the language file for the text of the + * message. * @param messageArguments object with the arguments for the message. * @param options object with language options. */ - notify: function(displayName, displayNameKey, - cls, messageKey, messageArguments, options) { + notify: function(displayName, displayNameKey, cls, messageKey, + messageArguments, options) { if(!notificationsEnabled) return; diff --git a/modules/UI/videolayout/SmallVideo.js b/modules/UI/videolayout/SmallVideo.js index a750e1156..37a5d1c2a 100644 --- a/modules/UI/videolayout/SmallVideo.js +++ b/modules/UI/videolayout/SmallVideo.js @@ -422,37 +422,69 @@ SmallVideo.prototype.avatarChanged = function (avatarUrl) { }; /** - * Updates the Indicator for dominant speaker. - * - * @param isSpeaker indicates the current indicator state + * Shows or hides the dominant speaker indicator. + * @param show whether to show or hide. */ -SmallVideo.prototype.updateDominantSpeakerIndicator = function (isSpeaker) { - +SmallVideo.prototype.showDominantSpeakerIndicator = function (show) { if (!this.container) { console.warn( "Unable to set dominant speaker indicator - " + this.videoSpanId + " does not exist"); return; } - var indicatorSpan - = $('#' + this.videoSpanId + '>span.dominantspeakerindicator'); + var indicatorSpanId = "dominantspeakerindicator"; + var indicatorSpan = this.getIndicatorSpan(indicatorSpanId); - // If we do not have an indicator for this video. - if (indicatorSpan.length <= 0) { - indicatorSpan = document.createElement('span'); + indicatorSpan.innerHTML + = ""; + // adds a tooltip + UIUtil.setTooltip(indicatorSpan, "speaker", "left"); + APP.translation.translateElement($(indicatorSpan)); - indicatorSpan.innerHTML - = ""; - indicatorSpan.className = 'dominantspeakerindicator'; + $(indicatorSpan).css("visibility", show ? "visible" : "hidden"); +}; - $('#' + this.videoSpanId)[0].appendChild(indicatorSpan); - - // adds a tooltip - UIUtil.setTooltip(indicatorSpan, "speaker", "left"); - APP.translation.translateElement($(indicatorSpan)); +/** + * Shows or hides the raised hand indicator. + * @param show whether to show or hide. + */ +SmallVideo.prototype.showRaisedHandIndicator = function (show) { + if (!this.container) { + console.warn( "Unable to raised hand indication - " + + this.videoSpanId + " does not exist"); + return; } - $(indicatorSpan).css("visibility", isSpeaker ? "visible" : "hidden"); + var indicatorSpanId = "raisehandindicator"; + var indicatorSpan = this.getIndicatorSpan(indicatorSpanId); + + indicatorSpan.style.background = "#D6D61E"; + indicatorSpan.innerHTML + = ""; + + // adds a tooltip + UIUtil.setTooltip(indicatorSpan, "raisedHand", "left"); + APP.translation.translateElement($(indicatorSpan)); + + $(indicatorSpan).css("visibility", show ? "visible" : "hidden"); +}; + +/** + * Gets (creating if necessary) the "indicator" span for this SmallVideo + identified by an ID. + */ +SmallVideo.prototype.getIndicatorSpan = function(id) { + var indicatorSpan; + var spans = $(`#${this.videoSpanId}>[id=${id}`); + if (spans.length <= 0) { + indicatorSpan = document.createElement('span'); + indicatorSpan.id = id; + indicatorSpan.className = "indicator"; + $('#' + this.videoSpanId)[0].appendChild(indicatorSpan); + } else { + indicatorSpan = spans[0]; + } + return indicatorSpan; }; export default SmallVideo; diff --git a/modules/UI/videolayout/VideoLayout.js b/modules/UI/videolayout/VideoLayout.js index bb72899a8..c47be8854 100644 --- a/modules/UI/videolayout/VideoLayout.js +++ b/modules/UI/videolayout/VideoLayout.js @@ -562,6 +562,18 @@ var VideoLayout = { } }, + /** + * Sets the "raised hand" status for a participant identified by 'id'. + */ + setRaisedHandStatus(id, raisedHandStatus) { + var video + = APP.conference.isLocalId(id) + ? localVideoThumbnail : remoteVideos[id]; + if (video) { + video.showRaisedHandIndicator(raisedHandStatus); + } + }, + /** * On dominant speaker changed event. */ @@ -576,10 +588,10 @@ var VideoLayout = { if (APP.conference.isLocalId(id)) { if(oldSpeakerRemoteVideo) { - oldSpeakerRemoteVideo.updateDominantSpeakerIndicator(false); + oldSpeakerRemoteVideo.showDominantSpeakerIndicator(false); currentDominantSpeaker = null; } - localVideoThumbnail.updateDominantSpeakerIndicator(true); + localVideoThumbnail.showDominantSpeakerIndicator(true); return; } @@ -589,12 +601,12 @@ var VideoLayout = { } // Update the current dominant speaker. - remoteVideo.updateDominantSpeakerIndicator(true); - localVideoThumbnail.updateDominantSpeakerIndicator(false); + remoteVideo.showDominantSpeakerIndicator(true); + localVideoThumbnail.showDominantSpeakerIndicator(false); // let's remove the indications from the remote video if any if (oldSpeakerRemoteVideo) { - oldSpeakerRemoteVideo.updateDominantSpeakerIndicator(false); + oldSpeakerRemoteVideo.showDominantSpeakerIndicator(false); } currentDominantSpeaker = id; diff --git a/modules/keyboardshortcut/keyboardshortcut.js b/modules/keyboardshortcut/keyboardshortcut.js index 1d129c2b0..fbcb67e9e 100644 --- a/modules/keyboardshortcut/keyboardshortcut.js +++ b/modules/keyboardshortcut/keyboardshortcut.js @@ -40,6 +40,13 @@ function initShortcutHandlers() { APP.conference.toggleAudioMuted(); } }, + 82: { + character: "R", + function: function() { + APP.conference.maybeToggleRaisedHand(); + } + + }, 84: { character: "T", function: function() { From 375b145030b1469afef91396beca0db99d540847 Mon Sep 17 00:00:00 2001 From: tsareg Date: Tue, 21 Jun 2016 17:39:00 +0300 Subject: [PATCH 06/10] Prevent possible memory leak --- modules/UI/UI.js | 6 ++++++ modules/UI/util/MessageHandler.js | 8 ++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/modules/UI/UI.js b/modules/UI/UI.js index c3d6f97a7..fc72facb0 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -1289,6 +1289,12 @@ UI.showDeviceErrorDialog = function (micError, cameraError) { input.prop("checked"); } } + }, + null, + function () { + // Reset dialog reference to null to avoid memory leaks when + // user closed the dialog manually. + deviceErrorDialog = null; } ); diff --git a/modules/UI/util/MessageHandler.js b/modules/UI/util/MessageHandler.js index fdf274481..bbeacb4e1 100644 --- a/modules/UI/util/MessageHandler.js +++ b/modules/UI/util/MessageHandler.js @@ -113,9 +113,10 @@ var messageHandler = { * @param submitFunction function to be called on submit * @param loadedFunction function to be called after the prompt is fully * loaded + * @param closeFunction function to be called on dialog close */ openDialog: function (titleString, msgString, persistent, buttons, - submitFunction, loadedFunction) { + submitFunction, loadedFunction, closeFunction) { if (!popupEnabled) return; @@ -125,11 +126,14 @@ var messageHandler = { buttons: buttons, defaultButton: 1, loaded: loadedFunction, - submit: submitFunction + submit: submitFunction, + close: closeFunction }; + if (persistent) { args.closeText = ''; } + return new Impromptu(msgString, args); }, From 6715d41f92f538a546c8075d0d88e0ea6a73be2f Mon Sep 17 00:00:00 2001 From: damencho Date: Tue, 21 Jun 2016 10:38:31 -0500 Subject: [PATCH 07/10] Removes unused dependency. Used to minimize strophe-plugins which were inside the source tree and now npm handles them. --- debian/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/control b/debian/control index 96284058d..bb39000d6 100644 --- a/debian/control +++ b/debian/control @@ -3,7 +3,7 @@ Section: net Priority: extra Maintainer: Jitsi Team Uploaders: Emil Ivov , Damian Minkov -Build-Depends: debhelper (>= 8.0.0), yui-compressor +Build-Depends: debhelper (>= 8.0.0) Standards-Version: 3.9.6 Homepage: https://jitsi.org/meet From 70e5ce7aec5460b7da2444215b11887f6916ea0f Mon Sep 17 00:00:00 2001 From: yanas Date: Tue, 21 Jun 2016 15:01:43 -0500 Subject: [PATCH 08/10] Fixes avatar URL setting string --- index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.html b/index.html index 469b1e2cb..755a504db 100644 --- a/index.html +++ b/index.html @@ -241,7 +241,7 @@
- +