Browse Source

Merge pull request #1192 from jitsi/remotecontrol

Implement remote control support
master
Paweł Domas 8 years ago
parent
commit
c0e80c14f8

+ 4
- 0
ConferenceEvents.js View File

@@ -0,0 +1,4 @@
1
+/**
2
+ * Notifies interested parties that hangup procedure will start.
3
+ */
4
+export const BEFORE_HANGUP = "conference.before_hangup";

+ 3
- 1
app.js View File

@@ -22,6 +22,7 @@ import conference from './conference';
22 22
 import API from './modules/API/API';
23 23
 
24 24
 import translation from "./modules/translation/translation";
25
+import remoteControl from "./modules/remotecontrol/RemoteControl";
25 26
 
26 27
 const APP = {
27 28
     // Used by do_external_connect.js if we receive the attach data after
@@ -59,7 +60,8 @@ const APP = {
59 60
      */
60 61
     ConferenceUrl : null,
61 62
     connection: null,
62
-    API
63
+    API,
64
+    remoteControl
63 65
 };
64 66
 
65 67
 // TODO The execution of the mobile app starts from react/index.native.js.

+ 62
- 8
conference.js View File

@@ -14,9 +14,12 @@ import {reportError} from './modules/util/helpers';
14 14
 
15 15
 import UIEvents from './service/UI/UIEvents';
16 16
 import UIUtil from './modules/UI/util/UIUtil';
17
+import * as JitsiMeetConferenceEvents from './ConferenceEvents';
17 18
 
18 19
 import analytics from './modules/analytics/analytics';
19 20
 
21
+import EventEmitter from "events";
22
+
20 23
 const ConnectionEvents = JitsiMeetJS.events.connection;
21 24
 const ConnectionErrors = JitsiMeetJS.errors.connection;
22 25
 
@@ -28,6 +31,8 @@ const TrackErrors = JitsiMeetJS.errors.track;
28 31
 
29 32
 const ConnectionQualityEvents = JitsiMeetJS.events.connectionQuality;
30 33
 
34
+const eventEmitter = new EventEmitter();
35
+
31 36
 let room, connection, localAudio, localVideo;
32 37
 
33 38
 /**
@@ -485,10 +490,11 @@ export default {
485 490
             }).then(([tracks, con]) => {
486 491
                 logger.log('initialized with %s local tracks', tracks.length);
487 492
                 APP.connection = connection = con;
488
-                this._bindConnectionFailedHandler(con);
489
-                this._createRoom(tracks);
490 493
                 this.isDesktopSharingEnabled =
491 494
                     JitsiMeetJS.isDesktopSharingEnabled();
495
+                APP.remoteControl.init();
496
+                this._bindConnectionFailedHandler(con);
497
+                this._createRoom(tracks);
492 498
 
493 499
                 if (UIUtil.isButtonEnabled('contacts')
494 500
                     && !interfaceConfig.filmStripOnly) {
@@ -981,7 +987,7 @@ export default {
981 987
         let externalInstallation = false;
982 988
 
983 989
         if (shareScreen) {
984
-            createLocalTracks({
990
+            this.screenSharingPromise = createLocalTracks({
985 991
                 devices: ['desktop'],
986 992
                 desktopSharingExtensionExternalInstallation: {
987 993
                     interval: 500,
@@ -1070,7 +1076,10 @@ export default {
1070 1076
                     dialogTitleKey, dialogTxt, false);
1071 1077
             });
1072 1078
         } else {
1073
-            createLocalTracks({ devices: ['video'] }).then(
1079
+            APP.remoteControl.receiver.stop();
1080
+            this.screenSharingPromise = createLocalTracks(
1081
+                { devices: ['video'] })
1082
+            .then(
1074 1083
                 ([stream]) => this.useVideoStream(stream)
1075 1084
             ).then(() => {
1076 1085
                 this.videoSwitchInProgress = false;
@@ -1102,6 +1111,8 @@ export default {
1102 1111
             }
1103 1112
         );
1104 1113
 
1114
+        room.on(ConferenceEvents.PARTCIPANT_FEATURES_CHANGED,
1115
+            user => APP.UI.onUserFeaturesChanged(user));
1105 1116
         room.on(ConferenceEvents.USER_JOINED, (id, user) => {
1106 1117
             if (user.isHidden())
1107 1118
                 return;
@@ -1600,12 +1611,23 @@ export default {
1600 1611
     },
1601 1612
     /**
1602 1613
     * Adds any room listener.
1603
-    * @param eventName one of the ConferenceEvents
1604
-    * @param callBack the function to be called when the event occurs
1614
+    * @param {string} eventName one of the ConferenceEvents
1615
+    * @param {Function} listener the function to be called when the event
1616
+    * occurs
1605 1617
     */
1606
-    addConferenceListener(eventName, callBack) {
1607
-        room.on(eventName, callBack);
1618
+    addConferenceListener(eventName, listener) {
1619
+        room.on(eventName, listener);
1608 1620
     },
1621
+
1622
+    /**
1623
+    * Removes any room listener.
1624
+    * @param {string} eventName one of the ConferenceEvents
1625
+    * @param {Function} listener the listener to be removed.
1626
+    */
1627
+    removeConferenceListener(eventName, listener) {
1628
+        room.off(eventName, listener);
1629
+    },
1630
+
1609 1631
     /**
1610 1632
      * Inits list of current devices and event listener for device change.
1611 1633
      * @private
@@ -1763,6 +1785,7 @@ export default {
1763 1785
      * requested
1764 1786
      */
1765 1787
     hangup (requestFeedback = false) {
1788
+        eventEmitter.emit(JitsiMeetConferenceEvents.BEFORE_HANGUP);
1766 1789
         APP.UI.hideRingOverLay();
1767 1790
         let requestFeedbackPromise = requestFeedback
1768 1791
                 ? APP.UI.requestFeedbackOnHangup()
@@ -1813,5 +1836,36 @@ export default {
1813 1836
         APP.settings.setAvatarUrl(url);
1814 1837
         APP.UI.setUserAvatarUrl(room.myUserId(), url);
1815 1838
         sendData(commands.AVATAR_URL, url);
1839
+    },
1840
+
1841
+    /**
1842
+     * Sends a message via the data channel.
1843
+     * @param {string} to the id of the endpoint that should receive the
1844
+     * message. If "" - the message will be sent to all participants.
1845
+     * @param {object} payload the payload of the message.
1846
+     * @throws NetworkError or InvalidStateError or Error if the operation
1847
+     * fails.
1848
+     */
1849
+    sendEndpointMessage (to, payload) {
1850
+        room.sendEndpointMessage(to, payload);
1851
+    },
1852
+
1853
+    /**
1854
+     * Adds new listener.
1855
+     * @param {String} eventName the name of the event
1856
+     * @param {Function} listener the listener.
1857
+     */
1858
+    addListener (eventName, listener) {
1859
+        eventEmitter.addListener(eventName, listener);
1860
+    },
1861
+
1862
+    /**
1863
+     * Removes listener.
1864
+     * @param {String} eventName the name of the event that triggers the
1865
+     * listener
1866
+     * @param {Function} listener the listener.
1867
+     */
1868
+    removeListener (eventName, listener) {
1869
+        eventEmitter.removeListener(eventName, listener);
1816 1870
     }
1817 1871
 };

+ 3
- 3
css/_filmstrip.scss View File

@@ -92,7 +92,7 @@
92 92
                 0 0 3px $videoThumbnailSelected !important;
93 93
             }
94 94
 
95
-            .remotevideomenu {
95
+            .remotevideomenu > .icon-menu {
96 96
                 display: none;
97 97
             }
98 98
 
@@ -105,7 +105,7 @@
105 105
                 box-shadow: inset 0 0 3px $videoThumbnailHovered,
106 106
                 0 0 3px $videoThumbnailHovered;
107 107
 
108
-                .remotevideomenu {
108
+                .remotevideomenu > .icon-menu {
109 109
                     display: inline-block;
110 110
                 }
111 111
             }
@@ -121,4 +121,4 @@
121 121
             }
122 122
         }
123 123
     }
124
-}
124
+}

+ 6
- 2
css/_popup_menu.scss View File

@@ -6,7 +6,6 @@
6 6
     padding: 0;
7 7
     margin: 2px 0;
8 8
     bottom: 0;
9
-    width: 100px;
10 9
     height: auto;
11 10
 
