diff --git a/ConferenceEvents.js b/ConferenceEvents.js new file mode 100644 index 000000000..2dfa4cd87 --- /dev/null +++ b/ConferenceEvents.js @@ -0,0 +1,4 @@ +/** + * Notifies interested parties that hangup procedure will start. + */ +export const BEFORE_HANGUP = "conference.before_hangup"; diff --git a/app.js b/app.js index 044c8c0fc..f892d3e15 100644 --- a/app.js +++ b/app.js @@ -22,6 +22,7 @@ import conference from './conference'; import API from './modules/API/API'; import translation from "./modules/translation/translation"; +import remoteControl from "./modules/remotecontrol/RemoteControl"; const APP = { // Used by do_external_connect.js if we receive the attach data after @@ -59,7 +60,8 @@ const APP = { */ ConferenceUrl : null, connection: null, - API + API, + remoteControl }; // TODO The execution of the mobile app starts from react/index.native.js. diff --git a/conference.js b/conference.js index 9f00d7449..ffda92fb2 100644 --- a/conference.js +++ b/conference.js @@ -14,9 +14,12 @@ import {reportError} from './modules/util/helpers'; import UIEvents from './service/UI/UIEvents'; import UIUtil from './modules/UI/util/UIUtil'; +import * as JitsiMeetConferenceEvents from './ConferenceEvents'; import analytics from './modules/analytics/analytics'; +import EventEmitter from "events"; + const ConnectionEvents = JitsiMeetJS.events.connection; const ConnectionErrors = JitsiMeetJS.errors.connection; @@ -28,6 +31,8 @@ const TrackErrors = JitsiMeetJS.errors.track; const ConnectionQualityEvents = JitsiMeetJS.events.connectionQuality; +const eventEmitter = new EventEmitter(); + let room, connection, localAudio, localVideo; /** @@ -485,10 +490,11 @@ export default { }).then(([tracks, con]) => { logger.log('initialized with %s local tracks', tracks.length); APP.connection = connection = con; - this._bindConnectionFailedHandler(con); - this._createRoom(tracks); this.isDesktopSharingEnabled = JitsiMeetJS.isDesktopSharingEnabled(); + APP.remoteControl.init(); + this._bindConnectionFailedHandler(con); + this._createRoom(tracks); if (UIUtil.isButtonEnabled('contacts') && !interfaceConfig.filmStripOnly) { @@ -981,7 +987,7 @@ export default { let externalInstallation = false; if (shareScreen) { - createLocalTracks({ + this.screenSharingPromise = createLocalTracks({ devices: ['desktop'], desktopSharingExtensionExternalInstallation: { interval: 500, @@ -1070,7 +1076,10 @@ export default { dialogTitleKey, dialogTxt, false); }); } else { - createLocalTracks({ devices: ['video'] }).then( + APP.remoteControl.receiver.stop(); + this.screenSharingPromise = createLocalTracks( + { devices: ['video'] }) + .then( ([stream]) => this.useVideoStream(stream) ).then(() => { this.videoSwitchInProgress = false; @@ -1102,6 +1111,8 @@ export default { } ); + room.on(ConferenceEvents.PARTCIPANT_FEATURES_CHANGED, + user => APP.UI.onUserFeaturesChanged(user)); room.on(ConferenceEvents.USER_JOINED, (id, user) => { if (user.isHidden()) return; @@ -1600,12 +1611,23 @@ export default { }, /** * Adds any room listener. - * @param eventName one of the ConferenceEvents - * @param callBack the function to be called when the event occurs + * @param {string} eventName one of the ConferenceEvents + * @param {Function} listener the function to be called when the event + * occurs */ - addConferenceListener(eventName, callBack) { - room.on(eventName, callBack); + addConferenceListener(eventName, listener) { + room.on(eventName, listener); }, + + /** + * Removes any room listener. + * @param {string} eventName one of the ConferenceEvents + * @param {Function} listener the listener to be removed. + */ + removeConferenceListener(eventName, listener) { + room.off(eventName, listener); + }, + /** * Inits list of current devices and event listener for device change. * @private @@ -1763,6 +1785,7 @@ export default { * requested */ hangup (requestFeedback = false) { + eventEmitter.emit(JitsiMeetConferenceEvents.BEFORE_HANGUP); APP.UI.hideRingOverLay(); let requestFeedbackPromise = requestFeedback ? APP.UI.requestFeedbackOnHangup() @@ -1813,5 +1836,36 @@ export default { APP.settings.setAvatarUrl(url); APP.UI.setUserAvatarUrl(room.myUserId(), url); sendData(commands.AVATAR_URL, url); + }, + + /** + * Sends a message via the data channel. + * @param {string} to the id of the endpoint that should receive the + * message. If "" - the message will be sent to all participants. + * @param {object} payload the payload of the message. + * @throws NetworkError or InvalidStateError or Error if the operation + * fails. + */ + sendEndpointMessage (to, payload) { + room.sendEndpointMessage(to, payload); + }, + + /** + * Adds new listener. + * @param {String} eventName the name of the event + * @param {Function} listener the listener. + */ + addListener (eventName, listener) { + eventEmitter.addListener(eventName, listener); + }, + + /** + * Removes listener. + * @param {String} eventName the name of the event that triggers the + * listener + * @param {Function} listener the listener. + */ + removeListener (eventName, listener) { + eventEmitter.removeListener(eventName, listener); } }; diff --git a/css/_filmstrip.scss b/css/_filmstrip.scss index dddbc886e..1b6dd089e 100644 --- a/css/_filmstrip.scss +++ b/css/_filmstrip.scss @@ -92,7 +92,7 @@ 0 0 3px $videoThumbnailSelected !important; } - .remotevideomenu { + .remotevideomenu > .icon-menu { display: none; } @@ -105,7 +105,7 @@ box-shadow: inset 0 0 3px $videoThumbnailHovered, 0 0 3px $videoThumbnailHovered; - .remotevideomenu { + .remotevideomenu > .icon-menu { display: inline-block; } } @@ -121,4 +121,4 @@ } } } -} \ No newline at end of file +} diff --git a/css/_popup_menu.scss b/css/_popup_menu.scss index f129321e0..2283926d4 100644 --- a/css/_popup_menu.scss +++ b/css/_popup_menu.scss @@ -6,7 +6,6 @@ padding: 0; margin: 2px 0; bottom: 0; - width: 100px; height: auto; &:first-child { @@ -66,4 +65,9 @@ span.remotevideomenu:hover ul.popupmenu, ul.popupmenu:hover { display:block !important; -} \ No newline at end of file +} + +.remote-control-spinner { + top: 6px; + left: 2px; +} diff --git a/lang/main.json b/lang/main.json index 2dc1487fd..3e398633a 100644 --- a/lang/main.json +++ b/lang/main.json @@ -156,8 +156,8 @@ "kick": "Kick out", "muted": "Muted", "domute": "Mute", - "flip": "Flip" - + "flip": "Flip", + "remoteControl": "Remote control" }, "connectionindicator": { @@ -316,7 +316,12 @@ "externalInstallationMsg": "You need to install our desktop sharing extension.", "muteParticipantTitle": "Mute this participant?", "muteParticipantBody": "You won't be able to unmute them, but they can unmute themselves at any time.", - "muteParticipantButton": "Mute" + "muteParticipantButton": "Mute", + "remoteControlTitle": "Remote Control", + "remoteControlDeniedMessage": "__user__ rejected your remote control request!", + "remoteControlAllowedMessage": "__user__ accepted your remote control request!", + "remoteControlErrorMessage": "An error occurred while trying to request remote control permissions from __user__!", + "remoteControlStopMessage": "The remote control session ended!" }, "email": { diff --git a/modules/API/API.js b/modules/API/API.js index fc2ce7bc4..8417e7db2 100644 --- a/modules/API/API.js +++ b/modules/API/API.js @@ -55,7 +55,9 @@ function initCommands() { APP.conference.toggleScreenSharing.bind(APP.conference), "video-hangup": () => APP.conference.hangup(), "email": APP.conference.changeLocalEmail, - "avatar-url": APP.conference.changeLocalAvatarUrl + "avatar-url": APP.conference.changeLocalAvatarUrl, + "remote-control-event": event => + APP.remoteControl.onRemoteControlAPIEvent(event) }; Object.keys(commands).forEach(function (key) { postis.listen(key, args => commands[key](...args)); @@ -94,7 +96,13 @@ function triggerEvent (name, object) { } } -export default { +class API { + /** + * Constructs new instance + * @constructor + */ + constructor() { } + /** * Initializes the APIConnector. Setups message event listeners that will * receive information from external applications that embed Jitsi Meet. @@ -108,6 +116,17 @@ export default { return; enabled = true; + + if(!postis) { + this._initPostis(); + } + } + + /** + * initializes postis library. + * @private + */ + _initPostis() { let postisOptions = { window: target }; @@ -116,7 +135,7 @@ export default { = "jitsi_meet_external_api_" + jitsi_meet_external_api_id; postis = postisInit(postisOptions); initCommands(); - }, + } /** * Notify external application (if API is enabled) that message was sent. @@ -124,7 +143,7 @@ export default { */ notifySendingChatMessage (body) { triggerEvent("outgoing-message", {"message": body}); - }, + } /** * Notify external application (if API is enabled) that @@ -143,7 +162,7 @@ export default { "incoming-message", {"from": id, "nick": nick, "message": body, "stamp": ts} ); - }, + } /** * Notify external application (if API is enabled) that @@ -152,7 +171,7 @@ export default { */ notifyUserJoined (id) { triggerEvent("participant-joined", {id}); - }, + } /** * Notify external application (if API is enabled) that @@ -161,7 +180,7 @@ export default { */ notifyUserLeft (id) { triggerEvent("participant-left", {id}); - }, + } /** * Notify external application (if API is enabled) that @@ -171,7 +190,7 @@ export default { */ notifyDisplayNameChanged (id, displayName) { triggerEvent("display-name-change", {id, displayname: displayName}); - }, + } /** * Notify external application (if API is enabled) that @@ -181,7 +200,7 @@ export default { */ notifyConferenceJoined (room) { triggerEvent("video-conference-joined", {roomName: room}); - }, + } /** * Notify external application (if API is enabled) that @@ -191,7 +210,7 @@ export default { */ notifyConferenceLeft (room) { triggerEvent("video-conference-left", {roomName: room}); - }, + } /** * Notify external application (if API is enabled) that @@ -199,13 +218,23 @@ export default { */ notifyReadyToClose () { triggerEvent("video-ready-to-close", {}); - }, + } + + /** + * Sends remote control event. + * @param {RemoteControlEvent} event the remote control event. + */ + sendRemoteControlEvent(event) { + sendMessage({method: "remote-control-event", params: event}); + } /** * Removes the listeners. */ - dispose: function () { + dispose () { if(enabled) postis.destroy(); } -}; +} + +export default new API(); diff --git a/modules/UI/UI.js b/modules/UI/UI.js index 75752268f..624b5c1ba 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -1441,4 +1441,11 @@ UI.hideUserMediaPermissionsGuidanceOverlay = function () { GumPermissionsOverlay.hide(); }; +/** + * Handles user's features changes. + */ +UI.onUserFeaturesChanged = function (user) { + VideoLayout.onUserFeaturesChanged(user); +}; + module.exports = UI; diff --git a/modules/UI/videolayout/LargeVideoManager.js b/modules/UI/videolayout/LargeVideoManager.js index 225370ddf..0a52afb01 100644 --- a/modules/UI/videolayout/LargeVideoManager.js +++ b/modules/UI/videolayout/LargeVideoManager.js @@ -3,6 +3,7 @@ const logger = require("jitsi-meet-logger").getLogger(__filename); import Avatar from "../avatar/Avatar"; import {createDeferred} from '../../util/helpers'; +import UIEvents from "../../../service/UI/UIEvents"; import UIUtil from "../util/UIUtil"; import {VideoContainer, VIDEO_CONTAINER_TYPE} from "./VideoContainer"; @@ -19,6 +20,7 @@ export default class LargeVideoManager { * @type {Object.} */ this.containers = {}; + this.eventEmitter = emitter; this.state = VIDEO_CONTAINER_TYPE; this.videoContainer = new VideoContainer( @@ -164,6 +166,7 @@ export default class LargeVideoManager { // after everything is done check again if there are any pending // new streams. this.updateInProcess = false; + this.eventEmitter.emit(UIEvents.LARGE_VIDEO_ID_CHANGED, this.id); this.scheduleLargeVideoUpdate(); }); } diff --git a/modules/UI/videolayout/RemoteVideo.js b/modules/UI/videolayout/RemoteVideo.js index ac156ddbd..21b780f3b 100644 --- a/modules/UI/videolayout/RemoteVideo.js +++ b/modules/UI/videolayout/RemoteVideo.js @@ -29,6 +29,7 @@ function RemoteVideo(user, VideoLayout, emitter) { this.videoSpanId = `participant_${this.id}`; SmallVideo.call(this, VideoLayout); this.hasRemoteVideoMenu = false; + this._supportsRemoteControl = false; this.addRemoteVideoContainer(); this.connectionIndicator = new ConnectionIndicator(this, this.id); this.setDisplayName(); @@ -64,7 +65,7 @@ RemoteVideo.prototype.addRemoteVideoContainer = function() { this.initBrowserSpecificProperties(); - if (APP.conference.isModerator) { + if (APP.conference.isModerator || this._supportsRemoteControl) { this.addRemoteVideoMenu(); } @@ -106,14 +107,6 @@ RemoteVideo.prototype._initPopupMenu = function (popupMenuElement) { // call the original show, passing its actual this origShowFunc.call(this.popover); }.bind(this); - - // override popover hide method so we can cleanup click handlers - let origHideFunc = this.popover.forceHide; - this.popover.forceHide = function () { - $(document).off("click", '#mutelink_' + this.id); - $(document).off("click", '#ejectlink_' + this.id); - origHideFunc.call(this.popover); - }.bind(this); }; /** @@ -139,38 +132,68 @@ RemoteVideo.prototype._generatePopupContent = function () { let popupmenuElement = document.createElement('ul'); popupmenuElement.className = 'popupmenu'; popupmenuElement.id = `remote_popupmenu_${this.id}`; + let menuItems = []; - let muteTranslationKey; - let muteClassName; - if (this.isAudioMuted) { - muteTranslationKey = 'videothumbnail.muted'; - muteClassName = 'mutelink disabled'; - } else { - muteTranslationKey = 'videothumbnail.domute'; - muteClassName = 'mutelink'; + if(APP.conference.isModerator) { + let muteTranslationKey; + let muteClassName; + if (this.isAudioMuted) { + muteTranslationKey = 'videothumbnail.muted'; + muteClassName = 'mutelink disabled'; + } else { + muteTranslationKey = 'videothumbnail.domute'; + muteClassName = 'mutelink'; + } + + let muteHandler = this._muteHandler.bind(this); + let kickHandler = this._kickHandler.bind(this); + + menuItems = [ + { + id: 'mutelink_' + this.id, + handler: muteHandler, + icon: 'icon-mic-disabled', + className: muteClassName, + data: { + i18n: muteTranslationKey + } + }, { + id: 'ejectlink_' + this.id, + handler: kickHandler, + icon: 'icon-kick', + data: { + i18n: 'videothumbnail.kick' + } + } + ]; } - let muteHandler = this._muteHandler.bind(this); - let kickHandler = this._kickHandler.bind(this); - - let menuItems = [ - { - id: 'mutelink_' + this.id, - handler: muteHandler, - icon: 'icon-mic-disabled', - className: muteClassName, - data: { - i18n: muteTranslationKey - } - }, { - id: 'ejectlink_' + this.id, - handler: kickHandler, - icon: 'icon-kick', - data: { - i18n: 'videothumbnail.kick' - } + if(this._supportsRemoteControl) { + let icon, handler, className; + if(APP.remoteControl.controller.getRequestedParticipant() + === this.id) { + handler = () => {}; + className = "requestRemoteControlLink disabled"; + icon = "remote-control-spinner fa fa-spinner fa-spin"; + } else if(!APP.remoteControl.controller.isStarted()) { + handler = this._requestRemoteControlPermissions.bind(this); + icon = "fa fa-play"; + className = "requestRemoteControlLink"; + } else { + handler = this._stopRemoteControl.bind(this); + icon = "fa fa-stop"; + className = "requestRemoteControlLink"; } - ]; + menuItems.push({ + id: 'remoteControl_' + this.id, + handler, + icon, + className, + data: { + i18n: 'videothumbnail.remoteControl' + } + }); + } menuItems.forEach(el => { let menuItem = this._generatePopupMenuItem(el); @@ -182,6 +205,76 @@ RemoteVideo.prototype._generatePopupContent = function () { return popupmenuElement; }; +/** + * Sets the remote control supported value and initializes or updates the menu + * depending on the remote control is supported or not. + * @param {boolean} isSupported + */ +RemoteVideo.prototype.setRemoteControlSupport = function(isSupported = false) { + if(this._supportsRemoteControl === isSupported) { + return; + } + this._supportsRemoteControl = isSupported; + if(!isSupported) { + return; + } + + if(!this.hasRemoteVideoMenu) { + //create menu + this.addRemoteVideoMenu(); + } else { + //update the content + this.updateRemoteVideoMenu(this.isAudioMuted, true); + } + +}; + +/** + * Requests permissions for remote control session. + */ +RemoteVideo.prototype._requestRemoteControlPermissions = function () { + APP.remoteControl.controller.requestPermissions( + this.id, this.VideoLayout.getLargeVideoWrapper()).then(result => { + if(result === null) { + return; + } + this.updateRemoteVideoMenu(this.isAudioMuted, true); + APP.UI.messageHandler.openMessageDialog( + "dialog.remoteControlTitle", + (result === false) ? "dialog.remoteControlDeniedMessage" + : "dialog.remoteControlAllowedMessage", + {user: this.user.getDisplayName() + || interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME} + ); + if(result === true) {//the remote control permissions has been granted + // pin the controlled participant + let pinnedId = this.VideoLayout.getPinnedId(); + if(pinnedId !== this.id) { + this.VideoLayout.handleVideoThumbClicked(this.id); + } + } + }, error => { + logger.error(error); + this.updateRemoteVideoMenu(this.isAudioMuted, true); + APP.UI.messageHandler.openMessageDialog( + "dialog.remoteControlTitle", + "dialog.remoteControlErrorMessage", + {user: this.user.getDisplayName() + || interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME} + ); + }); + this.updateRemoteVideoMenu(this.isAudioMuted, true); +}; + +/** + * Stops remote control session. + */ +RemoteVideo.prototype._stopRemoteControl = function () { + // send message about stopping + APP.remoteControl.controller.stop(); + this.updateRemoteVideoMenu(this.isAudioMuted, true); +}; + RemoteVideo.prototype._muteHandler = function () { if (this.isAudioMuted) return; @@ -244,8 +337,7 @@ RemoteVideo.prototype._generatePopupMenuItem = function (opts = {}) { linkItem.appendChild(textContent); linkItem.id = id; - // Delegate event to the document. - $(document).on("click", `#${id}`, handler); + linkItem.onclick = handler; menuItem.appendChild(linkItem); return menuItem; diff --git a/modules/UI/videolayout/VideoLayout.js b/modules/UI/videolayout/VideoLayout.js index 9f80bc017..cc4911deb 100644 --- a/modules/UI/videolayout/VideoLayout.js +++ b/modules/UI/videolayout/VideoLayout.js @@ -406,6 +406,7 @@ var VideoLayout = { remoteVideo = smallVideo; else remoteVideo = new RemoteVideo(user, VideoLayout, eventEmitter); + this._setRemoteControlProperties(user, remoteVideo); this.addRemoteVideoContainer(id, remoteVideo); }, @@ -1158,12 +1159,44 @@ var VideoLayout = { * Sets the flipX state of the local video. * @param {boolean} true for flipped otherwise false; */ - setLocalFlipX: function (val) { + setLocalFlipX (val) { this.localFlipX = val; - }, - getEventEmitter: () => {return eventEmitter;} + getEventEmitter() {return eventEmitter;}, + + /** + * Handles user's features changes. + */ + onUserFeaturesChanged (user) { + let video = this.getSmallVideo(user.getId()); + + if (!video) { + return; + } + this._setRemoteControlProperties(user, video); + }, + + /** + * Sets the remote control properties (checks whether remote control + * is supported and executes remoteVideo.setRemoteControlSupport). + * @param {JitsiParticipant} user the user that will be checked for remote + * control support. + * @param {RemoteVideo} remoteVideo the remoteVideo on which the properties + * will be set. + */ + _setRemoteControlProperties (user, remoteVideo) { + APP.remoteControl.checkUserRemoteControlSupport(user).then(result => + remoteVideo.setRemoteControlSupport(result)); + }, + + /** + * Returns the wrapper jquery selector for the largeVideo + * @returns {JQuerySelector} the wrapper jquery selector for the largeVideo + */ + getLargeVideoWrapper() { + return this.getCurrentlyOnLargeContainer().$wrapper; + } }; export default VideoLayout; diff --git a/modules/keyboardshortcut/keyboardshortcut.js b/modules/keyboardshortcut/keyboardshortcut.js index 53497347a..1eac7b6bf 100644 --- a/modules/keyboardshortcut/keyboardshortcut.js +++ b/modules/keyboardshortcut/keyboardshortcut.js @@ -65,6 +65,12 @@ function showKeyboardShortcutsPanel(show) { */ let _shortcuts = {}; +/** + * True if the keyboard shortcuts are enabled and false if not. + * @type {boolean} + */ +let enabled = true; + /** * Maps keycode to character, id of popover for given function and function. */ @@ -74,6 +80,9 @@ var KeyboardShortcut = { var self = this; window.onkeyup = function(e) { + if(!enabled) { + return; + } var key = self._getKeyboardKey(e).toUpperCase(); var num = parseInt(key, 10); if(!($(":focus").is("input[type=text]") || @@ -93,6 +102,9 @@ var KeyboardShortcut = { }; window.onkeydown = function(e) { + if(!enabled) { + return; + } if(!($(":focus").is("input[type=text]") || $(":focus").is("input[type=password]") || $(":focus").is("textarea"))) { @@ -105,6 +117,14 @@ var KeyboardShortcut = { }; }, + /** + * Enables/Disables the keyboard shortcuts. + * @param {boolean} value - the new value. + */ + enable: function (value) { + enabled = value; + }, + /** * Registers a new shortcut. * diff --git a/modules/keycode/keycode.js b/modules/keycode/keycode.js new file mode 100644 index 000000000..38c6e0348 --- /dev/null +++ b/modules/keycode/keycode.js @@ -0,0 +1,163 @@ +/** + * Enumerates the supported keys. + * NOTE: The maps represents physical keys on the keyboard, not chars. + * @readonly + * @enum {string} + */ +export const KEYS = { + BACKSPACE: "backspace" , + DELETE : "delete", + RETURN : "enter", + TAB : "tab", + ESCAPE : "escape", + UP : "up", + DOWN : "down", + RIGHT : "right", + LEFT : "left", + HOME : "home", + END : "end", + PAGEUP : "pageup", + PAGEDOWN : "pagedown", + + F1 : "f1", + F2 : "f2", + F3 : "f3", + F4 : "f4", + F5 : "f5", + F6 : "f6", + F7 : "f7", + F8 : "f8", + F9 : "f9", + F10 : "f10", + F11 : "f11", + F12 : "f12", + META : "command", + CMD_L: "command", + CMD_R: "command", + ALT : "alt", + CONTROL : "control", + SHIFT : "shift", + CAPS_LOCK: "caps_lock", //not supported by robotjs + SPACE : "space", + PRINTSCREEN : "printscreen", + INSERT : "insert", + + NUMPAD_0 : "numpad_0", + NUMPAD_1 : "numpad_1", + NUMPAD_2 : "numpad_2", + NUMPAD_3 : "numpad_3", + NUMPAD_4 : "numpad_4", + NUMPAD_5 : "numpad_5", + NUMPAD_6 : "numpad_6", + NUMPAD_7 : "numpad_7", + NUMPAD_8 : "numpad_8", + NUMPAD_9 : "numpad_9", + + COMMA: ",", + + PERIOD: ".", + SEMICOLON: ";", + QUOTE: "'", + BRACKET_LEFT: "[", + BRACKET_RIGHT: "]", + BACKQUOTE: "`", + BACKSLASH: "\\", + MINUS: "-", + EQUAL: "=", + SLASH: "/" +}; + +/** + * Mapping between the key codes and keys deined in KEYS. + * The mappings are based on + * https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode#Specifications + */ +let keyCodeToKey = { + 8: KEYS.BACKSPACE, + 9: KEYS.TAB, + 13: KEYS.RETURN, + 16: KEYS.SHIFT, + 17: KEYS.CONTROL, + 18: KEYS.ALT, + 20: KEYS.CAPS_LOCK, + 27: KEYS.ESCAPE, + 32: KEYS.SPACE, + 33: KEYS.PAGEUP, + 34: KEYS.PAGEDOWN, + 35: KEYS.END, + 36: KEYS.HOME, + 37: KEYS.LEFT, + 38: KEYS.UP, + 39: KEYS.RIGHT, + 40: KEYS.DOWN, + 42: KEYS.PRINTSCREEN, + 44: KEYS.PRINTSCREEN, + 45: KEYS.INSERT, + 46: KEYS.DELETE, + 59: KEYS.SEMICOLON, + 61: KEYS.EQUAL, + 91: KEYS.CMD_L, + 92: KEYS.CMD_R, + 93: KEYS.CMD_R, + 96: KEYS.NUMPAD_0, + 97: KEYS.NUMPAD_1, + 98: KEYS.NUMPAD_2, + 99: KEYS.NUMPAD_3, + 100: KEYS.NUMPAD_4, + 101: KEYS.NUMPAD_5, + 102: KEYS.NUMPAD_6, + 103: KEYS.NUMPAD_7, + 104: KEYS.NUMPAD_8, + 105: KEYS.NUMPAD_9, + 112: KEYS.F1, + 113: KEYS.F2, + 114: KEYS.F3, + 115: KEYS.F4, + 116: KEYS.F5, + 117: KEYS.F6, + 118: KEYS.F7, + 119: KEYS.F8, + 120: KEYS.F9, + 121: KEYS.F10, + 122: KEYS.F11, + 123: KEYS.F12, + 124: KEYS.PRINTSCREEN, + 173: KEYS.MINUS, + 186: KEYS.SEMICOLON, + 187: KEYS.EQUAL, + 188: KEYS.COMMA, + 189: KEYS.MINUS, + 190: KEYS.PERIOD, + 191: KEYS.SLASH, + 192: KEYS.BACKQUOTE, + 219: KEYS.BRACKET_LEFT, + 220: KEYS.BACKSLASH, + 221: KEYS.BRACKET_RIGHT, + 222: KEYS.QUOTE, + 224: KEYS.META, + 229: KEYS.SEMICOLON +}; + +/** + * Generate codes for digit keys (0-9) + */ +for(let i = 0; i < 10; i++) { + keyCodeToKey[i + 48] = `${i}`; +} + +/** + * Generate codes for letter keys (a-z) + */ +for(let i = 0; i < 26; i++) { + let keyCode = i + 65; + keyCodeToKey[keyCode] = String.fromCharCode(keyCode).toLowerCase(); +} + +/** + * Returns key associated with the keyCode from the passed event. + * @param {KeyboardEvent} event the event + * @returns {KEYS} the key on the keyboard. + */ +export function keyboardEventToKey(event) { + return keyCodeToKey[event.which]; +} diff --git a/modules/remotecontrol/Controller.js b/modules/remotecontrol/Controller.js new file mode 100644 index 000000000..44f338198 --- /dev/null +++ b/modules/remotecontrol/Controller.js @@ -0,0 +1,374 @@ +/* global $, JitsiMeetJS, APP */ +const logger = require("jitsi-meet-logger").getLogger(__filename); +import * as KeyCodes from "../keycode/keycode"; +import {EVENT_TYPES, REMOTE_CONTROL_EVENT_TYPE, PERMISSIONS_ACTIONS} + from "../../service/remotecontrol/Constants"; +import RemoteControlParticipant from "./RemoteControlParticipant"; +import UIEvents from "../../service/UI/UIEvents"; + +const ConferenceEvents = JitsiMeetJS.events.conference; + +/** + * Extract the keyboard key from the keyboard event. + * @param event {KeyboardEvent} the event. + * @returns {KEYS} the key that is pressed or undefined. + */ +function getKey(event) { + return KeyCodes.keyboardEventToKey(event); +} + +/** + * Extract the modifiers from the keyboard event. + * @param event {KeyboardEvent} the event. + * @returns {Array} with possible values: "shift", "control", "alt", "command". + */ +function getModifiers(event) { + let modifiers = []; + if(event.shiftKey) { + modifiers.push("shift"); + } + + if(event.ctrlKey) { + modifiers.push("control"); + } + + + if(event.altKey) { + modifiers.push("alt"); + } + + if(event.metaKey) { + modifiers.push("command"); + } + + return modifiers; +} + +/** + * This class represents the controller party for a remote controller session. + * It listens for mouse and keyboard events and sends them to the receiver + * party of the remote control session. + */ +export default class Controller extends RemoteControlParticipant { + /** + * Creates new instance. + */ + constructor() { + super(); + this.isCollectingEvents = false; + this.controlledParticipant = null; + this.requestedParticipant = null; + this._stopListener = this._handleRemoteControlStoppedEvent.bind(this); + this._userLeftListener = this._onUserLeft.bind(this); + this._largeVideoChangedListener + = this._onLargeVideoIdChanged.bind(this); + } + + /** + * Requests permissions from the remote control receiver side. + * @param {string} userId the user id of the participant that will be + * requested. + * @param {JQuerySelector} eventCaptureArea the area that is going to be + * used mouse and keyboard event capture. + * @returns {Promise} - resolve values: + * true - accept + * false - deny + * null - the participant has left. + */ + requestPermissions(userId, eventCaptureArea) { + if(!this.enabled) { + return Promise.reject(new Error("Remote control is disabled!")); + } + this.area = eventCaptureArea;// $("#largeVideoWrapper") + logger.log("Requsting remote control permissions from: " + userId); + return new Promise((resolve, reject) => { + const clearRequest = () => { + this.requestedParticipant = null; + APP.conference.removeConferenceListener( + ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, + permissionsReplyListener); + APP.conference.removeConferenceListener( + ConferenceEvents.USER_LEFT, + onUserLeft); + }; + const permissionsReplyListener = (participant, event) => { + let result = null; + try { + result = this._handleReply(participant, event); + } catch (e) { + reject(e); + } + if(result !== null) { + clearRequest(); + resolve(result); + } + }; + const onUserLeft = (id) => { + if(id === this.requestedParticipant) { + clearRequest(); + resolve(null); + } + }; + APP.conference.addConferenceListener( + ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, + permissionsReplyListener); + APP.conference.addConferenceListener(ConferenceEvents.USER_LEFT, + onUserLeft); + this.requestedParticipant = userId; + this._sendRemoteControlEvent(userId, { + type: EVENT_TYPES.permissions, + action: PERMISSIONS_ACTIONS.request + }, e => { + clearRequest(); + reject(e); + }); + }); + } + + /** + * Handles the reply of the permissions request. + * @param {JitsiParticipant} participant the participant that has sent the + * reply + * @param {RemoteControlEvent} event the remote control event. + */ + _handleReply(participant, event) { + const remoteControlEvent = event.event; + const userId = participant.getId(); + if(this.enabled && event.type === REMOTE_CONTROL_EVENT_TYPE + && remoteControlEvent.type === EVENT_TYPES.permissions + && userId === this.requestedParticipant) { + if(remoteControlEvent.action !== PERMISSIONS_ACTIONS.grant) { + this.area = null; + } + switch(remoteControlEvent.action) { + case PERMISSIONS_ACTIONS.grant: { + this.controlledParticipant = userId; + logger.log("Remote control permissions granted to: " + + userId); + this._start(); + return true; + } + case PERMISSIONS_ACTIONS.deny: + return false; + case PERMISSIONS_ACTIONS.error: + throw new Error("Error occurred on receiver side"); + default: + throw new Error("Unknown reply received!"); + } + } else { + //different message type or another user -> ignoring the message + return null; + } + } + + /** + * Handles remote control stopped. + * @param {JitsiParticipant} participant the participant that has sent the + * event + * @param {Object} event EndpointMessage event from the data channels. + * @property {string} type property. The function process only events of + * type REMOTE_CONTROL_EVENT_TYPE + * @property {RemoteControlEvent} event - the remote control event. + */ + _handleRemoteControlStoppedEvent(participant, event) { + if(this.enabled && event.type === REMOTE_CONTROL_EVENT_TYPE + && event.event.type === EVENT_TYPES.stop + && participant.getId() === this.controlledParticipant) { + this._stop(); + } + } + + /** + * Starts processing the mouse and keyboard events. Sets conference + * listeners. Disables keyboard events. + */ + _start() { + logger.log("Starting remote control controller."); + APP.UI.addListener(UIEvents.LARGE_VIDEO_ID_CHANGED, + this._largeVideoChangedListener); + APP.conference.addConferenceListener( + ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, + this._stopListener); + APP.conference.addConferenceListener(ConferenceEvents.USER_LEFT, + this._userLeftListener); + this.resume(); + } + + /** + * Disables the keyboatd shortcuts. Starts collecting remote control + * events. + * + * It can be used to resume an active remote control session wchich was + * paused with this.pause(). + */ + resume() { + if(!this.enabled || this.isCollectingEvents) { + return; + } + logger.log("Resuming remote control controller."); + this.isCollectingEvents = true; + APP.keyboardshortcut.enable(false); + this.area.mousemove(event => { + const position = this.area.position(); + this._sendRemoteControlEvent(this.controlledParticipant, { + type: EVENT_TYPES.mousemove, + x: (event.pageX - position.left)/this.area.width(), + y: (event.pageY - position.top)/this.area.height() + }); + }); + this.area.mousedown(this._onMouseClickHandler.bind(this, + EVENT_TYPES.mousedown)); + this.area.mouseup(this._onMouseClickHandler.bind(this, + EVENT_TYPES.mouseup)); + this.area.dblclick( + this._onMouseClickHandler.bind(this, EVENT_TYPES.mousedblclick)); + this.area.contextmenu(() => false); + this.area[0].onmousewheel = event => { + this._sendRemoteControlEvent(this.controlledParticipant, { + type: EVENT_TYPES.mousescroll, + x: event.deltaX, + y: event.deltaY + }); + }; + $(window).keydown(this._onKeyPessHandler.bind(this, + EVENT_TYPES.keydown)); + $(window).keyup(this._onKeyPessHandler.bind(this, EVENT_TYPES.keyup)); + } + + /** + * Stops processing the mouse and keyboard events. Removes added listeners. + * Enables the keyboard shortcuts. Displays dialog to notify the user that + * remote control session has ended. + */ + _stop() { + if(!this.controlledParticipant) { + return; + } + logger.log("Stopping remote control controller."); + APP.UI.removeListener(UIEvents.LARGE_VIDEO_ID_CHANGED, + this._largeVideoChangedListener); + APP.conference.removeConferenceListener( + ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, + this._stopListener); + APP.conference.removeConferenceListener(ConferenceEvents.USER_LEFT, + this._userLeftListener); + this.controlledParticipant = null; + this.pause(); + this.area = null; + APP.UI.messageHandler.openMessageDialog( + "dialog.remoteControlTitle", + "dialog.remoteControlStopMessage" + ); + } + + /** + * Executes this._stop() mehtod: + * Stops processing the mouse and keyboard events. Removes added listeners. + * Enables the keyboard shortcuts. Displays dialog to notify the user that + * remote control session has ended. + * + * In addition: + * Sends stop message to the controlled participant. + */ + stop() { + if(!this.controlledParticipant) { + return; + } + this._sendRemoteControlEvent(this.controlledParticipant, { + type: EVENT_TYPES.stop + }); + this._stop(); + } + + /** + * Pauses the collecting of events and enables the keyboard shortcus. But + * it doesn't removes any other listeners. Basically the remote control + * session will be still active after this.pause(), but no events from the + * controller side will be captured and sent. + * + * You can resume the collecting of the events with this.resume(). + */ + pause() { + if(!this.controlledParticipant) { + return; + } + logger.log("Pausing remote control controller."); + this.isCollectingEvents = false; + APP.keyboardshortcut.enable(true); + this.area.off( "mousemove" ); + this.area.off( "mousedown" ); + this.area.off( "mouseup" ); + this.area.off( "contextmenu" ); + this.area.off( "dblclick" ); + $(window).off( "keydown"); + $(window).off( "keyup"); + this.area[0].onmousewheel = undefined; + } + + /** + * Handler for mouse click events. + * @param {String} type the type of event ("mousedown"/"mouseup") + * @param {Event} event the mouse event. + */ + _onMouseClickHandler(type, event) { + this._sendRemoteControlEvent(this.controlledParticipant, { + type: type, + button: event.which + }); + } + + /** + * Returns true if the remote control session is started. + * @returns {boolean} + */ + isStarted() { + return this.controlledParticipant !== null; + } + + /** + * Returns the id of the requested participant + * @returns {string} this.requestedParticipant. + * NOTE: This id should be the result of JitsiParticipant.getId() call. + */ + getRequestedParticipant() { + return this.requestedParticipant; + } + + /** + * Handler for key press events. + * @param {String} type the type of event ("keydown"/"keyup") + * @param {Event} event the key event. + */ + _onKeyPessHandler(type, event) { + this._sendRemoteControlEvent(this.controlledParticipant, { + type: type, + key: getKey(event), + modifiers: getModifiers(event), + }); + } + + /** + * Calls the stop method if the other side have left. + * @param {string} id - the user id for the participant that have left + */ + _onUserLeft(id) { + if(this.controlledParticipant === id) { + this._stop(); + } + } + + /** + * Handles changes of the participant displayed on the large video. + * @param {string} id - the user id for the participant that is displayed. + */ + _onLargeVideoIdChanged(id) { + if (!this.controlledParticipant) { + return; + } + if(this.controlledParticipant == id) { + this.resume(); + } else { + this.pause(); + } + } +} diff --git a/modules/remotecontrol/Receiver.js b/modules/remotecontrol/Receiver.js new file mode 100644 index 000000000..cf6063617 --- /dev/null +++ b/modules/remotecontrol/Receiver.js @@ -0,0 +1,192 @@ +/* global APP, JitsiMeetJS, interfaceConfig */ +const logger = require("jitsi-meet-logger").getLogger(__filename); +import {DISCO_REMOTE_CONTROL_FEATURE, REMOTE_CONTROL_EVENT_TYPE, EVENT_TYPES, + PERMISSIONS_ACTIONS} from "../../service/remotecontrol/Constants"; +import RemoteControlParticipant from "./RemoteControlParticipant"; +import * as JitsiMeetConferenceEvents from '../../ConferenceEvents'; + +const ConferenceEvents = JitsiMeetJS.events.conference; + +/** + * This class represents the receiver party for a remote controller session. + * It handles "remote-control-event" events and sends them to the + * API module. From there the events can be received from wrapper application + * and executed. + */ +export default class Receiver extends RemoteControlParticipant { + /** + * Creates new instance. + * @constructor + */ + constructor() { + super(); + this.controller = null; + this._remoteControlEventsListener + = this._onRemoteControlEvent.bind(this); + this._userLeftListener = this._onUserLeft.bind(this); + this._hangupListener = this._onHangup.bind(this); + } + + /** + * Enables / Disables the remote control + * @param {boolean} enabled the new state. + */ + enable(enabled) { + if(this.enabled === enabled) { + return; + } + this.enabled = enabled; + if(enabled === true) { + logger.log("Remote control receiver enabled."); + // Announce remote control support. + APP.connection.addFeature(DISCO_REMOTE_CONTROL_FEATURE, true); + APP.conference.addConferenceListener( + ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, + this._remoteControlEventsListener); + APP.conference.addListener(JitsiMeetConferenceEvents.BEFORE_HANGUP, + this._hangupListener); + } else { + logger.log("Remote control receiver disabled."); + this._stop(true); + APP.connection.removeFeature(DISCO_REMOTE_CONTROL_FEATURE); + APP.conference.removeConferenceListener( + ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, + this._remoteControlEventsListener); + APP.conference.removeListener( + JitsiMeetConferenceEvents.BEFORE_HANGUP, + this._hangupListener); + } + } + + /** + * Removes the listener for ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED + * events. Sends stop message to the wrapper application. Optionally + * displays dialog for informing the user that remote control session + * ended. + * @param {boolean} dontShowDialog - if true the dialog won't be displayed. + */ + _stop(dontShowDialog = false) { + if(!this.controller) { + return; + } + logger.log("Remote control receiver stop."); + this.controller = null; + APP.conference.removeConferenceListener(ConferenceEvents.USER_LEFT, + this._userLeftListener); + APP.API.sendRemoteControlEvent({ + type: EVENT_TYPES.stop + }); + if(!dontShowDialog) { + APP.UI.messageHandler.openMessageDialog( + "dialog.remoteControlTitle", + "dialog.remoteControlStopMessage" + ); + } + } + + /** + * Calls this._stop() and sends stop message to the controller participant + */ + stop() { + if(!this.controller) { + return; + } + this._sendRemoteControlEvent(this.controller, { + type: EVENT_TYPES.stop + }); + this._stop(); + } + + /** + * Listens for data channel EndpointMessage events. Handles only events of + * type remote control. Sends "remote-control-event" events to the API + * module. + * @param {JitsiParticipant} participant the controller participant + * @param {Object} event EndpointMessage event from the data channels. + * @property {string} type property. The function process only events of + * type REMOTE_CONTROL_EVENT_TYPE + * @property {RemoteControlEvent} event - the remote control event. + */ + _onRemoteControlEvent(participant, event) { + if(this.enabled && event.type === REMOTE_CONTROL_EVENT_TYPE) { + const remoteControlEvent = event.event; + if(this.controller === null + && remoteControlEvent.type === EVENT_TYPES.permissions + && remoteControlEvent.action === PERMISSIONS_ACTIONS.request) { + remoteControlEvent.userId = participant.getId(); + remoteControlEvent.userJID = participant.getJid(); + remoteControlEvent.displayName = participant.getDisplayName() + || interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME; + remoteControlEvent.screenSharing + = APP.conference.isSharingScreen; + } else if(this.controller !== participant.getId()) { + return; + } else if(remoteControlEvent.type === EVENT_TYPES.stop) { + this._stop(); + return; + } + APP.API.sendRemoteControlEvent(remoteControlEvent); + } else if(event.type === REMOTE_CONTROL_EVENT_TYPE) { + logger.log("Remote control event is ignored because remote " + + "control is disabled", event); + } + } + + /** + * Handles remote control permission events received from the API module. + * @param {String} userId the user id of the participant related to the + * event. + * @param {PERMISSIONS_ACTIONS} action the action related to the event. + */ + _onRemoteControlPermissionsEvent(userId, action) { + if(action === PERMISSIONS_ACTIONS.grant) { + APP.conference.addConferenceListener(ConferenceEvents.USER_LEFT, + this._userLeftListener); + this.controller = userId; + logger.log("Remote control permissions granted to: " + userId); + if(!APP.conference.isSharingScreen) { + APP.conference.toggleScreenSharing(); + APP.conference.screenSharingPromise.then(() => { + if(APP.conference.isSharingScreen) { + this._sendRemoteControlEvent(userId, { + type: EVENT_TYPES.permissions, + action: action + }); + } else { + this._sendRemoteControlEvent(userId, { + type: EVENT_TYPES.permissions, + action: PERMISSIONS_ACTIONS.error + }); + } + }).catch(() => { + this._sendRemoteControlEvent(userId, { + type: EVENT_TYPES.permissions, + action: PERMISSIONS_ACTIONS.error + }); + }); + return; + } + } + this._sendRemoteControlEvent(userId, { + type: EVENT_TYPES.permissions, + action: action + }); + } + + /** + * Calls the stop method if the other side have left. + * @param {string} id - the user id for the participant that have left + */ + _onUserLeft(id) { + if(this.controller === id) { + this._stop(); + } + } + + /** + * Handles hangup events. Disables the receiver. + */ + _onHangup() { + this.enable(false); + } +} diff --git a/modules/remotecontrol/RemoteControl.js b/modules/remotecontrol/RemoteControl.js new file mode 100644 index 000000000..86f1f51e5 --- /dev/null +++ b/modules/remotecontrol/RemoteControl.js @@ -0,0 +1,89 @@ +/* global APP, config */ +const logger = require("jitsi-meet-logger").getLogger(__filename); +import Controller from "./Controller"; +import Receiver from "./Receiver"; +import {EVENT_TYPES, DISCO_REMOTE_CONTROL_FEATURE} + from "../../service/remotecontrol/Constants"; + +/** + * Implements the remote control functionality. + */ +class RemoteControl { + /** + * Constructs new instance. Creates controller and receiver properties. + * @constructor + */ + constructor() { + this.controller = new Controller(); + this.receiver = new Receiver(); + this.enabled = false; + this.initialized = false; + } + + /** + * Initializes the remote control - checks if the remote control should be + * enabled or not, initializes the API module. + */ + init() { + if(config.disableRemoteControl || this.initialized + || !APP.conference.isDesktopSharingEnabled) { + return; + } + logger.log("Initializing remote control."); + this.initialized = true; + APP.API.init({ + forceEnable: true, + }); + this.controller.enable(true); + if(this.enabled) { // supported message came before init. + this._onRemoteControlSupported(); + } + } + + /** + * Handles remote control events from the API module. Currently only events + * with type = EVENT_TYPES.supported or EVENT_TYPES.permissions + * @param {RemoteControlEvent} event the remote control event. + */ + onRemoteControlAPIEvent(event) { + switch(event.type) { + case EVENT_TYPES.supported: + this._onRemoteControlSupported(); + break; + case EVENT_TYPES.permissions: + this.receiver._onRemoteControlPermissionsEvent( + event.userId, event.action); + break; + } + } + + /** + * Handles API event for support for executing remote control events into + * the wrapper application. + */ + _onRemoteControlSupported() { + logger.log("Remote Control supported."); + if(!config.disableRemoteControl) { + this.enabled = true; + if(this.initialized) { + this.receiver.enable(true); + } + } else { + logger.log("Remote Control disabled."); + } + } + + /** + * Checks whether the passed user supports remote control or not + * @param {JitsiParticipant} user the user to be tested + * @returns {Promise} the promise will be resolved with true if + * the user supports remote control and with false if not. + */ + checkUserRemoteControlSupport(user) { + return user.getFeatures().then(features => + features.has(DISCO_REMOTE_CONTROL_FEATURE), () => false + ); + } +} + +export default new RemoteControl(); diff --git a/modules/remotecontrol/RemoteControlParticipant.js b/modules/remotecontrol/RemoteControlParticipant.js new file mode 100644 index 000000000..0f283ba9f --- /dev/null +++ b/modules/remotecontrol/RemoteControlParticipant.js @@ -0,0 +1,42 @@ +/* global APP */ +const logger = require("jitsi-meet-logger").getLogger(__filename); +import {REMOTE_CONTROL_EVENT_TYPE} + from "../../service/remotecontrol/Constants"; + +export default class RemoteControlParticipant { + /** + * Creates new instance. + */ + constructor() { + this.enabled = false; + } + + /** + * Enables / Disables the remote control + * @param {boolean} enabled the new state. + */ + enable(enabled) { + this.enabled = enabled; + } + + /** + * Sends remote control event to other participant trough data channel. + * @param {RemoteControlEvent} event the remote control event. + * @param {Function} onDataChannelFail handler for data channel failure. + */ + _sendRemoteControlEvent(to, event, onDataChannelFail = () => {}) { + if(!this.enabled || !to) { + logger.warn("Remote control: Skip sending remote control event." + + " Params:", this.enable, to); + return; + } + try{ + APP.conference.sendEndpointMessage(to, + {type: REMOTE_CONTROL_EVENT_TYPE, event}); + } catch (e) { + logger.error("Failed to send EndpointMessage via the datachannels", + e); + onDataChannelFail(e); + } + } +} diff --git a/modules/tokendata/TokenData.js b/modules/tokendata/TokenData.js index 6e1856ddf..86438937b 100644 --- a/modules/tokendata/TokenData.js +++ b/modules/tokendata/TokenData.js @@ -73,10 +73,6 @@ class TokenData{ this.jwt = jwt; - //External API settings - this.externalAPISettings = { - forceEnable: true - }; this._decode(); // Use JWT param as token if there is not other token set and if the // iss field is not anonymous. If you want to pass data with JWT token diff --git a/react/features/app/functions.web.js b/react/features/app/functions.web.js index ec4bbee3a..22d161843 100644 --- a/react/features/app/functions.web.js +++ b/react/features/app/functions.web.js @@ -23,7 +23,11 @@ export function init() { APP.keyboardshortcut = KeyboardShortcut; APP.tokenData = getTokenData(); - APP.API.init(APP.tokenData.externalAPISettings); + + // Force enable the API if jwt token is passed because most probably + // jitsi meet is displayed inside of wrapper that will need to communicate + // with jitsi meet. + APP.API.init(APP.tokenData.jwt ? { forceEnable: true } : undefined); APP.translation.init(settings.getLanguage()); } diff --git a/service/UI/UIEvents.js b/service/UI/UIEvents.js index 28ba05535..30cac66b6 100644 --- a/service/UI/UIEvents.js +++ b/service/UI/UIEvents.js @@ -120,6 +120,11 @@ export default { */ LARGE_VIDEO_AVATAR_VISIBLE: "UI.large_video_avatar_visible", + /** + * Notifies that the displayed particpant id on the largeVideo is changed. + */ + LARGE_VIDEO_ID_CHANGED: "UI.large_video_id_changed", + /** * Toggling room lock */ diff --git a/service/remotecontrol/Constants.js b/service/remotecontrol/Constants.js new file mode 100644 index 000000000..c856185ac --- /dev/null +++ b/service/remotecontrol/Constants.js @@ -0,0 +1,69 @@ +/** + * The value for the "var" attribute of feature tag in disco-info packets. + */ +export const DISCO_REMOTE_CONTROL_FEATURE + = "http://jitsi.org/meet/remotecontrol"; + +/** + * Types of remote-control-event events. + * @readonly + * @enum {string} + */ +export const EVENT_TYPES = { + mousemove: "mousemove", + mousedown: "mousedown", + mouseup: "mouseup", + mousedblclick: "mousedblclick", + mousescroll: "mousescroll", + keydown: "keydown", + keyup: "keyup", + permissions: "permissions", + stop: "stop", + supported: "supported" +}; + +/** + * Actions for the remote control permission events. + * @readonly + * @enum {string} + */ +export const PERMISSIONS_ACTIONS = { + request: "request", + grant: "grant", + deny: "deny", + error: "error" +}; + +/** + * The type of remote control events sent trough the API module. + */ +export const REMOTE_CONTROL_EVENT_TYPE = "remote-control-event"; + +/** + * The remote control event. + * @typedef {object} RemoteControlEvent + * @property {EVENT_TYPES} type - the type of the event + * @property {int} x - avaibale for type === mousemove only. The new x + * coordinate of the mouse + * @property {int} y - For mousemove type - the new y + * coordinate of the mouse and for mousescroll - represents the vertical + * scrolling diff value + * @property {int} button - 1(left), 2(middle) or 3 (right). Supported by + * mousedown, mouseup and mousedblclick types. + * @property {KEYS} key - Represents the key related to the event. Supported by + * keydown and keyup types. + * @property {KEYS[]} modifiers - Represents the modifier related to the event. + * Supported by keydown and keyup types. + * @property {PERMISSIONS_ACTIONS} action - Supported by type === permissions. + * Represents the action related to the permissions event. + * + * Optional properties. Supported for permissions event for action === request: + * @property {string} userId - The user id of the participant that has sent the + * request. + * @property {string} userJID - The full JID in the MUC of the user that has + * sent the request. + * @property {string} displayName - the displayName of the participant that has + * sent the request. + * @property {boolean} screenSharing - true if the SS is started for the local + * participant and false if not. + */