12 11
     &:first-child {
@@ -66,4 +65,9 @@
66 65
 
67 66
 span.remotevideomenu:hover ul.popupmenu, ul.popupmenu:hover {
68 67
     display:block !important;
69
-}
68
+}
69
+
70
+.remote-control-spinner {
71
+    top: 6px;
72
+    left: 2px;
73
+}

+ 8
- 3
lang/main.json View File

@@ -156,8 +156,8 @@
156 156
         "kick": "Kick out",
157 157
         "muted": "Muted",
158 158
         "domute": "Mute",
159
-        "flip": "Flip"
160
-
159
+        "flip": "Flip",
160
+        "remoteControl": "Remote control"
161 161
     },
162 162
     "connectionindicator":
163 163
     {
@@ -316,7 +316,12 @@
316 316
         "externalInstallationMsg": "You need to install our desktop sharing extension.",
317 317
         "muteParticipantTitle": "Mute this participant?",
318 318
         "muteParticipantBody": "You won't be able to unmute them, but they can unmute themselves at any time.",
319
-        "muteParticipantButton": "Mute"
319
+        "muteParticipantButton": "Mute",
320
+        "remoteControlTitle": "Remote Control",
321
+        "remoteControlDeniedMessage": "__user__ rejected your remote control request!",
322
+        "remoteControlAllowedMessage": "__user__ accepted your remote control request!",
323
+        "remoteControlErrorMessage": "An error occurred while trying to request remote control permissions from __user__!",
324
+        "remoteControlStopMessage": "The remote control session ended!"
320 325
     },
321 326
     "email":
322 327
     {

+ 42
- 13
modules/API/API.js View File

@@ -55,7 +55,9 @@ function initCommands() {
55 55
             APP.conference.toggleScreenSharing.bind(APP.conference),
56 56
         "video-hangup": () => APP.conference.hangup(),
57 57
         "email": APP.conference.changeLocalEmail,
58
-        "avatar-url": APP.conference.changeLocalAvatarUrl
58
+        "avatar-url": APP.conference.changeLocalAvatarUrl,
59
+        "remote-control-event": event =>
60
+            APP.remoteControl.onRemoteControlAPIEvent(event)
59 61
     };
60 62
     Object.keys(commands).forEach(function (key) {
61 63
         postis.listen(key, args => commands[key](...args));
@@ -94,7 +96,13 @@ function triggerEvent (name, object) {
94 96
     }
95 97
 }
96 98
 
97
-export default {
99
+class API {
100
+    /**
101
+     * Constructs new instance
102
+     * @constructor
103
+     */
104
+    constructor() { }
105
+
98 106
     /**
99 107
      * Initializes the APIConnector. Setups message event listeners that will
100 108
      * receive information from external applications that embed Jitsi Meet.
@@ -108,6 +116,17 @@ export default {
108 116
             return;
109 117
 
110 118
         enabled = true;
119
+
120
+        if(!postis) {
121
+            this._initPostis();
122
+        }
123
+    }
124
+
125
+    /**
126
+     * initializes postis library.
127
+     * @private
128
+     */
129
+    _initPostis() {
111 130
         let postisOptions = {
112 131
             window: target
113 132
         };
@@ -116,7 +135,7 @@ export default {
116 135
                 = "jitsi_meet_external_api_" + jitsi_meet_external_api_id;
117 136
         postis = postisInit(postisOptions);
118 137
         initCommands();
119
-    },
138
+    }
120 139
 
121 140
     /**
122 141
      * Notify external application (if API is enabled) that message was sent.
@@ -124,7 +143,7 @@ export default {
124 143
      */
125 144
     notifySendingChatMessage (body) {
126 145
         triggerEvent("outgoing-message", {"message": body});
127
-    },
146
+    }
128 147
 
129 148
     /**
130 149
      * Notify external application (if API is enabled) that
@@ -143,7 +162,7 @@ export default {
143 162
             "incoming-message",
144 163
             {"from": id, "nick": nick, "message": body, "stamp": ts}
145 164
         );
146
-    },
165
+    }
147 166
 
148 167
     /**
149 168
      * Notify external application (if API is enabled) that
@@ -152,7 +171,7 @@ export default {
152 171
      */
153 172
     notifyUserJoined (id) {
154 173
         triggerEvent("participant-joined", {id});
155
-    },
174
+    }
156 175
 
157 176
     /**
158 177
      * Notify external application (if API is enabled) that
@@ -161,7 +180,7 @@ export default {
161 180
      */
162 181
     notifyUserLeft (id) {
163 182
         triggerEvent("participant-left", {id});
164
-    },
183
+    }
165 184
 
166 185
     /**
167 186
      * Notify external application (if API is enabled) that
@@ -171,7 +190,7 @@ export default {
171 190
      */
172 191
     notifyDisplayNameChanged (id, displayName) {
173 192
         triggerEvent("display-name-change", {id, displayname: displayName});
174
-    },
193
+    }
175 194
 
176 195
     /**
177 196
      * Notify external application (if API is enabled) that
@@ -181,7 +200,7 @@ export default {
181 200
      */
182 201
     notifyConferenceJoined (room) {
183 202
         triggerEvent("video-conference-joined", {roomName: room});
184
-    },
203
+    }
185 204
 
186 205
     /**
187 206
      * Notify external application (if API is enabled) that
@@ -191,7 +210,7 @@ export default {
191 210
      */
192 211
     notifyConferenceLeft (room) {
193 212
         triggerEvent("video-conference-left", {roomName: room});
194
-    },
213
+    }
195 214
 
196 215
     /**
197 216
      * Notify external application (if API is enabled) that
@@ -199,13 +218,23 @@ export default {
199 218
      */
200 219
     notifyReadyToClose () {
201 220
         triggerEvent("video-ready-to-close", {});
202
-    },
221
+    }
222
+
223
+    /**
224
+     * Sends remote control event.
225
+     * @param {RemoteControlEvent} event the remote control event.
226
+     */
227
+    sendRemoteControlEvent(event) {
228
+        sendMessage({method: "remote-control-event", params: event});
229
+    }
203 230
 
204 231
     /**
205 232
      * Removes the listeners.
206 233
      */
207
-    dispose: function () {
234
+    dispose () {
208 235
         if(enabled)
209 236
             postis.destroy();
210 237
     }
211
-};
238
+}
239
+
240
+export default new API();

+ 7
- 0
modules/UI/UI.js View File

@@ -1441,4 +1441,11 @@ UI.hideUserMediaPermissionsGuidanceOverlay = function () {
1441 1441
     GumPermissionsOverlay.hide();
1442 1442
 };
1443 1443
 
1444
+/**
1445
+ * Handles user's features changes.
1446
+ */
1447
+UI.onUserFeaturesChanged = function (user) {
1448
+    VideoLayout.onUserFeaturesChanged(user);
1449
+};
1450
+
1444 1451
 module.exports = UI;

+ 3
- 0
modules/UI/videolayout/LargeVideoManager.js View File

@@ -3,6 +3,7 @@ const logger = require("jitsi-meet-logger").getLogger(__filename);
3 3
 
4 4
 import Avatar from "../avatar/Avatar";
5 5
 import {createDeferred} from '../../util/helpers';
6
+import UIEvents from "../../../service/UI/UIEvents";
6 7
 import UIUtil from "../util/UIUtil";
7 8
 import {VideoContainer, VIDEO_CONTAINER_TYPE} from "./VideoContainer";
8 9
 
@@ -19,6 +20,7 @@ export default class LargeVideoManager {
19 20
          * @type {Object.<string, LargeContainer>}
20 21
          */
21 22
         this.containers = {};
23
+        this.eventEmitter = emitter;
22 24
 
23 25
         this.state = VIDEO_CONTAINER_TYPE;
24 26
         this.videoContainer = new VideoContainer(
@@ -164,6 +166,7 @@ export default class LargeVideoManager {
164 166
             // after everything is done check again if there are any pending
165 167
             // new streams.
166 168
             this.updateInProcess = false;
169
+            this.eventEmitter.emit(UIEvents.LARGE_VIDEO_ID_CHANGED, this.id);
167 170
             this.scheduleLargeVideoUpdate();
168 171
         });
169 172
     }

+ 130
- 38
modules/UI/videolayout/RemoteVideo.js View File

@@ -29,6 +29,7 @@ function RemoteVideo(user, VideoLayout, emitter) {
29 29
     this.videoSpanId = `participant_${this.id}`;
30 30
     SmallVideo.call(this, VideoLayout);
31 31
     this.hasRemoteVideoMenu = false;
32
+    this._supportsRemoteControl = false;
32 33
     this.addRemoteVideoContainer();
33 34
     this.connectionIndicator = new ConnectionIndicator(this, this.id);
34 35
     this.setDisplayName();
@@ -64,7 +65,7 @@ RemoteVideo.prototype.addRemoteVideoContainer = function() {
64 65
 
65 66
     this.initBrowserSpecificProperties();
66 67
 
67
-    if (APP.conference.isModerator) {
68
+    if (APP.conference.isModerator || this._supportsRemoteControl) {
68 69
         this.addRemoteVideoMenu();
69 70
     }
70 71
 
@@ -106,14 +107,6 @@ RemoteVideo.prototype._initPopupMenu = function (popupMenuElement) {
106 107
         // call the original show, passing its actual this
107 108
         origShowFunc.call(this.popover);
108 109
     }.bind(this);
109
-
110
-    // override popover hide method so we can cleanup click handlers
111
-    let origHideFunc = this.popover.forceHide;
112
-    this.popover.forceHide = function () {
113
-        $(document).off("click", '#mutelink_' + this.id);
114
-        $(document).off("click", '#ejectlink_' + this.id);
115
-        origHideFunc.call(this.popover);
116
-    }.bind(this);
117 110
 };
118 111
 
119 112
 /**
@@ -139,38 +132,68 @@ RemoteVideo.prototype._generatePopupContent = function () {
139 132
     let popupmenuElement = document.createElement('ul');
140 133
     popupmenuElement.className = 'popupmenu';
141 134
     popupmenuElement.id = `remote_popupmenu_${this.id}`;
135
+    let menuItems = [];
136
+
137
+    if(APP.conference.isModerator) {
138
+        let muteTranslationKey;
139
+        let muteClassName;
140
+        if (this.isAudioMuted) {
141
+            muteTranslationKey = 'videothumbnail.muted';
142
+            muteClassName = 'mutelink disabled';
143
+        } else {
144
+            muteTranslationKey = 'videothumbnail.domute';
145
+            muteClassName = 'mutelink';
146
+        }
142 147
 
143
-    let muteTranslationKey;
144
-    let muteClassName;
145
-    if (this.isAudioMuted) {
146
-        muteTranslationKey = 'videothumbnail.muted';
147
-        muteClassName = 'mutelink disabled';
148
-    } else {
149
-        muteTranslationKey = 'videothumbnail.domute';
150
-        muteClassName = 'mutelink';
148
+        let muteHandler = this._muteHandler.bind(this);
149
+        let kickHandler = this._kickHandler.bind(this);
150
+
151
+        menuItems = [
152
+            {
153
+                id: 'mutelink_' + this.id,
154
+                handler: muteHandler,
155
+                icon: 'icon-mic-disabled',
156
+                className: muteClassName,
157
+                data: {
158
+                    i18n: muteTranslationKey
159
+                }
160
+            }, {
161
+                id: 'ejectlink_' + this.id,
162
+                handler: kickHandler,
163
+                icon: 'icon-kick',
164
+                data: {
165
+                    i18n: 'videothumbnail.kick'
166
+                }
167
+            }
168
+        ];
151 169
     }
152 170
 
153
-    let muteHandler = this._muteHandler.bind(this);
154
-    let kickHandler = this._kickHandler.bind(this);
155
-
156
-    let menuItems = [
157
-        {
158
-            id: 'mutelink_' + this.id,
159
-            handler: muteHandler,
160
-            icon: 'icon-mic-disabled',
161
-            className: muteClassName,
162
-            data: {
163
-                i18n: muteTranslationKey
164
-            }
165
-        }, {
166
-            id: 'ejectlink_' + this.id,
167
-            handler: kickHandler,
168
-            icon: 'icon-kick',
171
+    if(this._supportsRemoteControl) {
172
+        let icon, handler, className;
173
+        if(APP.remoteControl.controller.getRequestedParticipant()
174
+            === this.id) {
175
+            handler = () => {};
176
+            className = "requestRemoteControlLink disabled";
177
+            icon = "remote-control-spinner fa fa-spinner fa-spin";
178
+        } else if(!APP.remoteControl.controller.isStarted()) {
179
+            handler = this._requestRemoteControlPermissions.bind(this);
180
+            icon = "fa fa-play";
181
+            className = "requestRemoteControlLink";
182
+        } else {
183
+            handler = this._stopRemoteControl.bind(this);
184
+            icon = "fa fa-stop";
185
+            className = "requestRemoteControlLink";
186
+        }
187
+        menuItems.push({
188
+            id: 'remoteControl_' + this.id,
189
+            handler,
190
+            icon,
191
+            className,
169 192
             data: {
170
-                i18n: 'videothumbnail.kick'
193
+                i18n: 'videothumbnail.remoteControl'
171 194
             }
172
-        }
173
-    ];
195
+        });
196
+    }
174 197
 
175 198
     menuItems.forEach(el => {
176 199
         let menuItem = this._generatePopupMenuItem(el);
@@ -182,6 +205,76 @@ RemoteVideo.prototype._generatePopupContent = function () {
182 205
     return popupmenuElement;
183 206
 };
184 207
 
208
+/**
209
+ * Sets the remote control supported value and initializes or updates the menu
210
+ * depending on the remote control is supported or not.
211
+ * @param {boolean} isSupported
212
+ */
213
+RemoteVideo.prototype.setRemoteControlSupport = function(isSupported = false) {
214
+    if(this._supportsRemoteControl === isSupported) {
215
+        return;
216
+    }
217
+    this._supportsRemoteControl = isSupported;
218
+    if(!isSupported) {
219
+        return;
220
+    }
221
+
222
+    if(!this.hasRemoteVideoMenu) {
223
+        //create menu
224
+        this.addRemoteVideoMenu();
225
+    } else {
226
+        //update the content
227
+        this.updateRemoteVideoMenu(this.isAudioMuted, true);
228
+    }
229
+
230
+};
231
+
232
+/**
233
+ * Requests permissions for remote control session.
234
+ */
235
+RemoteVideo.prototype._requestRemoteControlPermissions = function () {
236
+    APP.remoteControl.controller.requestPermissions(
237
+        this.id, this.VideoLayout.getLargeVideoWrapper()).then(result => {
238
+        if(result === null) {
239
+            return;
240
+        }
241
+        this.updateRemoteVideoMenu(this.isAudioMuted, true);
242
+        APP.UI.messageHandler.openMessageDialog(
243
+            "dialog.remoteControlTitle",
244
+            (result === false) ? "dialog.remoteControlDeniedMessage"
245
+                : "dialog.remoteControlAllowedMessage",
246
+            {user: this.user.getDisplayName()
247
+                || interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME}
248
+        );
249
+        if(result === true) {//the remote control permissions has been granted
250
+            // pin the controlled participant
251
+            let pinnedId = this.VideoLayout.getPinnedId();
252
+            if(pinnedId !== this.id) {
253
+                this.VideoLayout.handleVideoThumbClicked(this.id);
254
+            }
255
+        }
256
+    }, error => {
257
+        logger.error(error);
258
+        this.updateRemoteVideoMenu(this.isAudioMuted, true);
259
+        APP.UI.messageHandler.openMessageDialog(
260
+            "dialog.remoteControlTitle",
261
+            "dialog.remoteControlErrorMessage",
262
+            {user: this.user.getDisplayName()
263
+                || interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME}
264
+        );
265
+    });
266
+    this.updateRemoteVideoMenu(this.isAudioMuted, true);
267
+};
268
+
269
+/**
270
+ * Stops remote control session.
271
+ */
272
+RemoteVideo.prototype._stopRemoteControl = function () {
273
+    // send message about stopping
274
+    APP.remoteControl.controller.stop();
275
+    this.updateRemoteVideoMenu(this.isAudioMuted, true);
276
+};
277
+
185 278
 RemoteVideo.prototype._muteHandler = function () {
186 279
     if (this.isAudioMuted)
187 280
         return;
@@ -244,8 +337,7 @@ RemoteVideo.prototype._generatePopupMenuItem = function (opts = {}) {
244 337
     linkItem.appendChild(textContent);
245 338
     linkItem.id = id;
246 339
 
247
-    // Delegate event to the document.
248
-    $(document).on("click", `#${id}`, handler);
340
+    linkItem.onclick = handler;
249 341
     menuItem.appendChild(linkItem);
250 342
 
251 343
     return menuItem;

+ 35
- 2
modules/UI/videolayout/VideoLayout.js View File

@@ -406,6 +406,7 @@ var VideoLayout = {
406 406
             remoteVideo = smallVideo;
407 407
         else
408 408
             remoteVideo = new RemoteVideo(user, VideoLayout, eventEmitter);
409
+        this._setRemoteControlProperties(user, remoteVideo);
409 410
         this.addRemoteVideoContainer(id, remoteVideo);
410 411
     },
411 412
 
@@ -1158,12 +1159,44 @@ var VideoLayout = {
1158 1159
      * Sets the flipX state of the local video.
1159 1160
      * @param {boolean} true for flipped otherwise false;
1160 1161
      */
1161
-    setLocalFlipX: function (val) {
1162
+    setLocalFlipX (val) {
1162 1163
         this.localFlipX = val;
1164
+    },
1165
+
1166
+    getEventEmitter() {return eventEmitter;},
1167
+
1168
+    /**
1169
+     * Handles user's features changes.
1170
+     */
1171
+    onUserFeaturesChanged (user) {
1172
+        let video = this.getSmallVideo(user.getId());
1163 1173
 
1174
+        if (!video) {
1175
+            return;
1176
+        }
1177
+        this._setRemoteControlProperties(user, video);
1164 1178
     },
1165 1179
 
1166
-    getEventEmitter: () => {return eventEmitter;}
1180
+    /**
1181
+     * Sets the remote control properties (checks whether remote control
1182
+     * is supported and executes remoteVideo.setRemoteControlSupport).
1183
+     * @param {JitsiParticipant} user the user that will be checked for remote
1184
+     * control support.
1185
+     * @param {RemoteVideo} remoteVideo the remoteVideo on which the properties
1186
+     * will be set.
1187
+     */
1188
+    _setRemoteControlProperties (user, remoteVideo) {
1189
+        APP.remoteControl.checkUserRemoteControlSupport(user).then(result =>
1190
+            remoteVideo.setRemoteControlSupport(result));
1191
+    },
1192
+
1193
+    /**
1194
+     * Returns the wrapper jquery selector for the largeVideo
1195
+     * @returns {JQuerySelector} the wrapper jquery selector for the largeVideo
1196
+     */
1197
+    getLargeVideoWrapper() {
1198
+        return this.getCurrentlyOnLargeContainer().$wrapper;
1199
+    }
1167 1200
 };
1168 1201
 
1169 1202
 export default VideoLayout;

+ 20
- 0
modules/keyboardshortcut/keyboardshortcut.js View File

@@ -65,6 +65,12 @@ function showKeyboardShortcutsPanel(show) {
65 65
  */
66 66
 let _shortcuts = {};
67 67
 
68
+/**
69
+ * True if the keyboard shortcuts are enabled and false if not.
70
+ * @type {boolean}
71
+ */
72
+let enabled = true;
73
+
68 74
 /**
69 75
  * Maps keycode to character, id of popover for given function and function.
70 76
  */
@@ -74,6 +80,9 @@ var KeyboardShortcut = {
74 80
 
75 81
         var self = this;
76 82
         window.onkeyup = function(e) {
83
+            if(!enabled) {
84
+                return;
85
+            }
77 86
             var key = self._getKeyboardKey(e).toUpperCase();
78 87
             var num = parseInt(key, 10);
79 88
             if(!($(":focus").is("input[type=text]") ||
@@ -93,6 +102,9 @@ var KeyboardShortcut = {
93 102
         };
94 103
 
95 104
         window.onkeydown = function(e) {
105
+            if(!enabled) {
106
+                return;
107
+            }
96 108
             if(!($(":focus").is("input[type=text]") ||
97 109
                 $(":focus").is("input[type=password]") ||
98 110
                 $(":focus").is("textarea"))) {
@@ -105,6 +117,14 @@ var KeyboardShortcut = {
105 117
         };
106 118
     },
107 119
 
120
+    /**
121
+     * Enables/Disables the keyboard shortcuts.
122
+     * @param {boolean} value - the new value.
123
+     */
124
+    enable: function (value) {
125
+        enabled = value;
126
+    },
127
+
108 128
     /**
109 129
      * Registers a new shortcut.
110 130
      *

+ 163
- 0
modules/keycode/keycode.js View File

@@ -0,0 +1,163 @@
1
+/**
2
+ * Enumerates the supported keys.
3
+ * NOTE: The maps represents physical keys on the keyboard, not chars.
4
+ * @readonly
5
+ * @enum {string}
6
+ */
7
+export const KEYS = {
8
+    BACKSPACE: "backspace" ,
9
+    DELETE : "delete",
10
+    RETURN : "enter",
11
+    TAB : "tab",
12
+    ESCAPE : "escape",
13
+    UP : "up",
14
+    DOWN : "down",
15
+    RIGHT : "right",
16
+    LEFT : "left",
17
+    HOME : "home",
18
+    END : "end",
19
+    PAGEUP : "pageup",
20
+    PAGEDOWN : "pagedown",
21
+
22
+    F1 : "f1",
23
+    F2 : "f2",
24
+    F3 : "f3",
25
+    F4 : "f4",
26
+    F5 : "f5",
27
+    F6 : "f6",
28
+    F7 : "f7",
29
+    F8 : "f8",
30
+    F9 : "f9",
31
+    F10 : "f10",
32
+    F11 : "f11",
33
+    F12 : "f12",
34
+    META : "command",
35
+    CMD_L: "command",
36
+    CMD_R: "command",
37
+    ALT : "alt",
38
+    CONTROL : "control",
39
+    SHIFT : "shift",
40
+    CAPS_LOCK: "caps_lock", //not supported by robotjs
41
+    SPACE : "space",
42
+    PRINTSCREEN : "printscreen",
43
+    INSERT : "insert",
44
+
45
+    NUMPAD_0 : "numpad_0",
46
+    NUMPAD_1 : "numpad_1",
47
+    NUMPAD_2 : "numpad_2",
48
+    NUMPAD_3 : "numpad_3",
49
+    NUMPAD_4 : "numpad_4",
50
+    NUMPAD_5 : "numpad_5",
51
+    NUMPAD_6 : "numpad_6",
52
+    NUMPAD_7 : "numpad_7",
53
+    NUMPAD_8 : "numpad_8",
54
+    NUMPAD_9 : "numpad_9",
55
+
56
+    COMMA: ",",
57
+
58
+    PERIOD: ".",
59
+    SEMICOLON: ";",
60
+    QUOTE: "'",
61
+    BRACKET_LEFT: "[",
62
+    BRACKET_RIGHT: "]",
63
+    BACKQUOTE: "`",
64
+    BACKSLASH: "\\",
65
+    MINUS: "-",
66
+    EQUAL: "=",
67
+    SLASH: "/"
68
+};
69
+
70
+/**
71
+ * Mapping between the key codes and keys deined in KEYS.
72
+ * The mappings are based on
73
+ * https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode#Specifications
74
+ */
75
+let keyCodeToKey = {
76
+    8: KEYS.BACKSPACE,
77
+    9: KEYS.TAB,
78
+    13: KEYS.RETURN,
79
+    16: KEYS.SHIFT,
80
+    17: KEYS.CONTROL,
81
+    18: KEYS.ALT,
82
+    20: KEYS.CAPS_LOCK,
83
+    27: KEYS.ESCAPE,
84
+    32: KEYS.SPACE,
85
+    33: KEYS.PAGEUP,
86
+    34: KEYS.PAGEDOWN,
87
+    35: KEYS.END,
88
+    36: KEYS.HOME,
89
+    37: KEYS.LEFT,
90
+    38: KEYS.UP,
91
+    39: KEYS.RIGHT,
92
+    40: KEYS.DOWN,
93
+    42: KEYS.PRINTSCREEN,
94
+    44: KEYS.PRINTSCREEN,
95
+    45: KEYS.INSERT,
96
+    46: KEYS.DELETE,
97
+    59: KEYS.SEMICOLON,
98
+    61: KEYS.EQUAL,
99
+    91: KEYS.CMD_L,
100
+    92: KEYS.CMD_R,
101
+    93: KEYS.CMD_R,
102
+    96: KEYS.NUMPAD_0,
103
+    97: KEYS.NUMPAD_1,
104
+    98: KEYS.NUMPAD_2,
105
+    99: KEYS.NUMPAD_3,
106
+    100: KEYS.NUMPAD_4,
107
+    101: KEYS.NUMPAD_5,
108
+    102: KEYS.NUMPAD_6,
109
+    103: KEYS.NUMPAD_7,
110
+    104: KEYS.NUMPAD_8,
111
+    105: KEYS.NUMPAD_9,
112
+    112: KEYS.F1,
113
+    113: KEYS.F2,
114
+    114: KEYS.F3,
115
+    115: KEYS.F4,
116
+    116: KEYS.F5,
117
+    117: KEYS.F6,
118
+    118: KEYS.F7,
119
+    119: KEYS.F8,
120
+    120: KEYS.F9,
121
+    121: KEYS.F10,
122
+    122: KEYS.F11,
123
+    123: KEYS.F12,
124
+    124: KEYS.PRINTSCREEN,
125
+    173: KEYS.MINUS,
126
+    186: KEYS.SEMICOLON,
127
+    187: KEYS.EQUAL,
128
+    188: KEYS.COMMA,
129
+    189: KEYS.MINUS,
130
+    190: KEYS.PERIOD,
131
+    191: KEYS.SLASH,
132
+    192: KEYS.BACKQUOTE,
133
+    219: KEYS.BRACKET_LEFT,
134
+    220: KEYS.BACKSLASH,
135
+    221: KEYS.BRACKET_RIGHT,
136
+    222: KEYS.QUOTE,
137
+    224: KEYS.META,
138
+    229: KEYS.SEMICOLON
139
+};
140
+
141
+/**
142
+ * Generate codes for digit keys (0-9)
143
+ */
144
+for(let i = 0; i < 10; i++) {
145
+    keyCodeToKey[i + 48] = `${i}`;
146
+}
147
+
148
+/**
149
+ * Generate codes for letter keys (a-z)
150
+ */
151
+for(let i = 0; i < 26; i++) {
152
+    let keyCode = i + 65;
153
+    keyCodeToKey[keyCode] = String.fromCharCode(keyCode).toLowerCase();
154
+}
155
+
156
+/**
157
+ * Returns key associated with the keyCode from the passed event.
158
+ * @param {KeyboardEvent} event the event
159
+ * @returns {KEYS} the key on the keyboard.
160
+ */
161
+export function keyboardEventToKey(event) {
162
+    return keyCodeToKey[event.which];
163
+}

+ 374
- 0
modules/remotecontrol/Controller.js View File

@@ -0,0 +1,374 @@
1
+/* global $, JitsiMeetJS, APP */
2
+const logger = require("jitsi-meet-logger").getLogger(__filename);
3
+import * as KeyCodes from "../keycode/keycode";
4
+import {EVENT_TYPES, REMOTE_CONTROL_EVENT_TYPE, PERMISSIONS_ACTIONS}
5
+    from "../../service/remotecontrol/Constants";
6
+import RemoteControlParticipant from "./RemoteControlParticipant";
7
+import UIEvents from "../../service/UI/UIEvents";
8
+
9
+const ConferenceEvents = JitsiMeetJS.events.conference;
10
+
11
+/**
12
+ * Extract the keyboard key from the keyboard event.
13
+ * @param event {KeyboardEvent} the event.
14
+ * @returns {KEYS} the key that is pressed or undefined.
15
+ */
16
+function getKey(event) {
17
+    return KeyCodes.keyboardEventToKey(event);
18
+}
19
+
20
+/**
21
+ * Extract the modifiers from the keyboard event.
22
+ * @param event {KeyboardEvent} the event.
23
+ * @returns {Array} with possible values: "shift", "control", "alt", "command".
24
+ */
25
+function getModifiers(event) {
26
+    let modifiers = [];
27
+    if(event.shiftKey) {
28
+        modifiers.push("shift");
29
+    }
30
+
31
+    if(event.ctrlKey) {
32
+        modifiers.push("control");
33
+    }
34
+
35
+
36
+    if(event.altKey) {
37
+        modifiers.push("alt");
38
+    }
39
+
40
+    if(event.metaKey) {
41
+        modifiers.push("command");
42
+    }
43
+
44
+    return modifiers;
45
+}
46
+
47
+/**
48
+ * This class represents the controller party for a remote controller session.
49
+ * It listens for mouse and keyboard events and sends them to the receiver
50
+ * party of the remote control session.
51
+ */
52
+export default class Controller extends RemoteControlParticipant {
53
+    /**
54
+     * Creates new instance.
55
+     */
56
+    constructor() {
57
+        super();
58
+        this.isCollectingEvents = false;
59
+        this.controlledParticipant = null;
60
+        this.requestedParticipant = null;
61
+        this._stopListener = this._handleRemoteControlStoppedEvent.bind(this);
62
+        this._userLeftListener = this._onUserLeft.bind(this);
63
+        this._largeVideoChangedListener
64
+            = this._onLargeVideoIdChanged.bind(this);
65
+    }
66
+
67
+    /**
68
+     * Requests permissions from the remote control receiver side.
69
+     * @param {string} userId the user id of the participant that will be
70
+     * requested.
71
+     * @param {JQuerySelector} eventCaptureArea the area that is going to be
72
+     * used mouse and keyboard event capture.
73
+     * @returns {Promise<boolean>} - resolve values:
74
+     * true - accept
75
+     * false - deny
76
+     * null - the participant has left.
77
+     */
78
+    requestPermissions(userId, eventCaptureArea) {
79
+        if(!this.enabled) {
80
+            return Promise.reject(new Error("Remote control is disabled!"));
81
+        }
82
+        this.area = eventCaptureArea;// $("#largeVideoWrapper")
83
+        logger.log("Requsting remote control permissions from: " + userId);
84
+        return new Promise((resolve, reject) => {
85
+            const clearRequest = () => {
86
+                this.requestedParticipant = null;
87
+                APP.conference.removeConferenceListener(
88
+                    ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
89
+                    permissionsReplyListener);
90
+                APP.conference.removeConferenceListener(
91
+                    ConferenceEvents.USER_LEFT,
92
+                    onUserLeft);
93
+            };
94
+            const permissionsReplyListener = (participant, event) => {
95
+                let result = null;
96
+                try {
97
+                    result = this._handleReply(participant, event);
98
+                } catch (e) {
99
+                    reject(e);
100
+                }
101
+                if(result !== null) {
102
+                    clearRequest();
103
+                    resolve(result);
104
+                }
105
+            };
106
+            const onUserLeft = (id) => {
107
+                if(id === this.requestedParticipant) {
108
+                    clearRequest();
109
+                    resolve(null);
110
+                }
111
+            };
112
+            APP.conference.addConferenceListener(
113
+                ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
114
+                permissionsReplyListener);
115
+            APP.conference.addConferenceListener(ConferenceEvents.USER_LEFT,
116
+                onUserLeft);
117
+            this.requestedParticipant = userId;
118
+            this._sendRemoteControlEvent(userId, {
119
+                type: EVENT_TYPES.permissions,
120
+                action: PERMISSIONS_ACTIONS.request
121
+            }, e => {
122
+                clearRequest();
123
+                reject(e);
124
+            });
125
+        });
126
+    }
127
+
128
+    /**
129
+     * Handles the reply of the permissions request.
130
+     * @param {JitsiParticipant} participant the participant that has sent the
131
+     * reply
132
+     * @param {RemoteControlEvent} event the remote control event.
133
+     */
134
+    _handleReply(participant, event) {
135
+        const remoteControlEvent = event.event;
136
+        const userId = participant.getId();
137
+        if(this.enabled && event.type === REMOTE_CONTROL_EVENT_TYPE
138
+            && remoteControlEvent.type === EVENT_TYPES.permissions
139
+            && userId === this.requestedParticipant) {
140
+            if(remoteControlEvent.action !== PERMISSIONS_ACTIONS.grant) {
141
+                this.area = null;
142
+            }
143
+            switch(remoteControlEvent.action) {
144
+                case PERMISSIONS_ACTIONS.grant: {
145
+                    this.controlledParticipant = userId;
146
+                    logger.log("Remote control permissions granted to: "
147
+                        + userId);
148
+                    this._start();
149
+                    return true;
150
+                }
151
+                case PERMISSIONS_ACTIONS.deny:
152
+                    return false;
153
+                case PERMISSIONS_ACTIONS.error:
154
+                    throw new Error("Error occurred on receiver side");
155
+                default:
156
+                    throw new Error("Unknown reply received!");
157
+            }
158
+        } else {
159
+            //different message type or another user -> ignoring the message
160
+            return null;
161
+        }
162
+    }
163
+
164
+    /**
165
+     * Handles remote control stopped.
166
+     * @param {JitsiParticipant} participant the participant that has sent the
167
+     * event
168
+     * @param {Object} event EndpointMessage event from the data channels.
169
+     * @property {string} type property. The function process only events of
170
+     * type REMOTE_CONTROL_EVENT_TYPE
171
+     * @property {RemoteControlEvent} event - the remote control event.
172
+     */
173
+    _handleRemoteControlStoppedEvent(participant, event) {
174
+        if(this.enabled && event.type === REMOTE_CONTROL_EVENT_TYPE
175
+            && event.event.type === EVENT_TYPES.stop
176
+            && participant.getId() === this.controlledParticipant) {
177
+            this._stop();
178
+        }
179
+    }
180
+
181
+    /**
182
+     * Starts processing the mouse and keyboard events. Sets conference
183
+     * listeners. Disables keyboard events.
184
+     */
185
+    _start() {
186
+        logger.log("Starting remote control controller.");
187
+        APP.UI.addListener(UIEvents.LARGE_VIDEO_ID_CHANGED,
188
+            this._largeVideoChangedListener);
189
+        APP.conference.addConferenceListener(
190
+            ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
191
+            this._stopListener);
192
+        APP.conference.addConferenceListener(ConferenceEvents.USER_LEFT,
193
+            this._userLeftListener);
194
+        this.resume();
195
+    }
196
+
197
+    /**
198
+     * Disables the keyboatd shortcuts. Starts collecting remote control
199
+     * events.
200
+     *
201
+     * It can be used to resume an active remote control session wchich was
202
+     * paused with this.pause().
203
+     */
204
+    resume() {
205
+        if(!this.enabled || this.isCollectingEvents) {
206
+            return;
207
+        }
208
+        logger.log("Resuming remote control controller.");
209
+        this.isCollectingEvents = true;
210
+        APP.keyboardshortcut.enable(false);
211
+        this.area.mousemove(event => {
212
+            const position = this.area.position();
213
+            this._sendRemoteControlEvent(this.controlledParticipant, {
214
+                type: EVENT_TYPES.mousemove,
215
+                x: (event.pageX - position.left)/this.area.width(),
216
+                y: (event.pageY - position.top)/this.area.height()
217
+            });
218
+        });
219
+        this.area.mousedown(this._onMouseClickHandler.bind(this,
220
+            EVENT_TYPES.mousedown));
221
+        this.area.mouseup(this._onMouseClickHandler.bind(this,
222
+            EVENT_TYPES.mouseup));
223
+        this.area.dblclick(
224
+            this._onMouseClickHandler.bind(this, EVENT_TYPES.mousedblclick));
225
+        this.area.contextmenu(() => false);
226
+        this.area[0].onmousewheel = event => {
227
+            this._sendRemoteControlEvent(this.controlledParticipant, {
228
+                type: EVENT_TYPES.mousescroll,
229
+                x: event.deltaX,
230
+                y: event.deltaY
231
+            });
232
+        };
233
+        $(window).keydown(this._onKeyPessHandler.bind(this,
234
+            EVENT_TYPES.keydown));
235
+        $(window).keyup(this._onKeyPessHandler.bind(this, EVENT_TYPES.keyup));
236
+    }
237
+
238
+    /**
239
+     * Stops processing the mouse and keyboard events. Removes added listeners.
240
+     * Enables the keyboard shortcuts. Displays dialog to notify the user that
241
+     * remote control session has ended.
242
+     */
243
+    _stop() {
244
+        if(!this.controlledParticipant) {
245
+            return;
246
+        }
247
+        logger.log("Stopping remote control controller.");
248
+        APP.UI.removeListener(UIEvents.LARGE_VIDEO_ID_CHANGED,
249
+            this._largeVideoChangedListener);
250
+        APP.conference.removeConferenceListener(
251
+            ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
252
+            this._stopListener);
253
+        APP.conference.removeConferenceListener(ConferenceEvents.USER_LEFT,
254
+            this._userLeftListener);
255
+        this.controlledParticipant = null;
256
+        this.pause();
257
+        this.area = null;
258
+        APP.UI.messageHandler.openMessageDialog(
259
+            "dialog.remoteControlTitle",
260
+            "dialog.remoteControlStopMessage"
261
+        );
262
+    }
263
+
264
+    /**
265
+     * Executes this._stop() mehtod:
266
+     * Stops processing the mouse and keyboard events. Removes added listeners.
267
+     * Enables the keyboard shortcuts. Displays dialog to notify the user that
268
+     * remote control session has ended.
269
+     *
270
+     * In addition:
271
+     * Sends stop message to the controlled participant.
272
+     */
273
+    stop() {
274
+        if(!this.controlledParticipant) {
275
+            return;
276
+        }
277
+        this._sendRemoteControlEvent(this.controlledParticipant, {
278
+            type: EVENT_TYPES.stop
279
+        });
280
+        this._stop();
281
+    }
282
+
283
+    /**
284
+     * Pauses the collecting of events and enables the keyboard shortcus. But
285
+     * it doesn't removes any other listeners. Basically the remote control
286
+     * session will be still active after this.pause(), but no events from the
287
+     * controller side will be captured and sent.
288
+     *
289
+     * You can resume the collecting of the events with this.resume().
290
+     */
291
+    pause() {
292
+        if(!this.controlledParticipant) {
293
+            return;
294
+        }
295
+        logger.log("Pausing remote control controller.");
296
+        this.isCollectingEvents = false;
297
+        APP.keyboardshortcut.enable(true);
298
+        this.area.off( "mousemove" );
299
+        this.area.off( "mousedown" );
300
+        this.area.off( "mouseup" );
301
+        this.area.off( "contextmenu" );
302
+        this.area.off( "dblclick" );
303
+        $(window).off( "keydown");
304
+        $(window).off( "keyup");
305
+        this.area[0].onmousewheel = undefined;
306
+    }
307
+
308
+    /**
309
+     * Handler for mouse click events.
310
+     * @param {String} type the type of event ("mousedown"/"mouseup")
311
+     * @param {Event} event the mouse event.
312
+     */
313
+    _onMouseClickHandler(type, event) {
314
+        this._sendRemoteControlEvent(this.controlledParticipant, {
315
+            type: type,
316
+            button: event.which
317
+        });
318
+    }
319
+
320
+    /**
321
+     * Returns true if the remote control session is started.
322
+     * @returns {boolean}
323
+     */
324
+    isStarted() {
325
+        return this.controlledParticipant !== null;
326
+    }
327
+
328
+    /**
329
+     * Returns the id of the requested participant
330
+     * @returns {string} this.requestedParticipant.
331
+     * NOTE: This id should be the result of JitsiParticipant.getId() call.
332
+     */
333
+    getRequestedParticipant() {
334
+        return this.requestedParticipant;
335
+    }
336
+
337
+    /**
338
+     * Handler for key press events.
339
+     * @param {String} type the type of event ("keydown"/"keyup")
340
+     * @param {Event} event the key event.
341
+     */
342
+    _onKeyPessHandler(type, event) {
343
+        this._sendRemoteControlEvent(this.controlledParticipant, {
344
+            type: type,
345
+            key: getKey(event),
346
+            modifiers: getModifiers(event),
347
+        });
348
+    }
349
+
350
+    /**
351
+     * Calls the stop method if the other side have left.
352
+     * @param {string} id - the user id for the participant that have left
353
+     */
354
+    _onUserLeft(id) {
355
+        if(this.controlledParticipant === id) {
356
+            this._stop();
357
+        }
358
+    }
359
+
360
+    /**
361
+     * Handles changes of the participant displayed on the large video.
362
+     * @param {string} id - the user id for the participant that is displayed.
363
+     */
364
+    _onLargeVideoIdChanged(id) {
365
+        if (!this.controlledParticipant) {
366
+            return;
367
+        }
368
+        if(this.controlledParticipant == id) {
369
+            this.resume();
370
+        } else {
371
+            this.pause();
372
+        }
373
+    }
374
+}

+ 192
- 0
modules/remotecontrol/Receiver.js View File

@@ -0,0 +1,192 @@
1
+/* global APP, JitsiMeetJS, interfaceConfig */
2
+const logger = require("jitsi-meet-logger").getLogger(__filename);
3
+import {DISCO_REMOTE_CONTROL_FEATURE, REMOTE_CONTROL_EVENT_TYPE, EVENT_TYPES,
4
+    PERMISSIONS_ACTIONS} from "../../service/remotecontrol/Constants";
5
+import RemoteControlParticipant from "./RemoteControlParticipant";
6
+import * as JitsiMeetConferenceEvents from '../../ConferenceEvents';
7
+
8
+const ConferenceEvents = JitsiMeetJS.events.conference;
9
+
10
+/**
11
+ * This class represents the receiver party for a remote controller session.
12
+ * It handles "remote-control-event" events and sends them to the
13
+ * API module. From there the events can be received from wrapper application
14
+ * and executed.
15
+ */
16
+export default class Receiver extends RemoteControlParticipant {
17
+    /**
18
+     * Creates new instance.
19
+     * @constructor
20
+     */
21
+    constructor() {
22
+        super();
23
+        this.controller = null;
24
+        this._remoteControlEventsListener
25
+            = this._onRemoteControlEvent.bind(this);
26
+        this._userLeftListener = this._onUserLeft.bind(this);
27
+        this._hangupListener = this._onHangup.bind(this);
28
+    }
29
+
30
+    /**
31
+     * Enables / Disables the remote control
32
+     * @param {boolean} enabled the new state.
33
+     */
34
+    enable(enabled) {
35
+        if(this.enabled === enabled) {
36
+            return;
37
+        }
38
+        this.enabled = enabled;
39
+        if(enabled === true) {
40
+            logger.log("Remote control receiver enabled.");
41
+            // Announce remote control support.
42
+            APP.connection.addFeature(DISCO_REMOTE_CONTROL_FEATURE, true);
43
+            APP.conference.addConferenceListener(
44
+                ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
45
+                this._remoteControlEventsListener);
46
+            APP.conference.addListener(JitsiMeetConferenceEvents.BEFORE_HANGUP,
47
+                this._hangupListener);
48
+        } else {
49
+            logger.log("Remote control receiver disabled.");
50
+            this._stop(true);
51
+            APP.connection.removeFeature(DISCO_REMOTE_CONTROL_FEATURE);
52
+            APP.conference.removeConferenceListener(
53
+                ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
54
+                this._remoteControlEventsListener);
55
+            APP.conference.removeListener(
56
+                JitsiMeetConferenceEvents.BEFORE_HANGUP,
57
+                this._hangupListener);
58
+        }
59
+    }
60
+
61
+    /**
62
+     * Removes the listener for ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED
63
+     * events. Sends stop message to the wrapper application. Optionally
64
+     * displays dialog for informing the user that remote control session
65
+     * ended.
66
+     * @param {boolean} dontShowDialog - if true the dialog won't be displayed.
67
+     */
68
+    _stop(dontShowDialog = false) {
69
+        if(!this.controller) {
70
+            return;
71
+        }
72
+        logger.log("Remote control receiver stop.");
73
+        this.controller = null;
74
+        APP.conference.removeConferenceListener(ConferenceEvents.USER_LEFT,
75
+            this._userLeftListener);
76
+        APP.API.sendRemoteControlEvent({
77
+            type: EVENT_TYPES.stop
78
+        });
79
+        if(!dontShowDialog) {
80
+            APP.UI.messageHandler.openMessageDialog(
81
+                "dialog.remoteControlTitle",
82
+                "dialog.remoteControlStopMessage"
83
+            );
84
+        }
85
+    }
86
+
87
+    /**
88
+     * Calls this._stop() and sends stop message to the controller participant
89
+     */
90
+    stop() {
91
+        if(!this.controller) {
92
+            return;
93
+        }
94
+        this._sendRemoteControlEvent(this.controller, {
95
+            type: EVENT_TYPES.stop
96
+        });
97
+        this._stop();
98
+    }
99
+
100
+    /**
101
+     * Listens for data channel EndpointMessage events. Handles only events of
102
+     * type remote control. Sends "remote-control-event" events to the API
103
+     * module.
104
+     * @param {JitsiParticipant} participant the controller participant
105
+     * @param {Object} event EndpointMessage event from the data channels.
106
+     * @property {string} type property. The function process only events of
107
+     * type REMOTE_CONTROL_EVENT_TYPE
108
+     * @property {RemoteControlEvent} event - the remote control event.
109
+     */
110
+    _onRemoteControlEvent(participant, event) {
111
+        if(this.enabled && event.type === REMOTE_CONTROL_EVENT_TYPE) {
112
+            const remoteControlEvent = event.event;
113
+            if(this.controller === null
114
+                && remoteControlEvent.type === EVENT_TYPES.permissions
115
+                && remoteControlEvent.action === PERMISSIONS_ACTIONS.request) {
116
+                remoteControlEvent.userId = participant.getId();
117
+                remoteControlEvent.userJID = participant.getJid();
118
+                remoteControlEvent.displayName = participant.getDisplayName()
119
+                    || interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME;
120
+                remoteControlEvent.screenSharing
121
+                    = APP.conference.isSharingScreen;
122
+            } else if(this.controller !== participant.getId()) {
123
+                return;
124
+            } else if(remoteControlEvent.type === EVENT_TYPES.stop) {
125
+                this._stop();
126
+                return;
127
+            }
128
+            APP.API.sendRemoteControlEvent(remoteControlEvent);
129
+        } else if(event.type === REMOTE_CONTROL_EVENT_TYPE) {
130
+            logger.log("Remote control event is ignored because remote "
131
+                + "control is disabled", event);
132
+        }
133
+    }
134
+
135
+    /**
136
+     * Handles remote control permission events received from the API module.
137
+     * @param {String} userId the user id of the participant related to the
138
+     * event.
139
+     * @param {PERMISSIONS_ACTIONS} action the action related to the event.
140
+     */
141
+    _onRemoteControlPermissionsEvent(userId, action) {
142
+        if(action === PERMISSIONS_ACTIONS.grant) {
143
+            APP.conference.addConferenceListener(ConferenceEvents.USER_LEFT,
144
+                this._userLeftListener);
145
+            this.controller = userId;
146
+            logger.log("Remote control permissions granted to: " + userId);
147
+            if(!APP.conference.isSharingScreen) {
148
+                APP.conference.toggleScreenSharing();
149
+                APP.conference.screenSharingPromise.then(() => {
150
+                    if(APP.conference.isSharingScreen) {
151
+                        this._sendRemoteControlEvent(userId, {
152
+                            type: EVENT_TYPES.permissions,
153
+                            action: action
154
+                        });
155
+                    } else {
156
+                        this._sendRemoteControlEvent(userId, {
157
+                            type: EVENT_TYPES.permissions,
158
+                            action: PERMISSIONS_ACTIONS.error
159
+                        });
160
+                    }
161
+                }).catch(() => {
162
+                    this._sendRemoteControlEvent(userId, {
163
+                        type: EVENT_TYPES.permissions,
164
+                        action: PERMISSIONS_ACTIONS.error
165
+                    });
166
+                });
167
+                return;
168
+            }
169
+        }
170
+        this._sendRemoteControlEvent(userId, {
171
+            type: EVENT_TYPES.permissions,
172
+            action: action
173
+        });
174
+    }
175
+
176
+    /**
177
+     * Calls the stop method if the other side have left.
178
+     * @param {string} id - the user id for the participant that have left
179
+     */
180
+    _onUserLeft(id) {
181
+        if(this.controller === id) {
182
+            this._stop();
183
+        }
184
+    }
185
+
186
+    /**
187
+     * Handles hangup events. Disables the receiver.
188
+     */
189
+    _onHangup() {
190
+        this.enable(false);
191
+    }
192
+}

+ 89
- 0
modules/remotecontrol/RemoteControl.js View File

@@ -0,0 +1,89 @@
1
+/* global APP, config */
2
+const logger = require("jitsi-meet-logger").getLogger(__filename);
3
+import Controller from "./Controller";
4
+import Receiver from "./Receiver";
5
+import {EVENT_TYPES, DISCO_REMOTE_CONTROL_FEATURE}
6
+    from "../../service/remotecontrol/Constants";
7
+
8
+/**
9
+ * Implements the remote control functionality.
10
+ */
11
+class RemoteControl {
12
+    /**
13
+     * Constructs new instance. Creates controller and receiver properties.
14
+     * @constructor
15
+     */
16
+    constructor() {
17
+        this.controller = new Controller();
18
+        this.receiver = new Receiver();
19
+        this.enabled = false;
20
+        this.initialized = false;
21
+    }
22
+
23
+    /**
24
+     * Initializes the remote control - checks if the remote control should be
25
+     * enabled or not, initializes the API module.
26
+     */
27
+    init() {
28
+        if(config.disableRemoteControl || this.initialized
29
+            || !APP.conference.isDesktopSharingEnabled) {
30
+            return;
31
+        }
32
+        logger.log("Initializing remote control.");
33
+        this.initialized = true;
34
+        APP.API.init({
35
+            forceEnable: true,
36
+        });
37
+        this.controller.enable(true);
38
+        if(this.enabled) { // supported message came before init.
39
+            this._onRemoteControlSupported();
40
+        }
41
+    }
42
+
43
+    /**
44
+     * Handles remote control events from the API module. Currently only events
45
+     * with type = EVENT_TYPES.supported or EVENT_TYPES.permissions
46
+     * @param {RemoteControlEvent} event the remote control event.
47
+     */
48
+    onRemoteControlAPIEvent(event) {
49
+        switch(event.type) {
50
+            case EVENT_TYPES.supported:
51
+                this._onRemoteControlSupported();
52
+                break;
53
+            case EVENT_TYPES.permissions:
54
+                this.receiver._onRemoteControlPermissionsEvent(
55
+                    event.userId, event.action);
56
+                break;
57
+        }
58
+    }
59
+
60
+    /**
61
+     * Handles API event for support for executing remote control events into
62
+     * the wrapper application.
63
+     */
64
+    _onRemoteControlSupported() {
65
+        logger.log("Remote Control supported.");
66
+        if(!config.disableRemoteControl) {
67
+            this.enabled = true;
68
+            if(this.initialized) {
69
+                this.receiver.enable(true);
70
+            }
71
+        } else {
72
+            logger.log("Remote Control disabled.");
73
+        }
74
+    }
75
+
76
+    /**
77
+     * Checks whether the passed user supports remote control or not
78
+     * @param {JitsiParticipant} user the user to be tested
79
+     * @returns {Promise<boolean>} the promise will be resolved with true if
80
+     * the user supports remote control and with false if not.
81
+     */
82
+    checkUserRemoteControlSupport(user) {
83
+        return user.getFeatures().then(features =>
84
+            features.has(DISCO_REMOTE_CONTROL_FEATURE), () => false
85
+        );
86
+    }
87
+}
88
+
89
+export default new RemoteControl();

+ 42
- 0
modules/remotecontrol/RemoteControlParticipant.js View File

@@ -0,0 +1,42 @@
1
+/* global APP */
2
+const logger = require("jitsi-meet-logger").getLogger(__filename);
3
+import {REMOTE_CONTROL_EVENT_TYPE}
4
+    from "../../service/remotecontrol/Constants";
5
+
6
+export default class RemoteControlParticipant {
7
+    /**
8
+     * Creates new instance.
9
+     */
10
+    constructor() {
11
+        this.enabled = false;
12
+    }
13
+
14
+    /**
15
+     * Enables / Disables the remote control
16
+     * @param {boolean} enabled the new state.
17
+     */
18
+    enable(enabled) {
19
+        this.enabled = enabled;
20
+    }
21
+
22
+    /**
23
+     * Sends remote control event to other participant trough data channel.
24
+     * @param {RemoteControlEvent} event the remote control event.
25
+     * @param {Function} onDataChannelFail handler for data channel failure.
26
+     */
27
+    _sendRemoteControlEvent(to, event, onDataChannelFail = () => {}) {
28
+        if(!this.enabled || !to) {
29
+            logger.warn("Remote control: Skip sending remote control event."
30
+                + " Params:", this.enable, to);
31
+            return;
32
+        }
33
+        try{
34
+            APP.conference.sendEndpointMessage(to,
35
+                {type: REMOTE_CONTROL_EVENT_TYPE, event});
36
+        } catch (e) {
37
+            logger.error("Failed to send EndpointMessage via the datachannels",
38
+                e);
39
+            onDataChannelFail(e);
40
+        }
41
+    }
42
+}

+ 0
- 4
modules/tokendata/TokenData.js View File

@@ -73,10 +73,6 @@ class TokenData{
73 73
 
74 74
         this.jwt = jwt;
75 75
 
76
-        //External API settings
77
-        this.externalAPISettings = {
78
-            forceEnable: true
79
-        };
80 76
         this._decode();
81 77
         // Use JWT param as token if there is not other token set and if the
82 78
         // iss field is not anonymous. If you want to pass data with JWT token

+ 5
- 1
react/features/app/functions.web.js View File

@@ -23,7 +23,11 @@ export function init() {
23 23
 
24 24
     APP.keyboardshortcut = KeyboardShortcut;
25 25
     APP.tokenData = getTokenData();
26
-    APP.API.init(APP.tokenData.externalAPISettings);
26
+
27
+    // Force enable the API if jwt token is passed because most probably
28
+    // jitsi meet is displayed inside of wrapper that will need to communicate
29
+    // with jitsi meet.
30
+    APP.API.init(APP.tokenData.jwt ? { forceEnable: true } : undefined);
27 31
 
28 32
     APP.translation.init(settings.getLanguage());
29 33
 }

+ 5
- 0
service/UI/UIEvents.js View File

@@ -120,6 +120,11 @@ export default {
120 120
      */
121 121
     LARGE_VIDEO_AVATAR_VISIBLE: "UI.large_video_avatar_visible",
122 122
 
123
+    /**
124
+     * Notifies that the displayed particpant id on the largeVideo is changed.
125
+     */
126
+    LARGE_VIDEO_ID_CHANGED: "UI.large_video_id_changed",
127
+
123 128
     /**
124 129
      * Toggling room lock
125 130
      */

+ 69
- 0
service/remotecontrol/Constants.js View File

@@ -0,0 +1,69 @@
1
+/**
2
+ * The value for the "var" attribute of feature tag in disco-info packets.
3
+ */
4
+export const DISCO_REMOTE_CONTROL_FEATURE
5
+    = "http://jitsi.org/meet/remotecontrol";
6
+
7
+/**
8
+ * Types of remote-control-event events.
9
+  * @readonly
10
+  * @enum {string}
11
+ */
12
+export const EVENT_TYPES = {
13
+    mousemove: "mousemove",
14
+    mousedown: "mousedown",
15
+    mouseup: "mouseup",
16
+    mousedblclick: "mousedblclick",
17
+    mousescroll: "mousescroll",
18
+    keydown: "keydown",
19
+    keyup: "keyup",
20
+    permissions: "permissions",
21
+    stop: "stop",
22
+    supported: "supported"
23
+};
24
+
25
+/**
26
+ * Actions for the remote control permission events.
27
+ * @readonly
28
+ * @enum {string}
29
+ */
30
+export const PERMISSIONS_ACTIONS = {
31
+    request: "request",
32
+    grant: "grant",
33
+    deny: "deny",
34
+    error: "error"
35
+};
36
+
37
+/**
38
+ * The type of remote control events sent trough the API module.
39
+ */
40
+export const REMOTE_CONTROL_EVENT_TYPE = "remote-control-event";
41
+
42
+/**
43
+ * The remote control event.
44
+ * @typedef {object} RemoteControlEvent
45
+ * @property {EVENT_TYPES} type - the type of the event
46
+ * @property {int} x - avaibale for type === mousemove only. The new x
47
+ * coordinate of the mouse
48
+ * @property {int} y - For mousemove type - the new y
49
+ * coordinate of the mouse and for mousescroll - represents the vertical
50
+ * scrolling diff value
51
+ * @property {int} button - 1(left), 2(middle) or 3 (right). Supported by
52
+ * mousedown, mouseup and mousedblclick types.
53
+ * @property {KEYS} key - Represents the key related to the event. Supported by
54
+ * keydown and keyup types.
55
+ * @property {KEYS[]} modifiers - Represents the modifier related to the event.
56
+ * Supported by keydown and keyup types.
57
+ * @property {PERMISSIONS_ACTIONS} action - Supported by type === permissions.
58
+ * Represents the action related to the permissions event.
59
+ *
60
+ * Optional properties. Supported for permissions event for action === request:
61
+ * @property {string} userId - The user id of the participant that has sent the
62
+ * request.
63
+ * @property {string} userJID - The full JID in the MUC of the user that has
64
+ * sent the request.
65
+ * @property {string} displayName - the displayName of the participant that has
66
+ * sent the request.
67
+ * @property {boolean} screenSharing - true if the SS is started for the local
68
+ * participant and false if not.
69
+ */

Loading…
Cancel
Save