Selaa lähdekoodia

ref(remote-control): Use React/Redux.

master
Hristo Terezov 4 vuotta sitten
vanhempi
commit
af6c794fda
30 muutettua tiedostoa jossa 1347 lisäystä ja 1454 poistoa
  1. 0
    4
      ConferenceEvents.js
  2. 0
    2
      app.js
  3. 9
    79
      conference.js
  4. 0
    97
      modules/UI/UI.js
  5. 0
    42
      modules/UI/audio_levels/AudioLevels.js
  6. 4
    118
      modules/UI/videolayout/RemoteVideo.js
  7. 0
    50
      modules/UI/videolayout/VideoLayout.js
  8. 0
    3
      modules/remotecontrol/.eslintrc.js
  9. 0
    474
      modules/remotecontrol/Controller.js
  10. 0
    331
      modules/remotecontrol/Receiver.js
  11. 0
    98
      modules/remotecontrol/RemoteControl.js
  12. 0
    72
      modules/remotecontrol/RemoteControlParticipant.js
  13. 1
    0
      react/features/app/middlewares.any.js
  14. 1
    0
      react/features/app/reducers.any.js
  15. 47
    1
      react/features/base/participants/actions.js
  16. 5
    0
      react/features/base/participants/constants.js
  17. 5
    0
      react/features/base/participants/logger.js
  18. 7
    0
      react/features/base/participants/middleware.js
  19. 70
    0
      react/features/remote-control/actionTypes.js
  20. 733
    1
      react/features/remote-control/actions.js
  21. 25
    11
      react/features/remote-control/components/RemoteControlAuthorizationDialog.js
  22. 35
    35
      react/features/remote-control/constants.js
  23. 128
    0
      react/features/remote-control/functions.js
  24. 3
    2
      react/features/remote-control/keycodes.js
  25. 5
    0
      react/features/remote-control/logger.js
  26. 92
    0
      react/features/remote-control/middleware.js
  27. 68
    0
      react/features/remote-control/reducer.js
  28. 33
    0
      react/features/remote-control/subscriber.js
  29. 76
    26
      react/features/remote-video-menu/components/web/RemoteVideoMenuTriggerButton.js
  30. 0
    8
      service/remotecontrol/RemoteControlEvents.js

+ 0
- 4
ConferenceEvents.js Näytä tiedosto

1
-/**
2
- * Notifies interested parties that hangup procedure will start.
3
- */
4
-export const BEFORE_HANGUP = 'conference.before_hangup';

+ 0
- 2
app.js Näytä tiedosto

17
 import API from './modules/API';
17
 import API from './modules/API';
18
 import UI from './modules/UI/UI';
18
 import UI from './modules/UI/UI';
19
 import keyboardshortcut from './modules/keyboardshortcut/keyboardshortcut';
19
 import keyboardshortcut from './modules/keyboardshortcut/keyboardshortcut';
20
-import remoteControl from './modules/remotecontrol/RemoteControl';
21
 import translation from './modules/translation/translation';
20
 import translation from './modules/translation/translation';
22
 
21
 
23
 // Initialize Olm as early as possible.
22
 // Initialize Olm as early as possible.
49
     },
48
     },
50
 
49
 
51
     keyboardshortcut,
50
     keyboardshortcut,
52
-    remoteControl,
53
     translation,
51
     translation,
54
     UI
52
     UI
55
 };
53
 };

+ 9
- 79
conference.js Näytä tiedosto

3
 import EventEmitter from 'events';
3
 import EventEmitter from 'events';
4
 import Logger from 'jitsi-meet-logger';
4
 import Logger from 'jitsi-meet-logger';
5
 
5
 
6
-import * as JitsiMeetConferenceEvents from './ConferenceEvents';
7
 import { openConnection } from './connection';
6
 import { openConnection } from './connection';
8
 import { ENDPOINT_TEXT_MESSAGE_NAME } from './modules/API/constants';
7
 import { ENDPOINT_TEXT_MESSAGE_NAME } from './modules/API/constants';
9
 import AuthHandler from './modules/UI/authentication/AuthHandler';
8
 import AuthHandler from './modules/UI/authentication/AuthHandler';
86
     participantMutedUs,
85
     participantMutedUs,
87
     participantPresenceChanged,
86
     participantPresenceChanged,
88
     participantRoleChanged,
87
     participantRoleChanged,
89
-    participantUpdated
88
+    participantUpdated,
89
+    updateRemoteParticipantFeatures
90
 } from './react/features/base/participants';
90
 } from './react/features/base/participants';
91
 import {
91
 import {
92
     getUserSelectedCameraDeviceId,
92
     getUserSelectedCameraDeviceId,
122
     isPrejoinPageVisible,
122
     isPrejoinPageVisible,
123
     makePrecallTest
123
     makePrecallTest
124
 } from './react/features/prejoin';
124
 } from './react/features/prejoin';
125
+import { disableReceiver, stopReceiver } from './react/features/remote-control';
125
 import { toggleScreenshotCaptureEffect } from './react/features/screenshot-capture';
126
 import { toggleScreenshotCaptureEffect } from './react/features/screenshot-capture';
126
 import { setSharedVideoStatus } from './react/features/shared-video';
127
 import { setSharedVideoStatus } from './react/features/shared-video';
127
 import { AudioMixerEffect } from './react/features/stream-effects/audio-mixer/AudioMixerEffect';
128
 import { AudioMixerEffect } from './react/features/stream-effects/audio-mixer/AudioMixerEffect';
128
 import { createPresenterEffect } from './react/features/stream-effects/presenter';
129
 import { createPresenterEffect } from './react/features/stream-effects/presenter';
129
 import { endpointMessageReceived } from './react/features/subtitles';
130
 import { endpointMessageReceived } from './react/features/subtitles';
130
 import UIEvents from './service/UI/UIEvents';
131
 import UIEvents from './service/UI/UIEvents';
131
-import * as RemoteControlEvents
132
-    from './service/remotecontrol/RemoteControlEvents';
133
 
132
 
134
 const logger = Logger.getLogger(__filename);
133
 const logger = Logger.getLogger(__filename);
135
 
134
 
680
         APP.connection = connection = con;
679
         APP.connection = connection = con;
681
 
680
 
682
         this._createRoom(tracks);
681
         this._createRoom(tracks);
683
-        APP.remoteControl.init();
684
 
682
 
685
         // if user didn't give access to mic or camera or doesn't have
683
         // if user didn't give access to mic or camera or doesn't have
686
         // them at all, we mark corresponding toolbar buttons as muted,
684
         // them at all, we mark corresponding toolbar buttons as muted,
1445
     async _turnScreenSharingOff(didHaveVideo) {
1443
     async _turnScreenSharingOff(didHaveVideo) {
1446
         this._untoggleScreenSharing = null;
1444
         this._untoggleScreenSharing = null;
1447
         this.videoSwitchInProgress = true;
1445
         this.videoSwitchInProgress = true;
1448
-        const { receiver } = APP.remoteControl;
1449
 
1446
 
1450
-        if (receiver) {
1451
-            receiver.stop();
1452
-        }
1447
+        APP.store.dispatch(stopReceiver());
1453
 
1448
 
1454
         this._stopProxyConnection();
1449
         this._stopProxyConnection();
1455
         if (config.enableScreenshotCapture) {
1450
         if (config.enableScreenshotCapture) {
1872
             (authEnabled, authLogin) =>
1867
             (authEnabled, authLogin) =>
1873
                 APP.store.dispatch(authStatusChanged(authEnabled, authLogin)));
1868
                 APP.store.dispatch(authStatusChanged(authEnabled, authLogin)));
1874
 
1869
 
1875
-        room.on(JitsiConferenceEvents.PARTCIPANT_FEATURES_CHANGED,
1876
-            user => APP.UI.onUserFeaturesChanged(user));
1870
+        room.on(JitsiConferenceEvents.PARTCIPANT_FEATURES_CHANGED, user => {
1871
+            APP.store.dispatch(updateRemoteParticipantFeatures(user));
1872
+        });
1877
         room.on(JitsiConferenceEvents.USER_JOINED, (id, user) => {
1873
         room.on(JitsiConferenceEvents.USER_JOINED, (id, user) => {
1878
             // The logic shared between RN and web.
1874
             // The logic shared between RN and web.
1879
             commonUserJoinedHandling(APP.store, room, user);
1875
             commonUserJoinedHandling(APP.store, room, user);
1882
                 return;
1878
                 return;
1883
             }
1879
             }
1884
 
1880
 
1881
+            APP.store.dispatch(updateRemoteParticipantFeatures(user));
1885
             logger.log(`USER ${id} connnected:`, user);
1882
             logger.log(`USER ${id} connnected:`, user);
1886
             APP.UI.addUser(user);
1883
             APP.UI.addUser(user);
1887
         });
1884
         });
2052
             JitsiConferenceEvents.LOCK_STATE_CHANGED,
2049
             JitsiConferenceEvents.LOCK_STATE_CHANGED,
2053
             (...args) => APP.store.dispatch(lockStateChanged(room, ...args)));
2050
             (...args) => APP.store.dispatch(lockStateChanged(room, ...args)));
2054
 
2051
 
2055
-        APP.remoteControl.on(RemoteControlEvents.ACTIVE_CHANGED, isActive => {
2056
-            room.setLocalParticipantProperty(
2057
-                'remoteControlSessionStatus',
2058
-                isActive
2059
-            );
2060
-            APP.UI.setLocalRemoteControlActiveChanged();
2061
-        });
2062
-
2063
-        /* eslint-disable max-params */
2064
-        room.on(
2065
-            JitsiConferenceEvents.PARTICIPANT_PROPERTY_CHANGED,
2066
-            (participant, name, oldValue, newValue) => {
2067
-                switch (name) {
2068
-                case 'remoteControlSessionStatus':
2069
-                    APP.UI.setRemoteControlActiveStatus(
2070
-                        participant.getId(),
2071
-                        newValue);
2072
-                    break;
2073
-                default:
2074
-
2075
-                // ignore
2076
-                }
2077
-            });
2078
-
2079
         room.on(JitsiConferenceEvents.KICKED, participant => {
2052
         room.on(JitsiConferenceEvents.KICKED, participant => {
2080
             APP.UI.hideStats();
2053
             APP.UI.hideStats();
2081
             APP.store.dispatch(kickedOut(room, participant));
2054
             APP.store.dispatch(kickedOut(room, participant));
2420
         APP.UI.changeDisplayName('localVideoContainer', displayName);
2393
         APP.UI.changeDisplayName('localVideoContainer', displayName);
2421
     },
2394
     },
2422
 
2395
 
2423
-    /**
2424
-    * Adds any room listener.
2425
-    * @param {string} eventName one of the JitsiConferenceEvents
2426
-    * @param {Function} listener the function to be called when the event
2427
-    * occurs
2428
-    */
2429
-    addConferenceListener(eventName, listener) {
2430
-        room.on(eventName, listener);
2431
-    },
2432
-
2433
-    /**
2434
-    * Removes any room listener.
2435
-    * @param {string} eventName one of the JitsiConferenceEvents
2436
-    * @param {Function} listener the listener to be removed.
2437
-    */
2438
-    removeConferenceListener(eventName, listener) {
2439
-        room.off(eventName, listener);
2440
-    },
2441
-
2442
     /**
2396
     /**
2443
      * Updates the list of current devices.
2397
      * Updates the list of current devices.
2444
      * @param {boolean} setDeviceListChangeHandler - Whether to add the deviceList change handlers.
2398
      * @param {boolean} setDeviceListChangeHandler - Whether to add the deviceList change handlers.
2721
      * requested
2675
      * requested
2722
      */
2676
      */
2723
     hangup(requestFeedback = false) {
2677
     hangup(requestFeedback = false) {
2724
-        eventEmitter.emit(JitsiMeetConferenceEvents.BEFORE_HANGUP);
2678
+        APP.store.dispatch(disableReceiver());
2725
 
2679
 
2726
         this._stopProxyConnection();
2680
         this._stopProxyConnection();
2727
 
2681
 
2738
         }
2692
         }
2739
 
2693
 
2740
         APP.UI.removeAllListeners();
2694
         APP.UI.removeAllListeners();
2741
-        APP.remoteControl.removeAllListeners();
2742
 
2695
 
2743
         let requestFeedbackPromise;
2696
         let requestFeedbackPromise;
2744
 
2697
 
2921
         }
2874
         }
2922
     },
2875
     },
2923
 
2876
 
2924
-    /**
2925
-     * Returns the desktop sharing source id or undefined if the desktop sharing
2926
-     * is not active at the moment.
2927
-     *
2928
-     * @returns {string|undefined} - The source id. If the track is not desktop
2929
-     * track or the source id is not available, undefined will be returned.
2930
-     */
2931
-    getDesktopSharingSourceId() {
2932
-        return this.localVideo.sourceId;
2933
-    },
2934
-
2935
-    /**
2936
-     * Returns the desktop sharing source type or undefined if the desktop
2937
-     * sharing is not active at the moment.
2938
-     *
2939
-     * @returns {'screen'|'window'|undefined} - The source type. If the track is
2940
-     * not desktop track or the source type is not available, undefined will be
2941
-     * returned.
2942
-     */
2943
-    getDesktopSharingSourceType() {
2944
-        return this.localVideo.sourceType;
2945
-    },
2946
-
2947
     /**
2877
     /**
2948
      * Callback invoked by the external api create or update a direct connection
2878
      * Callback invoked by the external api create or update a direct connection
2949
      * from the local client to an external client.
2879
      * from the local client to an external client.

+ 0
- 97
modules/UI/UI.js Näytä tiedosto

59
     return UIUtil.isFullScreen();
59
     return UIUtil.isFullScreen();
60
 };
60
 };
61
 
61
 
62
-/**
63
- * Returns true if the etherpad window is currently visible.
64
- * @returns {Boolean} - true if the etherpad window is currently visible.
65
- */
66
-UI.isEtherpadVisible = function() {
67
-    return Boolean(etherpadManager && etherpadManager.isVisible());
68
-};
69
-
70
 /**
62
 /**
71
  * Returns true if there is a shared video which is being shown (?).
63
  * Returns true if there is a shared video which is being shown (?).
72
  * @returns {boolean} - true if there is a shared video which is being shown.
64
  * @returns {boolean} - true if there is a shared video which is being shown.
305
  */
297
  */
306
 UI.toggleChat = () => APP.store.dispatch(toggleChat());
298
 UI.toggleChat = () => APP.store.dispatch(toggleChat());
307
 
299
 
308
-/**
309
- * Handle new user display name.
310
- */
311
-UI.inputDisplayNameHandler = function(newDisplayName) {
312
-    eventEmitter.emit(UIEvents.NICKNAME_CHANGED, newDisplayName);
313
-};
314
-
315
-// FIXME check if someone user this
316
-UI.showLoginPopup = function(callback) {
317
-    logger.log('password is required');
318
-
319
-    const message
320
-        = `<input name="username" type="text"
321
-                placeholder="user@domain.net"
322
-                data-i18n="[placeholder]dialog.user"
323
-                class="input-control" autofocus>
324
-         <input name="password" type="password"
325
-                data-i18n="[placeholder]dialog.userPassword"
326
-                class="input-control"
327
-                placeholder="user password">`
328
-
329
-    ;
330
-
331
-    // eslint-disable-next-line max-params
332
-    const submitFunction = (e, v, m, f) => {
333
-        if (v && f.username && f.password) {
334
-            callback(f.username, f.password);
335
-        }
336
-    };
337
-
338
-    messageHandler.openTwoButtonDialog({
339
-        titleKey: 'dialog.passwordRequired',
340
-        msgString: message,
341
-        leftButtonKey: 'dialog.Ok',
342
-        submitFunction,
343
-        focus: ':input:first'
344
-    });
345
-};
346
-
347
 /**
300
 /**
348
  * Sets muted audio state for participant
301
  * Sets muted audio state for participant
349
  */
302
  */
500
     });
453
     });
501
 };
454
 };
502
 
455
 
503
-UI.notifyInternalError = function(error) {
504
-    messageHandler.showError({
505
-        descriptionArguments: { error },
506
-        descriptionKey: 'dialog.internalError',
507
-        titleKey: 'dialog.internalErrorTitle'
508
-    });
509
-};
510
-
511
 UI.notifyFocusDisconnected = function(focus, retrySec) {
456
 UI.notifyFocusDisconnected = function(focus, retrySec) {
512
     messageHandler.participantNotification(
457
     messageHandler.participantNotification(
513
         null, 'notify.focus',
458
         null, 'notify.focus',
517
     );
462
     );
518
 };
463
 };
519
 
464
 
520
-/**
521
- * Notifies interested listeners that the raise hand property has changed.
522
- *
523
- * @param {boolean} isRaisedHand indicates the current state of the
524
- * "raised hand"
525
- */
526
-UI.onLocalRaiseHandChanged = function(isRaisedHand) {
527
-    eventEmitter.emit(UIEvents.LOCAL_RAISE_HAND_CHANGED, isRaisedHand);
528
-};
529
-
530
 /**
465
 /**
531
  * Update list of available physical devices.
466
  * Update list of available physical devices.
532
  */
467
  */
586
     }
521
     }
587
 };
522
 };
588
 
523
 
589
-/**
590
- * Handles user's features changes.
591
- */
592
-UI.onUserFeaturesChanged = user => VideoLayout.onUserFeaturesChanged(user);
593
-
594
-/**
595
- * Returns the number of known remote videos.
596
- *
597
- * @returns {number} The number of remote videos.
598
- */
599
-UI.getRemoteVideosCount = () => VideoLayout.getRemoteVideosCount();
600
-
601
-/**
602
- * Sets the remote control active status for a remote participant.
603
- *
604
- * @param {string} participantID - The id of the remote participant.
605
- * @param {boolean} isActive - The new remote control active status.
606
- * @returns {void}
607
- */
608
-UI.setRemoteControlActiveStatus = function(participantID, isActive) {
609
-    VideoLayout.setRemoteControlActiveStatus(participantID, isActive);
610
-};
611
-
612
-/**
613
- * Sets the remote control active status for the local participant.
614
- *
615
- * @returns {void}
616
- */
617
-UI.setLocalRemoteControlActiveChanged = function() {
618
-    VideoLayout.setLocalRemoteControlActiveChanged();
619
-};
620
-
621
 // TODO: Export every function separately. For now there is no point of doing
524
 // TODO: Export every function separately. For now there is no point of doing
622
 // this because we are importing everything.
525
 // this because we are importing everything.
623
 export default UI;
526
 export default UI;

+ 0
- 42
modules/UI/audio_levels/AudioLevels.js Näytä tiedosto

6
  * Responsible for drawing audio levels.
6
  * Responsible for drawing audio levels.
7
  */
7
  */
8
 const AudioLevels = {
8
 const AudioLevels = {
9
-    /**
10
-     * Fills the dot(s) with the specified "index", with as much opacity as
11
-     * indicated by "opacity".
12
-     *
13
-     * @param {string} elementID the parent audio indicator span element
14
-     * @param {number} index the index of the dots to fill, where 0 indicates
15
-     * the middle dot and the following increments point toward the
16
-     * corresponding pair of dots.
17
-     * @param {number} opacity the opacity to set for the specified dot.
18
-     */
19
-    _setDotLevel(elementID, index, opacity) {
20
-        let audioSpan
21
-            = document.getElementById(elementID)
22
-                .getElementsByClassName('audioindicator');
23
-
24
-        // Make sure the audio span is still around.
25
-        if (audioSpan && audioSpan.length > 0) {
26
-            audioSpan = audioSpan[0];
27
-        } else {
28
-            return;
29
-        }
30
-
31
-        const audioTopDots
32
-            = audioSpan.getElementsByClassName('audiodot-top');
33
-        const audioDotMiddle
34
-            = audioSpan.getElementsByClassName('audiodot-middle');
35
-        const audioBottomDots
36
-            = audioSpan.getElementsByClassName('audiodot-bottom');
37
-
38
-        // First take care of the middle dot case.
39
-        if (index === 0) {
40
-            audioDotMiddle[0].style.opacity = opacity;
41
-
42
-            return;
43
-        }
44
-
45
-        // Index > 0 : we are setting non-middle dots.
46
-        index--;// eslint-disable-line no-param-reassign
47
-        audioBottomDots[index].style.opacity = opacity;
48
-        audioTopDots[this.sideDotsCount - index - 1].style.opacity = opacity;
49
-    },
50
-
51
     /**
9
     /**
52
      * Updates the audio level of the large video.
10
      * Updates the audio level of the large video.
53
      *
11
      *

+ 4
- 118
modules/UI/videolayout/RemoteVideo.js Näytä tiedosto

12
 import {
12
 import {
13
     JitsiParticipantConnectionStatus
13
     JitsiParticipantConnectionStatus
14
 } from '../../../react/features/base/lib-jitsi-meet';
14
 } from '../../../react/features/base/lib-jitsi-meet';
15
-import {
16
-    getParticipantById,
17
-    getPinnedParticipant,
18
-    pinParticipant
19
-} from '../../../react/features/base/participants';
15
+import { getParticipantById } from '../../../react/features/base/participants';
20
 import { isTestModeEnabled } from '../../../react/features/base/testing';
16
 import { isTestModeEnabled } from '../../../react/features/base/testing';
21
 import { updateLastTrackVideoMediaEvent } from '../../../react/features/base/tracks';
17
 import { updateLastTrackVideoMediaEvent } from '../../../react/features/base/tracks';
22
 import { PresenceLabel } from '../../../react/features/presence-status';
18
 import { PresenceLabel } from '../../../react/features/presence-status';
23
-import {
24
-    REMOTE_CONTROL_MENU_STATES,
25
-    RemoteVideoMenuTriggerButton
26
-} from '../../../react/features/remote-video-menu';
27
-import { LAYOUTS, getCurrentLayout } from '../../../react/features/video-layout';
19
+import { stopController, requestRemoteControl } from '../../../react/features/remote-control';
20
+import { RemoteVideoMenuTriggerButton } from '../../../react/features/remote-video-menu';
28
 /* eslint-enable no-unused-vars */
21
 /* eslint-enable no-unused-vars */
29
 import UIUtils from '../util/UIUtil';
22
 import UIUtils from '../util/UIUtil';
30
 
23
 
90
         this.videoSpanId = `participant_${this.id}`;
83
         this.videoSpanId = `participant_${this.id}`;
91
 
84
 
92
         this._audioStreamElement = null;
85
         this._audioStreamElement = null;
93
-        this._supportsRemoteControl = false;
94
         this.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP ? 'left bottom' : 'top center';
86
         this.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP ? 'left bottom' : 'top center';
95
         this.addRemoteVideoContainer();
87
         this.addRemoteVideoContainer();
96
         this.updateIndicators();
88
         this.updateIndicators();
98
         this.bindHoverHandler();
90
         this.bindHoverHandler();
99
         this.flipX = false;
91
         this.flipX = false;
100
         this.isLocal = false;
92
         this.isLocal = false;
101
-        this._isRemoteControlSessionActive = false;
102
 
93
 
103
         /**
94
         /**
104
          * The flag is set to <tt>true</tt> after the 'canplay' event has been
95
          * The flag is set to <tt>true</tt> after the 'canplay' event has been
112
         // Bind event handlers so they are only bound once for every instance.
103
         // Bind event handlers so they are only bound once for every instance.
113
         // TODO The event handlers should be turned into actions so changes can be
104
         // TODO The event handlers should be turned into actions so changes can be
114
         // handled through reducers and middleware.
105
         // handled through reducers and middleware.
115
-        this._requestRemoteControlPermissions
116
-            = this._requestRemoteControlPermissions.bind(this);
117
         this._setAudioVolume = this._setAudioVolume.bind(this);
106
         this._setAudioVolume = this._setAudioVolume.bind(this);
118
-        this._stopRemoteControl = this._stopRemoteControl.bind(this);
119
 
107
 
120
         this.container.onclick = this._onContainerClick;
108
         this.container.onclick = this._onContainerClick;
121
     }
109
     }
151
             return;
139
             return;
152
         }
140
         }
153
 
141
 
154
-        const { controller } = APP.remoteControl;
155
-        let remoteControlState = null;
156
-        let onRemoteControlToggle;
157
-
158
-        if (this._supportsRemoteControl
159
-            && ((!APP.remoteControl.active && !this._isRemoteControlSessionActive)
160
-                || APP.remoteControl.controller.activeParticipant === this.id)) {
161
-            if (controller.getRequestedParticipant() === this.id) {
162
-                remoteControlState = REMOTE_CONTROL_MENU_STATES.REQUESTING;
163
-            } else if (controller.isStarted()) {
164
-                onRemoteControlToggle = this._stopRemoteControl;
165
-                remoteControlState = REMOTE_CONTROL_MENU_STATES.STARTED;
166
-            } else {
167
-                onRemoteControlToggle = this._requestRemoteControlPermissions;
168
-                remoteControlState = REMOTE_CONTROL_MENU_STATES.NOT_STARTED;
169
-            }
170
-        }
171
-
172
         const initialVolumeValue = this._audioStreamElement && this._audioStreamElement.volume;
142
         const initialVolumeValue = this._audioStreamElement && this._audioStreamElement.volume;
173
 
143
 
174
         // hide volume when in silent mode
144
         // hide volume when in silent mode
175
         const onVolumeChange
145
         const onVolumeChange
176
             = APP.store.getState()['features/base/config'].startSilent ? undefined : this._setAudioVolume;
146
             = APP.store.getState()['features/base/config'].startSilent ? undefined : this._setAudioVolume;
177
-        const participantID = this.id;
178
-        const currentLayout = getCurrentLayout(APP.store.getState());
179
-        let remoteMenuPosition;
180
-
181
-        if (currentLayout === LAYOUTS.TILE_VIEW) {
182
-            remoteMenuPosition = 'left top';
183
-        } else if (currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
184
-            remoteMenuPosition = 'left bottom';
185
-        } else {
186
-            remoteMenuPosition = 'top center';
187
-        }
188
 
147
 
189
         ReactDOM.render(
148
         ReactDOM.render(
190
             <Provider store = { APP.store }>
149
             <Provider store = { APP.store }>
192
                     <AtlasKitThemeProvider mode = 'dark'>
151
                     <AtlasKitThemeProvider mode = 'dark'>
193
                         <RemoteVideoMenuTriggerButton
152
                         <RemoteVideoMenuTriggerButton
194
                             initialVolumeValue = { initialVolumeValue }
153
                             initialVolumeValue = { initialVolumeValue }
195
-                            menuPosition = { remoteMenuPosition }
196
                             onMenuDisplay
154
                             onMenuDisplay
197
                                 = {this._onRemoteVideoMenuDisplay.bind(this)}
155
                                 = {this._onRemoteVideoMenuDisplay.bind(this)}
198
-                            onRemoteControlToggle = { onRemoteControlToggle }
199
                             onVolumeChange = { onVolumeChange }
156
                             onVolumeChange = { onVolumeChange }
200
-                            participantID = { participantID }
201
-                            remoteControlState = { remoteControlState } />
157
+                            participantID = { this.id } />
202
                     </AtlasKitThemeProvider>
158
                     </AtlasKitThemeProvider>
203
                 </I18nextProvider>
159
                 </I18nextProvider>
204
             </Provider>,
160
             </Provider>,
212
         this.updateRemoteVideoMenu();
168
         this.updateRemoteVideoMenu();
213
     }
169
     }
214
 
170
 
215
-    /**
216
-     * Sets the remote control active status for the remote video.
217
-     *
218
-     * @param {boolean} isActive - The new remote control active status.
219
-     * @returns {void}
220
-     */
221
-    setRemoteControlActiveStatus(isActive) {
222
-        this._isRemoteControlSessionActive = isActive;
223
-        this.updateRemoteVideoMenu();
224
-    }
225
-
226
-    /**
227
-     * Sets the remote control supported value and initializes or updates the menu
228
-     * depending on the remote control is supported or not.
229
-     * @param {boolean} isSupported
230
-     */
231
-    setRemoteControlSupport(isSupported = false) {
232
-        if (this._supportsRemoteControl === isSupported) {
233
-            return;
234
-        }
235
-        this._supportsRemoteControl = isSupported;
236
-        this.updateRemoteVideoMenu();
237
-    }
238
-
239
-    /**
240
-     * Requests permissions for remote control session.
241
-     */
242
-    _requestRemoteControlPermissions() {
243
-        APP.remoteControl.controller.requestPermissions(this.id, this.VideoLayout.getLargeVideoWrapper())
244
-            .then(result => {
245
-                if (result === null) {
246
-                    return;
247
-                }
248
-                this.updateRemoteVideoMenu();
249
-                APP.UI.messageHandler.notify(
250
-                    'dialog.remoteControlTitle',
251
-                    result === false ? 'dialog.remoteControlDeniedMessage' : 'dialog.remoteControlAllowedMessage',
252
-                    { user: this.user.getDisplayName() || interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME }
253
-                );
254
-                if (result === true) {
255
-                    // the remote control permissions has been granted
256
-                    // pin the controlled participant
257
-                    const pinnedParticipant = getPinnedParticipant(APP.store.getState()) || {};
258
-                    const pinnedId = pinnedParticipant.id;
259
-
260
-                    if (pinnedId !== this.id) {
261
-                        APP.store.dispatch(pinParticipant(this.id));
262
-                    }
263
-                }
264
-            }, error => {
265
-                logger.error(error);
266
-                this.updateRemoteVideoMenu();
267
-                APP.UI.messageHandler.notify(
268
-                    'dialog.remoteControlTitle',
269
-                    'dialog.remoteControlErrorMessage',
270
-                    { user: this.user.getDisplayName() || interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME }
271
-                );
272
-            });
273
-        this.updateRemoteVideoMenu();
274
-    }
275
-
276
-    /**
277
-     * Stops remote control session.
278
-     */
279
-    _stopRemoteControl() {
280
-        // send message about stopping
281
-        APP.remoteControl.controller.stop();
282
-        this.updateRemoteVideoMenu();
283
-    }
284
-
285
     /**
171
     /**
286
      * Change the remote participant's volume level.
172
      * Change the remote participant's volume level.
287
      *
173
      *

+ 0
- 50
modules/UI/videolayout/VideoLayout.js Näytä tiedosto

293
         const jitsiParticipant = APP.conference.getParticipantById(id);
293
         const jitsiParticipant = APP.conference.getParticipantById(id);
294
         const remoteVideo = new RemoteVideo(jitsiParticipant, VideoLayout);
294
         const remoteVideo = new RemoteVideo(jitsiParticipant, VideoLayout);
295
 
295
 
296
-        this._setRemoteControlProperties(jitsiParticipant, remoteVideo);
297
         this.addRemoteVideoContainer(id, remoteVideo);
296
         this.addRemoteVideoContainer(id, remoteVideo);
298
 
297
 
299
         this.updateMutedForNoTracks(id, 'audio');
298
         this.updateMutedForNoTracks(id, 'audio');
645
         this.localFlipX = val;
644
         this.localFlipX = val;
646
     },
645
     },
647
 
646
 
648
-    /**
649
-     * Handles user's features changes.
650
-     */
651
-    onUserFeaturesChanged(user) {
652
-        const video = this.getSmallVideo(user.getId());
653
-
654
-        if (!video) {
655
-            return;
656
-        }
657
-        this._setRemoteControlProperties(user, video);
658
-    },
659
-
660
-    /**
661
-     * Sets the remote control properties (checks whether remote control
662
-     * is supported and executes remoteVideo.setRemoteControlSupport).
663
-     * @param {JitsiParticipant} user the user that will be checked for remote
664
-     * control support.
665
-     * @param {RemoteVideo} remoteVideo the remoteVideo on which the properties
666
-     * will be set.
667
-     */
668
-    _setRemoteControlProperties(user, remoteVideo) {
669
-        APP.remoteControl.checkUserRemoteControlSupport(user)
670
-            .then(result => remoteVideo.setRemoteControlSupport(result))
671
-            .catch(error =>
672
-                logger.warn(`could not get remote control properties for: ${user.getJid()}`, error));
673
-    },
674
-
675
     /**
647
     /**
676
      * Returns the wrapper jquery selector for the largeVideo
648
      * Returns the wrapper jquery selector for the largeVideo
677
      * @returns {JQuerySelector} the wrapper jquery selector for the largeVideo
649
      * @returns {JQuerySelector} the wrapper jquery selector for the largeVideo
689
         return Object.keys(remoteVideos).length;
661
         return Object.keys(remoteVideos).length;
690
     },
662
     },
691
 
663
 
692
-    /**
693
-     * Sets the remote control active status for a remote participant.
694
-     *
695
-     * @param {string} participantID - The id of the remote participant.
696
-     * @param {boolean} isActive - The new remote control active status.
697
-     * @returns {void}
698
-     */
699
-    setRemoteControlActiveStatus(participantID, isActive) {
700
-        remoteVideos[participantID].setRemoteControlActiveStatus(isActive);
701
-    },
702
-
703
-    /**
704
-     * Sets the remote control active status for the local participant.
705
-     *
706
-     * @returns {void}
707
-     */
708
-    setLocalRemoteControlActiveChanged() {
709
-        Object.values(remoteVideos).forEach(
710
-            remoteVideo => remoteVideo.updateRemoteVideoMenu()
711
-        );
712
-    },
713
-
714
     /**
664
     /**
715
      * Helper method to invoke when the video layout has changed and elements
665
      * Helper method to invoke when the video layout has changed and elements
716
      * have to be re-arranged and resized.
666
      * have to be re-arranged and resized.

+ 0
- 3
modules/remotecontrol/.eslintrc.js Näytä tiedosto

1
-module.exports = {
2
-    'extends': '../../react/.eslintrc.js'
3
-};

+ 0
- 474
modules/remotecontrol/Controller.js Näytä tiedosto

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

+ 0
- 331
modules/remotecontrol/Receiver.js Näytä tiedosto

1
-/* @flow */
2
-
3
-import { getLogger } from 'jitsi-meet-logger';
4
-
5
-import * as JitsiMeetConferenceEvents from '../../ConferenceEvents';
6
-import {
7
-    JitsiConferenceEvents
8
-} from '../../react/features/base/lib-jitsi-meet';
9
-import {
10
-    openRemoteControlAuthorizationDialog
11
-} from '../../react/features/remote-control';
12
-import {
13
-    DISCO_REMOTE_CONTROL_FEATURE,
14
-    EVENTS,
15
-    PERMISSIONS_ACTIONS,
16
-    REMOTE_CONTROL_MESSAGE_NAME,
17
-    REQUESTS
18
-} from '../../service/remotecontrol/Constants';
19
-import * as RemoteControlEvents
20
-    from '../../service/remotecontrol/RemoteControlEvents';
21
-import { Transport, PostMessageTransportBackend } from '../transport';
22
-
23
-import RemoteControlParticipant from './RemoteControlParticipant';
24
-
25
-declare var APP: Object;
26
-declare var config: Object;
27
-declare var interfaceConfig: Object;
28
-
29
-const logger = getLogger(__filename);
30
-
31
-/**
32
- * The transport instance used for communication with external apps.
33
- *
34
- * @type {Transport}
35
- */
36
-const transport = new Transport({
37
-    backend: new PostMessageTransportBackend({
38
-        postisOptions: { scope: 'jitsi-remote-control' }
39
-    })
40
-});
41
-
42
-/**
43
- * This class represents the receiver party for a remote controller session.
44
- * It handles "remote-control-event" events and sends them to the
45
- * API module. From there the events can be received from wrapper application
46
- * and executed.
47
- */
48
-export default class Receiver extends RemoteControlParticipant {
49
-    _controller: ?string;
50
-    _enabled: boolean;
51
-    _hangupListener: Function;
52
-    _remoteControlEventsListener: Function;
53
-    _userLeftListener: Function;
54
-
55
-    /**
56
-     * Creates new instance.
57
-     */
58
-    constructor() {
59
-        super();
60
-        this._controller = null;
61
-        this._remoteControlEventsListener
62
-            = this._onRemoteControlMessage.bind(this);
63
-        this._userLeftListener = this._onUserLeft.bind(this);
64
-        this._hangupListener = this._onHangup.bind(this);
65
-
66
-        // We expect here that even if we receive the supported event earlier
67
-        // it will be cached and we'll receive it.
68
-        transport.on('event', event => {
69
-            if (event.name === REMOTE_CONTROL_MESSAGE_NAME) {
70
-                this._onRemoteControlAPIEvent(event);
71
-
72
-                return true;
73
-            }
74
-
75
-            return false;
76
-        });
77
-    }
78
-
79
-    /**
80
-     * Enables / Disables the remote control.
81
-     *
82
-     * @param {boolean} enabled - The new state.
83
-     * @returns {void}
84
-     */
85
-    _enable(enabled: boolean) {
86
-        if (this._enabled === enabled) {
87
-            return;
88
-        }
89
-        this._enabled = enabled;
90
-        if (enabled === true) {
91
-            logger.log('Remote control receiver enabled.');
92
-
93
-            // Announce remote control support.
94
-            APP.connection.addFeature(DISCO_REMOTE_CONTROL_FEATURE, true);
95
-            APP.conference.addConferenceListener(
96
-                JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
97
-                this._remoteControlEventsListener);
98
-            APP.conference.addListener(JitsiMeetConferenceEvents.BEFORE_HANGUP,
99
-                this._hangupListener);
100
-        } else {
101
-            logger.log('Remote control receiver disabled.');
102
-            this._stop(true);
103
-            APP.connection.removeFeature(DISCO_REMOTE_CONTROL_FEATURE);
104
-            APP.conference.removeConferenceListener(
105
-                JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
106
-                this._remoteControlEventsListener);
107
-            APP.conference.removeListener(
108
-                JitsiMeetConferenceEvents.BEFORE_HANGUP,
109
-                this._hangupListener);
110
-        }
111
-    }
112
-
113
-    /**
114
-     * Removes the listener for JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED
115
-     * events. Sends stop message to the wrapper application. Optionally
116
-     * displays dialog for informing the user that remote control session
117
-     * ended.
118
-     *
119
-     * @param {boolean} [dontNotify] - If true - a notification about stopping
120
-     * the remote control won't be displayed.
121
-     * @returns {void}
122
-     */
123
-    _stop(dontNotify: boolean = false) {
124
-        if (!this._controller) {
125
-            return;
126
-        }
127
-        logger.log('Remote control receiver stop.');
128
-        this._controller = null;
129
-        APP.conference.removeConferenceListener(
130
-            JitsiConferenceEvents.USER_LEFT,
131
-            this._userLeftListener);
132
-        transport.sendEvent({
133
-            name: REMOTE_CONTROL_MESSAGE_NAME,
134
-            type: EVENTS.stop
135
-        });
136
-        this.emit(RemoteControlEvents.ACTIVE_CHANGED, false);
137
-        if (!dontNotify) {
138
-            APP.UI.messageHandler.notify(
139
-                'dialog.remoteControlTitle',
140
-                'dialog.remoteControlStopMessage'
141
-            );
142
-        }
143
-    }
144
-
145
-    /**
146
-     * Calls this._stop() and sends stop message to the controller participant.
147
-     *
148
-     * @returns {void}
149
-     */
150
-    stop() {
151
-        if (!this._controller) {
152
-            return;
153
-        }
154
-        this.sendRemoteControlEndpointMessage(this._controller, {
155
-            type: EVENTS.stop
156
-        });
157
-        this._stop();
158
-    }
159
-
160
-    /**
161
-     * Listens for data channel EndpointMessage. Handles only remote control
162
-     * messages. Sends the remote control messages to the external app that
163
-     * will execute them.
164
-     *
165
-     * @param {JitsiParticipant} participant - The controller participant.
166
-     * @param {Object} message - EndpointMessage from the data channels.
167
-     * @param {string} message.name - The function processes only messages with
168
-     * name REMOTE_CONTROL_MESSAGE_NAME.
169
-     * @returns {void}
170
-     */
171
-    _onRemoteControlMessage(participant: Object, message: Object) {
172
-        if (message.name !== REMOTE_CONTROL_MESSAGE_NAME) {
173
-            return;
174
-        }
175
-
176
-        if (this._enabled) {
177
-            if (this._controller === null
178
-                    && message.type === EVENTS.permissions
179
-                    && message.action === PERMISSIONS_ACTIONS.request) {
180
-                const userId = participant.getId();
181
-
182
-                this.emit(RemoteControlEvents.ACTIVE_CHANGED, true);
183
-                APP.store.dispatch(
184
-                    openRemoteControlAuthorizationDialog(userId));
185
-            } else if (this._controller === participant.getId()) {
186
-                if (message.type === EVENTS.stop) {
187
-                    this._stop();
188
-                } else { // forward the message
189
-                    transport.sendEvent(message);
190
-                }
191
-            } // else ignore
192
-        } else {
193
-            logger.log('Remote control message is ignored because remote '
194
-                + 'control is disabled', message);
195
-        }
196
-    }
197
-
198
-    /**
199
-     * Denies remote control access for user associated with the passed user id.
200
-     *
201
-     * @param {string} userId - The id associated with the user who sent the
202
-     * request for remote control authorization.
203
-     * @returns {void}
204
-     */
205
-    deny(userId: string) {
206
-        this.emit(RemoteControlEvents.ACTIVE_CHANGED, false);
207
-        this.sendRemoteControlEndpointMessage(userId, {
208
-            type: EVENTS.permissions,
209
-            action: PERMISSIONS_ACTIONS.deny
210
-        });
211
-    }
212
-
213
-    /**
214
-     * Grants remote control access to user associated with the passed user id.
215
-     *
216
-     * @param {string} userId - The id associated with the user who sent the
217
-     * request for remote control authorization.
218
-     * @returns {void}
219
-     */
220
-    grant(userId: string) {
221
-        APP.conference.addConferenceListener(JitsiConferenceEvents.USER_LEFT,
222
-            this._userLeftListener);
223
-        this._controller = userId;
224
-        logger.log(`Remote control permissions granted to: ${userId}`);
225
-
226
-        let promise;
227
-
228
-        if (APP.conference.isSharingScreen
229
-                && APP.conference.getDesktopSharingSourceType() === 'screen') {
230
-            promise = this._sendStartRequest();
231
-        } else {
232
-            promise = APP.conference.toggleScreenSharing(
233
-                true,
234
-                {
235
-                    desktopSharingSources: [ 'screen' ]
236
-                })
237
-                .then(() => this._sendStartRequest());
238
-        }
239
-
240
-        promise
241
-            .then(() =>
242
-                this.sendRemoteControlEndpointMessage(userId, {
243
-                    type: EVENTS.permissions,
244
-                    action: PERMISSIONS_ACTIONS.grant
245
-                })
246
-            )
247
-            .catch(error => {
248
-                logger.error(error);
249
-
250
-                this.sendRemoteControlEndpointMessage(userId, {
251
-                    type: EVENTS.permissions,
252
-                    action: PERMISSIONS_ACTIONS.error
253
-                });
254
-
255
-                APP.UI.messageHandler.notify(
256
-                    'dialog.remoteControlTitle',
257
-                    'dialog.startRemoteControlErrorMessage'
258
-                );
259
-
260
-                this._stop(true);
261
-            });
262
-    }
263
-
264
-    /**
265
-     * Sends remote control start request.
266
-     *
267
-     * @returns {Promise}
268
-     */
269
-    _sendStartRequest() {
270
-        return transport.sendRequest({
271
-            name: REMOTE_CONTROL_MESSAGE_NAME,
272
-            type: REQUESTS.start,
273
-            sourceId: APP.conference.getDesktopSharingSourceId()
274
-        });
275
-    }
276
-
277
-    /**
278
-     * Handles remote control events from the external app. Currently only
279
-     * events with type EVENTS.supported and EVENTS.stop are
280
-     * supported.
281
-     *
282
-     * @param {RemoteControlEvent} event - The remote control event.
283
-     * @returns {void}
284
-     */
285
-    _onRemoteControlAPIEvent(event: Object) {
286
-        switch (event.type) {
287
-        case EVENTS.supported:
288
-            this._onRemoteControlSupported();
289
-            break;
290
-        case EVENTS.stop:
291
-            this.stop();
292
-            break;
293
-        }
294
-    }
295
-
296
-    /**
297
-     * Handles events for support for executing remote control events into
298
-     * the wrapper application.
299
-     *
300
-     * @returns {void}
301
-     */
302
-    _onRemoteControlSupported() {
303
-        logger.log('Remote Control supported.');
304
-        if (config.disableRemoteControl) {
305
-            logger.log('Remote Control disabled.');
306
-        } else {
307
-            this._enable(true);
308
-        }
309
-    }
310
-
311
-    /**
312
-     * Calls the stop method if the other side have left.
313
-     *
314
-     * @param {string} id - The user id for the participant that have left.
315
-     * @returns {void}
316
-     */
317
-    _onUserLeft(id: string) {
318
-        if (this._controller === id) {
319
-            this._stop();
320
-        }
321
-    }
322
-
323
-    /**
324
-     * Handles hangup events. Disables the receiver.
325
-     *
326
-     * @returns {void}
327
-     */
328
-    _onHangup() {
329
-        this._enable(false);
330
-    }
331
-}

+ 0
- 98
modules/remotecontrol/RemoteControl.js Näytä tiedosto

1
-/* @flow */
2
-
3
-import EventEmitter from 'events';
4
-import { getLogger } from 'jitsi-meet-logger';
5
-
6
-import JitsiMeetJS from '../../react/features/base/lib-jitsi-meet';
7
-import { DISCO_REMOTE_CONTROL_FEATURE }
8
-    from '../../service/remotecontrol/Constants';
9
-import * as RemoteControlEvents
10
-    from '../../service/remotecontrol/RemoteControlEvents';
11
-
12
-import Controller from './Controller';
13
-import Receiver from './Receiver';
14
-
15
-const logger = getLogger(__filename);
16
-
17
-declare var APP: Object;
18
-declare var config: Object;
19
-
20
-/**
21
- * Implements the remote control functionality.
22
- */
23
-class RemoteControl extends EventEmitter {
24
-    _active: boolean;
25
-    _initialized: boolean;
26
-    controller: Controller;
27
-    receiver: Receiver;
28
-
29
-    /**
30
-     * Constructs new instance. Creates controller and receiver properties.
31
-     */
32
-    constructor() {
33
-        super();
34
-        this.controller = new Controller();
35
-        this._active = false;
36
-        this._initialized = false;
37
-
38
-        this.controller.on(RemoteControlEvents.ACTIVE_CHANGED, active => {
39
-            this.active = active;
40
-        });
41
-    }
42
-
43
-    /**
44
-     * Sets the remote control session active status.
45
-     *
46
-     * @param {boolean} isActive - True - if the controller or the receiver is
47
-     * currently in remote control session and false otherwise.
48
-     * @returns {void}
49
-     */
50
-    set active(isActive: boolean) {
51
-        this._active = isActive;
52
-        this.emit(RemoteControlEvents.ACTIVE_CHANGED, isActive);
53
-    }
54
-
55
-    /**
56
-     * Returns the remote control session active status.
57
-     *
58
-     * @returns {boolean} - True - if the controller or the receiver is
59
-     * currently in remote control session and false otherwise.
60
-     */
61
-    get active(): boolean {
62
-        return this._active;
63
-    }
64
-
65
-    /**
66
-     * Initializes the remote control - checks if the remote control should be
67
-     * enabled or not.
68
-     *
69
-     * @returns {void}
70
-     */
71
-    init() {
72
-        if (config.disableRemoteControl || this._initialized || !JitsiMeetJS.isDesktopSharingEnabled()) {
73
-            return;
74
-        }
75
-        logger.log('Initializing remote control.');
76
-        this._initialized = true;
77
-        this.controller.enable(true);
78
-        this.receiver = new Receiver();
79
-
80
-        this.receiver.on(RemoteControlEvents.ACTIVE_CHANGED, active => {
81
-            this.active = active;
82
-        });
83
-    }
84
-
85
-    /**
86
-     * Checks whether the passed user supports remote control or not.
87
-     *
88
-     * @param {JitsiParticipant} user - The user to be tested.
89
-     * @returns {Promise<boolean>} The promise will be resolved with true if
90
-     * the user supports remote control and with false if not.
91
-     */
92
-    checkUserRemoteControlSupport(user: Object) {
93
-        return user.getFeatures()
94
-            .then(features => features.has(DISCO_REMOTE_CONTROL_FEATURE));
95
-    }
96
-}
97
-
98
-export default new RemoteControl();

+ 0
- 72
modules/remotecontrol/RemoteControlParticipant.js Näytä tiedosto

1
-/* @flow */
2
-
3
-import EventEmitter from 'events';
4
-import { getLogger } from 'jitsi-meet-logger';
5
-
6
-import {
7
-    REMOTE_CONTROL_MESSAGE_NAME
8
-} from '../../service/remotecontrol/Constants';
9
-
10
-const logger = getLogger(__filename);
11
-
12
-declare var APP: Object;
13
-
14
-/**
15
- * Implements common logic for Receiver class and Controller class.
16
- */
17
-export default class RemoteControlParticipant extends EventEmitter {
18
-    _enabled: boolean;
19
-
20
-    /**
21
-     * Creates new instance.
22
-     */
23
-    constructor() {
24
-        super();
25
-        this._enabled = false;
26
-    }
27
-
28
-    /**
29
-     * Enables / Disables the remote control.
30
-     *
31
-     * @param {boolean} enabled - The new state.
32
-     * @returns {void}
33
-     */
34
-    enable(enabled: boolean) {
35
-        this._enabled = enabled;
36
-    }
37
-
38
-    /**
39
-     * Sends remote control message to other participant trough data channel.
40
-     *
41
-     * @param {string} to - The participant who will receive the event.
42
-     * @param {RemoteControlEvent} event - The remote control event.
43
-     * @param {Function} onDataChannelFail - Handler for data channel failure.
44
-     * @returns {void}
45
-     */
46
-    sendRemoteControlEndpointMessage(
47
-            to: ?string,
48
-            event: Object,
49
-            onDataChannelFail: ?Function) {
50
-        if (!this._enabled || !to) {
51
-            logger.warn(
52
-                'Remote control: Skip sending remote control event. Params:',
53
-                this.enable,
54
-                to);
55
-
56
-            return;
57
-        }
58
-        try {
59
-            APP.conference.sendEndpointMessage(to, {
60
-                name: REMOTE_CONTROL_MESSAGE_NAME,
61
-                ...event
62
-            });
63
-        } catch (e) {
64
-            logger.error(
65
-                'Failed to send EndpointMessage via the datachannels',
66
-                e);
67
-            if (typeof onDataChannelFail === 'function') {
68
-                onDataChannelFail(e);
69
-            }
70
-        }
71
-    }
72
-}

+ 1
- 0
react/features/app/middlewares.any.js Näytä tiedosto

37
 import '../recent-list/middleware';
37
 import '../recent-list/middleware';
38
 import '../recording/middleware';
38
 import '../recording/middleware';
39
 import '../rejoin/middleware';
39
 import '../rejoin/middleware';
40
+import '../remote-control/middleware';
40
 import '../room-lock/middleware';
41
 import '../room-lock/middleware';
41
 import '../rtcstats/middleware';
42
 import '../rtcstats/middleware';
42
 import '../subtitles/middleware';
43
 import '../subtitles/middleware';

+ 1
- 0
react/features/app/reducers.any.js Näytä tiedosto

43
 import '../overlay/reducer';
43
 import '../overlay/reducer';
44
 import '../recent-list/reducer';
44
 import '../recent-list/reducer';
45
 import '../recording/reducer';
45
 import '../recording/reducer';
46
+import '../remote-control/reducer';
46
 import '../settings/reducer';
47
 import '../settings/reducer';
47
 import '../subtitles/reducer';
48
 import '../subtitles/reducer';
48
 import '../toolbox/reducer';
49
 import '../toolbox/reducer';

+ 47
- 1
react/features/base/participants/actions.js Näytä tiedosto

16
     PIN_PARTICIPANT,
16
     PIN_PARTICIPANT,
17
     SET_LOADABLE_AVATAR_URL
17
     SET_LOADABLE_AVATAR_URL
18
 } from './actionTypes';
18
 } from './actionTypes';
19
+import { DISCO_REMOTE_CONTROL_FEATURE } from './constants';
19
 import {
20
 import {
20
     getLocalParticipant,
21
     getLocalParticipant,
21
     getNormalizedDisplayName,
22
     getNormalizedDisplayName,
22
-    getParticipantDisplayName
23
+    getParticipantDisplayName,
24
+    getParticipantById
23
 } from './functions';
25
 } from './functions';
26
+import logger from './logger';
24
 
27
 
25
 /**
28
 /**
26
  * Create an action for when dominant speaker changes.
29
  * Create an action for when dominant speaker changes.
272
     };
275
     };
273
 }
276
 }
274
 
277
 
278
+/**
279
+ * Updates the features of a remote participant.
280
+ *
281
+ * @param {JitsiParticipant} jitsiParticipant - The ID of the participant.
282
+ * @returns {{
283
+*     type: PARTICIPANT_UPDATED,
284
+*     participant: Participant
285
+* }}
286
+*/
287
+export function updateRemoteParticipantFeatures(jitsiParticipant) {
288
+    return (dispatch, getState) => {
289
+        if (!jitsiParticipant) {
290
+            return;
291
+        }
292
+
293
+        const id = jitsiParticipant.getId();
294
+
295
+        jitsiParticipant.getFeatures()
296
+            .then(features => {
297
+                const supportsRemoteControl = features.has(DISCO_REMOTE_CONTROL_FEATURE);
298
+                const participant = getParticipantById(getState(), id);
299
+
300
+                if (!participant || participant.local) {
301
+                    return;
302
+                }
303
+
304
+                if (participant?.supportsRemoteControl !== supportsRemoteControl) {
305
+                    return dispatch({
306
+                        type: PARTICIPANT_UPDATED,
307
+                        participant: {
308
+                            id,
309
+                            supportsRemoteControl
310
+                        }
311
+                    });
312
+                }
313
+            })
314
+            .catch(error => {
315
+                logger.error(`Failed to get participant features for ${id}!`, error);
316
+            });
317
+    };
318
+}
319
+
275
 /**
320
 /**
276
  * Action to signal that a hidden participant has joined the conference.
321
  * Action to signal that a hidden participant has joined the conference.
277
  *
322
  *
495
         }
540
         }
496
     };
541
     };
497
 }
542
 }
543
+

+ 5
- 0
react/features/base/participants/constants.js Näytä tiedosto

17
  */
17
  */
18
 export const DEFAULT_AVATAR_RELATIVE_PATH = 'images/avatar.png';
18
 export const DEFAULT_AVATAR_RELATIVE_PATH = 'images/avatar.png';
19
 
19
 
20
+/**
21
+ * The value for the "var" attribute of feature tag in disco-info packets.
22
+ */
23
+export const DISCO_REMOTE_CONTROL_FEATURE = 'http://jitsi.org/meet/remotecontrol';
24
+
20
 /**
25
 /**
21
  * Icon URL for jigasi participants.
26
  * Icon URL for jigasi participants.
22
  *
27
  *

+ 5
- 0
react/features/base/participants/logger.js Näytä tiedosto

1
+// @flow
2
+
3
+import { getLogger } from '../logging/functions';
4
+
5
+export default getLogger('features/base/participants');

+ 7
- 0
react/features/base/participants/middleware.js Näytä tiedosto

239
                         _raiseHandUpdated(store, conference, participant.getId(), newValue);
239
                         _raiseHandUpdated(store, conference, participant.getId(), newValue);
240
                         break;
240
                         break;
241
                     }
241
                     }
242
+                    case 'remoteControlSessionStatus':
243
+                        store.dispatch(participantUpdated({
244
+                            conference,
245
+                            id: participant.getId(),
246
+                            remoteControlSessionStatus: newValue
247
+                        }));
248
+                        break;
242
                     default:
249
                     default:
243
 
250
 
244
                         // Ignore for now.
251
                         // Ignore for now.

+ 70
- 0
react/features/remote-control/actionTypes.js Näytä tiedosto

1
+// @flow
2
+
3
+/**
4
+ * The type of (redux) action which signals that the controller is capturing mouse and keyboard events.
5
+ *
6
+ * {
7
+ *     type: CAPTURE_EVENTS,
8
+ *     isCapturingEvents: boolean
9
+ * }
10
+ */
11
+export const CAPTURE_EVENTS = 'CAPTURE_EVENTS';
12
+
13
+/**
14
+ * The type of (redux) action which signals that a remote control active state has changed.
15
+ *
16
+ * {
17
+ *     type: REMOTE_CONTROL_ACTIVE,
18
+ *     active: boolean
19
+ * }
20
+ */
21
+export const REMOTE_CONTROL_ACTIVE = 'REMOTE_CONTROL_ACTIVE';
22
+
23
+/**
24
+ * The type of (redux) action which sets the receiver transport object.
25
+ *
26
+ * {
27
+ *     type: SET_RECEIVER_TRANSPORT,
28
+ *     transport: Transport
29
+ * }
30
+ */
31
+export const SET_RECEIVER_TRANSPORT = 'SET_RECEIVER_TRANSPORT';
32
+
33
+/**
34
+ * The type of (redux) action which enables the receiver.
35
+ *
36
+ * {
37
+ *     type: SET_RECEIVER_ENABLED,
38
+ *     enabled: boolean
39
+ * }
40
+ */
41
+export const SET_RECEIVER_ENABLED = 'SET_RECEIVER_ENABLED';
42
+
43
+/**
44
+ * The type of (redux) action which sets the controller participant on the receiver side.
45
+ * {
46
+ *     type: SET_CONTROLLER,
47
+ *     controller: string
48
+ * }
49
+ */
50
+export const SET_CONTROLLER = 'SET_CONTROLLER';
51
+
52
+/**
53
+ * The type of (redux) action which sets the controlled participant on the controller side.
54
+ * {
55
+ *     type: SET_CONTROLLED_PARTICIPANT,
56
+ *     controlled: string
57
+ * }
58
+ */
59
+export const SET_CONTROLLED_PARTICIPANT = 'SET_CONTROLLED_PARTICIPANT';
60
+
61
+
62
+/**
63
+ * The type of (redux) action which sets the requested participant on the controller side.
64
+ * {
65
+ *     type: SET_REQUESTED_PARTICIPANT,
66
+ *     requestedParticipant: string
67
+ * }
68
+ */
69
+export const SET_REQUESTED_PARTICIPANT = 'SET_REQUESTED_PARTICIPANT';
70
+

+ 733
- 1
react/features/remote-control/actions.js Näytä tiedosto

1
+// @flow
2
+
1
 import { openDialog } from '../base/dialog';
3
 import { openDialog } from '../base/dialog';
4
+import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
5
+import { getParticipantDisplayName, getPinnedParticipant, pinParticipant } from '../base/participants';
6
+import { getLocalVideoTrack } from '../base/tracks';
7
+import { showNotification } from '../notifications';
2
 
8
 
9
+import {
10
+    CAPTURE_EVENTS,
11
+    REMOTE_CONTROL_ACTIVE,
12
+    SET_REQUESTED_PARTICIPANT,
13
+    SET_CONTROLLER,
14
+    SET_RECEIVER_ENABLED,
15
+    SET_RECEIVER_TRANSPORT,
16
+    SET_CONTROLLED_PARTICIPANT
17
+} from './actionTypes';
3
 import { RemoteControlAuthorizationDialog } from './components';
18
 import { RemoteControlAuthorizationDialog } from './components';
19
+import {
20
+    DISCO_REMOTE_CONTROL_FEATURE,
21
+    EVENTS,
22
+    REMOTE_CONTROL_MESSAGE_NAME,
23
+    PERMISSIONS_ACTIONS,
24
+    REQUESTS
25
+} from './constants';
26
+import {
27
+    getKey,
28
+    getModifiers,
29
+    getRemoteConrolEventCaptureArea,
30
+    isRemoteControlEnabled,
31
+    sendRemoteControlEndpointMessage
32
+} from './functions';
33
+import logger from './logger';
34
+
35
+/**
36
+ * Listeners.
37
+ */
38
+let permissionsReplyListener, receiverEndpointMessageListener, stopListener;
39
+
40
+declare var APP: Object;
41
+declare var $: Function;
4
 
42
 
5
 /**
43
 /**
6
  * Signals that the remote control authorization dialog should be displayed.
44
  * Signals that the remote control authorization dialog should be displayed.
16
  * }}
54
  * }}
17
  * @public
55
  * @public
18
  */
56
  */
19
-export function openRemoteControlAuthorizationDialog(participantId) {
57
+export function openRemoteControlAuthorizationDialog(participantId: string) {
20
     return openDialog(RemoteControlAuthorizationDialog, { participantId });
58
     return openDialog(RemoteControlAuthorizationDialog, { participantId });
21
 }
59
 }
60
+
61
+/**
62
+ * Sets the remote control active property.
63
+ *
64
+ * @param {boolean} active - The new value for the active property.
65
+ * @returns {Function}
66
+ */
67
+export function setRemoteControlActive(active: boolean) {
68
+    return (dispatch: Function, getState: Function) => {
69
+        const state = getState();
70
+        const { active: oldActive } = state['features/remote-control'];
71
+        const { conference } = state['features/base/conference'];
72
+
73
+        if (active !== oldActive) {
74
+            dispatch({
75
+                type: REMOTE_CONTROL_ACTIVE,
76
+                active
77
+            });
78
+            conference.setLocalParticipantProperty('remoteControlSessionStatus', active);
79
+        }
80
+    };
81
+}
82
+
83
+/**
84
+ * Requests permissions from the remote control receiver side.
85
+ *
86
+ * @param {string} userId - The user id of the participant that will be
87
+ * requested.
88
+ * @returns {Function}
89
+ */
90
+export function requestRemoteControl(userId: string) {
91
+    return (dispatch: Function, getState: Function) => {
92
+        const state = getState();
93
+        const enabled = isRemoteControlEnabled(state);
94
+
95
+        if (!enabled) {
96
+            return Promise.reject(new Error('Remote control is disabled!'));
97
+        }
98
+
99
+        dispatch(setRemoteControlActive(true));
100
+
101
+        logger.log(`Requsting remote control permissions from: ${userId}`);
102
+
103
+        const { conference } = state['features/base/conference'];
104
+
105
+
106
+        permissionsReplyListener = (participant, event) => {
107
+            dispatch(processPermissionRequestReply(participant.getId(), event));
108
+        };
109
+
110
+        conference.on(JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, permissionsReplyListener);
111
+
112
+        dispatch({
113
+            type: SET_REQUESTED_PARTICIPANT,
114
+            requestedParticipant: userId
115
+        });
116
+
117
+        if (!sendRemoteControlEndpointMessage(
118
+            conference,
119
+            userId,
120
+            {
121
+                type: EVENTS.permissions,
122
+                action: PERMISSIONS_ACTIONS.request
123
+            })) {
124
+            dispatch(clearRequest());
125
+        }
126
+    };
127
+}
128
+
129
+/**
130
+ * Handles permission request replies on the controller side.
131
+ *
132
+ * @param {string} participantId - The participant that sent the request.
133
+ * @param {EndpointMessage} event - The permission request event.
134
+ * @returns {Function}
135
+ */
136
+export function processPermissionRequestReply(participantId: string, event: Object) {
137
+    return (dispatch: Function, getState: Function) => {
138
+        const state = getState();
139
+        const { action, name, type } = event;
140
+        const { requestedParticipant } = state['features/remote-control'].controller;
141
+
142
+        if (isRemoteControlEnabled(state) && name === REMOTE_CONTROL_MESSAGE_NAME && type === EVENTS.permissions
143
+                && participantId === requestedParticipant) {
144
+            let descriptionKey, permissionGranted = false;
145
+
146
+            switch (action) {
147
+            case PERMISSIONS_ACTIONS.grant: {
148
+                dispatch({
149
+                    type: SET_CONTROLLED_PARTICIPANT,
150
+                    controlled: participantId
151
+                });
152
+
153
+                logger.log('Remote control permissions granted!', participantId);
154
+                logger.log('Starting remote control controller.');
155
+
156
+                const { conference } = state['features/base/conference'];
157
+
158
+                stopListener = (participant, stopEvent) => {
159
+                    dispatch(handleRemoteControlStoppedEvent(participant.getId(), stopEvent));
160
+                };
161
+
162
+                conference.on(JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, stopListener);
163
+
164
+                dispatch(resume());
165
+
166
+                permissionGranted = true;
167
+                descriptionKey = 'dialog.remoteControlAllowedMessage';
168
+                break;
169
+            }
170
+            case PERMISSIONS_ACTIONS.deny:
171
+                logger.log('Remote control permissions denied!', participantId);
172
+                descriptionKey = 'dialog.remoteControlDeniedMessage';
173
+                break;
174
+            case PERMISSIONS_ACTIONS.error:
175
+                logger.error('Error occurred on receiver side');
176
+                descriptionKey = 'dialog.remoteControlErrorMessage';
177
+                break;
178
+            default:
179
+                logger.error('Unknown reply received!');
180
+                descriptionKey = 'dialog.remoteControlErrorMessage';
181
+            }
182
+
183
+            dispatch(clearRequest());
184
+
185
+            if (!permissionGranted) {
186
+                dispatch(setRemoteControlActive(false));
187
+            }
188
+
189
+            dispatch(showNotification({
190
+                descriptionArguments: { user: getParticipantDisplayName(state, participantId) },
191
+                descriptionKey,
192
+                titleKey: 'dialog.remoteControlTitle'
193
+            }));
194
+
195
+            if (permissionGranted) {
196
+                // the remote control permissions has been granted
197
+                // pin the controlled participant
198
+                const pinnedParticipant = getPinnedParticipant(state);
199
+                const pinnedId = pinnedParticipant?.id;
200
+
201
+                if (pinnedId !== participantId) {
202
+                    dispatch(pinParticipant(participantId));
203
+                }
204
+            }
205
+        } else {
206
+            // different message type or another user -> ignoring the message
207
+        }
208
+    };
209
+}
210
+
211
+/**
212
+ * Handles remote control stopped.
213
+ *
214
+ * @param {string} participantId - The ID of the participant that has sent the event.
215
+ * @param {EndpointMessage} event - EndpointMessage event from the data channels.
216
+ * @property {string} type - The function process only events with name REMOTE_CONTROL_MESSAGE_NAME.
217
+ * @returns {void}
218
+ */
219
+export function handleRemoteControlStoppedEvent(participantId: Object, event: Object) {
220
+    return (dispatch: Function, getState: Function) => {
221
+        const state = getState();
222
+        const { name, type } = event;
223
+        const { controlled } = state['features/remote-control'].controller;
224
+
225
+        if (isRemoteControlEnabled(state) && name === REMOTE_CONTROL_MESSAGE_NAME && type === EVENTS.stop
226
+                && participantId === controlled) {
227
+            dispatch(stopController());
228
+        }
229
+    };
230
+}
231
+
232
+/**
233
+ * Stops processing the mouse and keyboard events. Removes added listeners.
234
+ * Enables the keyboard shortcuts. Displays dialog to notify the user that remote control session has ended.
235
+ *
236
+ * @param {boolean} notifyRemoteParty - If true a endpoint message to the controlled participant will be sent.
237
+ * @returns {void}
238
+ */
239
+export function stopController(notifyRemoteParty: boolean = false) {
240
+    return (dispatch: Function, getState: Function) => {
241
+        const state = getState();
242
+        const { controlled } = state['features/remote-control'].controller;
243
+
244
+        if (!controlled) {
245
+            return;
246
+        }
247
+
248
+        const { conference } = state['features/base/conference'];
249
+
250
+        if (notifyRemoteParty) {
251
+            sendRemoteControlEndpointMessage(conference, controlled, {
252
+                type: EVENTS.stop
253
+            });
254
+        }
255
+
256
+        logger.log('Stopping remote control controller.');
257
+
258
+        conference.off(JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, stopListener);
259
+        stopListener = undefined;
260
+
261
+        dispatch(pause());
262
+
263
+        dispatch({
264
+            type: SET_CONTROLLED_PARTICIPANT,
265
+            controlled: undefined
266
+        });
267
+
268
+        dispatch(setRemoteControlActive(false));
269
+        dispatch(showNotification({
270
+            descriptionKey: 'dialog.remoteControlStopMessage',
271
+            titleKey: 'dialog.remoteControlTitle'
272
+        }));
273
+    };
274
+}
275
+
276
+/**
277
+ * Clears a pending permission request.
278
+ *
279
+ * @returns {Function}
280
+ */
281
+export function clearRequest() {
282
+    return (dispatch: Function, getState: Function) => {
283
+        const { conference } = getState()['features/base/conference'];
284
+
285
+        dispatch({
286
+            type: SET_REQUESTED_PARTICIPANT,
287
+            requestedParticipant: undefined
288
+        });
289
+
290
+        conference.off(JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, permissionsReplyListener);
291
+        permissionsReplyListener = undefined;
292
+    };
293
+}
294
+
295
+
296
+/**
297
+ * Sets that trasnport object that is used by the receiver to communicate with the native part of the remote control
298
+ * implementation.
299
+ *
300
+ * @param {Transport} transport - The transport to be set.
301
+ * @returns {{
302
+ *      type: SET_RECEIVER_TRANSPORT,
303
+ *      transport: Transport
304
+ * }}
305
+ */
306
+export function setReceiverTransport(transport: Object) {
307
+    return {
308
+        type: SET_RECEIVER_TRANSPORT,
309
+        transport
310
+    };
311
+}
312
+
313
+/**
314
+ * Enables the receiver functionality.
315
+ *
316
+ * @returns {Function}
317
+ */
318
+export function enableReceiver() {
319
+    return (dispatch: Function, getState: Function) => {
320
+        const state = getState();
321
+        const { enabled } = state['features/remote-control'].receiver;
322
+
323
+        if (enabled) {
324
+            return;
325
+        }
326
+
327
+        const { connection } = state['features/base/connection'];
328
+        const { conference } = state['features/base/conference'];
329
+
330
+        if (!connection || !conference) {
331
+            logger.error('Couldn\'t enable the remote receiver! The connection or conference instance is undefined!');
332
+
333
+            return;
334
+        }
335
+
336
+        dispatch({
337
+            type: SET_RECEIVER_ENABLED,
338
+            enabled: true
339
+        });
340
+
341
+        connection.addFeature(DISCO_REMOTE_CONTROL_FEATURE, true);
342
+        receiverEndpointMessageListener = (participant, message) => {
343
+            dispatch(endpointMessageReceived(participant.getId(), message));
344
+        };
345
+        conference.on(JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, receiverEndpointMessageListener);
346
+    };
347
+}
348
+
349
+/**
350
+ * Disables the receiver functionality.
351
+ *
352
+ * @returns {Function}
353
+ */
354
+export function disableReceiver() {
355
+    return (dispatch: Function, getState: Function) => {
356
+        const state = getState();
357
+        const { enabled } = state['features/remote-control'].receiver;
358
+
359
+        if (!enabled) {
360
+            return;
361
+        }
362
+
363
+        const { connection } = state['features/base/connection'];
364
+        const { conference } = state['features/base/conference'];
365
+
366
+        if (!connection || !conference) {
367
+            logger.error('Couldn\'t enable the remote receiver! The connection or conference instance is undefined!');
368
+
369
+            return;
370
+        }
371
+
372
+        logger.log('Remote control receiver disabled.');
373
+
374
+        dispatch({
375
+            type: SET_RECEIVER_ENABLED,
376
+            enabled: false
377
+        });
378
+
379
+        dispatch(stopReceiver(true));
380
+
381
+        connection.removeFeature(DISCO_REMOTE_CONTROL_FEATURE);
382
+        conference.off(JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, receiverEndpointMessageListener);
383
+    };
384
+}
385
+
386
+/**
387
+ * Stops a remote control session on the receiver side.
388
+ *
389
+ * @param {boolean} [dontNotifyLocalParty] - If true - a notification about stopping
390
+ * the remote control won't be displayed.
391
+ * @param {boolean} [dontNotifyRemoteParty] - If true a endpoint message to the controller participant will be sent.
392
+ * @returns {Function}
393
+ */
394
+export function stopReceiver(dontNotifyLocalParty: boolean = false, dontNotifyRemoteParty: boolean = false) {
395
+    return (dispatch: Function, getState: Function) => {
396
+        const state = getState();
397
+        const { receiver } = state['features/remote-control'];
398
+        const { controller, transport } = receiver;
399
+
400
+        if (!controller) {
401
+            return;
402
+        }
403
+
404
+        const { conference } = state['features/base/conference'];
405
+
406
+        if (!dontNotifyRemoteParty) {
407
+            sendRemoteControlEndpointMessage(conference, controller, {
408
+                type: EVENTS.stop
409
+            });
410
+        }
411
+
412
+        dispatch({
413
+            type: SET_CONTROLLER,
414
+            controller: undefined
415
+        });
416
+
417
+        transport.sendEvent({
418
+            name: REMOTE_CONTROL_MESSAGE_NAME,
419
+            type: EVENTS.stop
420
+        });
421
+
422
+        dispatch(setRemoteControlActive(false));
423
+
424
+        if (!dontNotifyLocalParty) {
425
+            dispatch(showNotification({
426
+                descriptionKey: 'dialog.remoteControlStopMessage',
427
+                titleKey: 'dialog.remoteControlTitle'
428
+            }));
429
+        }
430
+    };
431
+}
432
+
433
+
434
+/**
435
+ * Handles only remote control endpoint messages.
436
+ *
437
+ * @param {string} participantId - The controller participant ID.
438
+ * @param {Object} message - EndpointMessage from the data channels.
439
+ * @param {string} message.name - The function processes only messages with
440
+ * name REMOTE_CONTROL_MESSAGE_NAME.
441
+ * @returns {Function}
442
+ */
443
+export function endpointMessageReceived(participantId: string, message: Object) {
444
+    return (dispatch: Function, getState: Function) => {
445
+        const { action, name, type } = message;
446
+
447
+        if (name !== REMOTE_CONTROL_MESSAGE_NAME) {
448
+            return;
449
+        }
450
+
451
+        const state = getState();
452
+        const { receiver } = state['features/remote-control'];
453
+        const { enabled, transport } = receiver;
454
+
455
+        if (enabled) {
456
+            const { controller } = receiver;
457
+
458
+            if (!controller && type === EVENTS.permissions && action === PERMISSIONS_ACTIONS.request) {
459
+                dispatch(setRemoteControlActive(true));
460
+                dispatch(openRemoteControlAuthorizationDialog(participantId));
461
+            } else if (controller === participantId) {
462
+                if (type === EVENTS.stop) {
463
+                    dispatch(stopReceiver(false, true));
464
+                } else { // forward the message
465
+                    transport.sendEvent(message);
466
+                }
467
+            } // else ignore
468
+        } else {
469
+            logger.log('Remote control message is ignored because remote control is disabled', message);
470
+        }
471
+    };
472
+}
473
+
474
+/**
475
+ * Denies remote control access for user associated with the passed user id.
476
+ *
477
+ * @param {string} participantId - The id associated with the user who sent the
478
+ * request for remote control authorization.
479
+ * @returns {Function}
480
+ */
481
+export function deny(participantId: string) {
482
+    return (dispatch: Function, getState: Function) => {
483
+        const state = getState();
484
+        const { conference } = state['features/base/conference'];
485
+
486
+        dispatch(setRemoteControlActive(false));
487
+        sendRemoteControlEndpointMessage(conference, participantId, {
488
+            type: EVENTS.permissions,
489
+            action: PERMISSIONS_ACTIONS.deny
490
+        });
491
+    };
492
+}
493
+
494
+/**
495
+ * Sends start remote control request to the native implementation.
496
+ *
497
+ * @returns {Function}
498
+ */
499
+export function sendStartRequest() {
500
+    return (dispatch: Function, getState: Function) => {
501
+        const state = getState();
502
+        const tracks = state['features/base/tracks'];
503
+        const track = getLocalVideoTrack(tracks);
504
+        const { sourceId } = track?.jitsiTrack || {};
505
+        const { transport } = state['features/remote-control'].receiver;
506
+
507
+        return transport.sendRequest({
508
+            name: REMOTE_CONTROL_MESSAGE_NAME,
509
+            type: REQUESTS.start,
510
+            sourceId
511
+        });
512
+    };
513
+}
514
+
515
+/**
516
+ * Grants remote control access to user associated with the passed user id.
517
+ *
518
+ * @param {string} participantId - The id associated with the user who sent the
519
+ * request for remote control authorization.
520
+ * @returns {Function}
521
+ */
522
+export function grant(participantId: string) {
523
+    return (dispatch: Function, getState: Function) => {
524
+        dispatch({
525
+            type: SET_CONTROLLER,
526
+            controller: participantId
527
+        });
528
+        logger.log(`Remote control permissions granted to: ${participantId}`);
529
+
530
+        let promise;
531
+        const state = getState();
532
+        const tracks = state['features/base/tracks'];
533
+        const track = getLocalVideoTrack(tracks);
534
+        const isScreenSharing = track?.videoType === 'desktop';
535
+        const { sourceType } = track?.jitsiTrack || {};
536
+
537
+        if (isScreenSharing && sourceType === 'screen') {
538
+            promise = dispatch(sendStartRequest());
539
+        } else {
540
+            // FIXME: Use action here once toggleScreenSharing is moved to redux.
541
+            promise = APP.conference.toggleScreenSharing(
542
+                true,
543
+                {
544
+                    desktopSharingSources: [ 'screen' ]
545
+                })
546
+                .then(() => dispatch(sendStartRequest()));
547
+        }
548
+
549
+        const { conference } = state['features/base/conference'];
550
+
551
+        promise
552
+            .then(() => sendRemoteControlEndpointMessage(conference, participantId, {
553
+                type: EVENTS.permissions,
554
+                action: PERMISSIONS_ACTIONS.grant
555
+            }))
556
+            .catch(error => {
557
+                logger.error(error);
558
+
559
+                sendRemoteControlEndpointMessage(conference, participantId, {
560
+                    type: EVENTS.permissions,
561
+                    action: PERMISSIONS_ACTIONS.error
562
+                });
563
+
564
+                dispatch(showNotification({
565
+                    descriptionKey: 'dialog.startRemoteControlErrorMessage',
566
+                    titleKey: 'dialog.remoteControlTitle'
567
+                }));
568
+
569
+                dispatch(stopReceiver(true));
570
+            });
571
+    };
572
+}
573
+
574
+/**
575
+ * Handler for mouse click events on the controller side.
576
+ *
577
+ * @param {string} type - The type of event ("mousedown"/"mouseup").
578
+ * @param {Event} event - The mouse event.
579
+ * @returns {Function}
580
+ */
581
+export function mouseClicked(type: string, event: Object) {
582
+    return (dispatch: Function, getState: Function) => {
583
+        const state = getState();
584
+        const { conference } = state['features/base/conference'];
585
+        const { controller } = state['features/remote-control'];
586
+
587
+        sendRemoteControlEndpointMessage(conference, controller.controlled, {
588
+            type,
589
+            button: event.which
590
+        });
591
+    };
592
+}
593
+
594
+/**
595
+ * Handles mouse moved events on the controller side.
596
+ *
597
+ * @param {Event} event - The mouse event.
598
+ * @returns {Function}
599
+ */
600
+export function mouseMoved(event: Object) {
601
+    return (dispatch: Function, getState: Function) => {
602
+        const area = getRemoteConrolEventCaptureArea();
603
+
604
+        if (!area) {
605
+            return;
606
+        }
607
+
608
+        const position = area.position();
609
+        const state = getState();
610
+        const { conference } = state['features/base/conference'];
611
+        const { controller } = state['features/remote-control'];
612
+
613
+        sendRemoteControlEndpointMessage(conference, controller.controlled, {
614
+            type: EVENTS.mousemove,
615
+            x: (event.pageX - position.left) / area.width(),
616
+            y: (event.pageY - position.top) / area.height()
617
+        });
618
+    };
619
+}
620
+
621
+/**
622
+ * Handles mouse scroll events on the controller side.
623
+ *
624
+ * @param {Event} event - The mouse event.
625
+ * @returns {Function}
626
+ */
627
+export function mouseScrolled(event: Object) {
628
+    return (dispatch: Function, getState: Function) => {
629
+        const state = getState();
630
+        const { conference } = state['features/base/conference'];
631
+        const { controller } = state['features/remote-control'];
632
+
633
+        sendRemoteControlEndpointMessage(conference, controller.controlled, {
634
+            type: EVENTS.mousescroll,
635
+            x: event.deltaX,
636
+            y: event.deltaY
637
+        });
638
+    };
639
+}
640
+
641
+/**
642
+ * Handles key press events on the controller side..
643
+ *
644
+ * @param {string} type - The type of event ("keydown"/"keyup").
645
+ * @param {Event} event - The key event.
646
+ * @returns {Function}
647
+ */
648
+export function keyPressed(type: string, event: Object) {
649
+    return (dispatch: Function, getState: Function) => {
650
+        const state = getState();
651
+        const { conference } = state['features/base/conference'];
652
+        const { controller } = state['features/remote-control'];
653
+
654
+        sendRemoteControlEndpointMessage(conference, controller.controlled, {
655
+            type,
656
+            key: getKey(event),
657
+            modifiers: getModifiers(event)
658
+        });
659
+    };
660
+}
661
+
662
+/**
663
+* Disables the keyboatd shortcuts. Starts collecting remote control
664
+* events. It can be used to resume an active remote control session which
665
+* was paused with the pause action.
666
+*
667
+* @returns {Function}
668
+*/
669
+export function resume() {
670
+    return (dispatch: Function, getState: Function) => {
671
+        const area = getRemoteConrolEventCaptureArea();
672
+        const state = getState();
673
+        const { controller } = state['features/remote-control'];
674
+        const { controlled, isCapturingEvents } = controller;
675
+
676
+        if (!isRemoteControlEnabled(state) || !area || !controlled || isCapturingEvents) {
677
+            return;
678
+        }
679
+
680
+        logger.log('Resuming remote control controller.');
681
+
682
+        // FIXME: Once the keyboard shortcuts are using react/redux.
683
+        APP.keyboardshortcut.enable(false);
684
+
685
+        area.mousemove(event => {
686
+            dispatch(mouseMoved(event));
687
+        });
688
+        area.mousedown(event => dispatch(mouseClicked(EVENTS.mousedown, event)));
689
+        area.mouseup(event => dispatch(mouseClicked(EVENTS.mouseup, event)));
690
+        area.dblclick(event => dispatch(mouseClicked(EVENTS.mousedblclick, event)));
691
+        area.contextmenu(() => false);
692
+        area[0].onmousewheel = event => {
693
+            event.preventDefault();
694
+            event.stopPropagation();
695
+            dispatch(mouseScrolled(event));
696
+
697
+            return false;
698
+        };
699
+        $(window).keydown(event => dispatch(keyPressed(EVENTS.keydown, event)));
700
+        $(window).keyup(event => dispatch(keyPressed(EVENTS.keyup, event)));
701
+
702
+        dispatch({
703
+            type: CAPTURE_EVENTS,
704
+            isCapturingEvents: true
705
+        });
706
+    };
707
+}
708
+
709
+
710
+/**
711
+ * Pauses the collecting of events and enables the keyboard shortcus. But
712
+ * it doesn't removes any other listeners. Basically the remote control
713
+ * session will be still active after the pause action, but no events from the
714
+ * controller side will be captured and sent. You can resume the collecting
715
+ * of the events with the resume action.
716
+ *
717
+ * @returns {Function}
718
+ */
719
+export function pause() {
720
+    return (dispatch: Function, getState: Function) => {
721
+        const state = getState();
722
+        const { controller } = state['features/remote-control'];
723
+        const { controlled, isCapturingEvents } = controller;
724
+
725
+        if (!isRemoteControlEnabled(state) || !controlled || !isCapturingEvents) {
726
+            return;
727
+        }
728
+
729
+        logger.log('Pausing remote control controller.');
730
+
731
+        // FIXME: Once the keyboard shortcuts are using react/redux.
732
+        APP.keyboardshortcut.enable(true);
733
+
734
+        const area = getRemoteConrolEventCaptureArea();
735
+
736
+        if (area) {
737
+            area.off('contextmenu');
738
+            area.off('dblclick');
739
+            area.off('mousedown');
740
+            area.off('mousemove');
741
+            area.off('mouseup');
742
+            area[0].onmousewheel = undefined;
743
+        }
744
+
745
+        $(window).off('keydown');
746
+        $(window).off('keyup');
747
+
748
+        dispatch({
749
+            type: CAPTURE_EVENTS,
750
+            isCapturingEvents: false
751
+        });
752
+    };
753
+}

+ 25
- 11
react/features/remote-control/components/RemoteControlAuthorizationDialog.js Näytä tiedosto

6
 import { translate } from '../../base/i18n';
6
 import { translate } from '../../base/i18n';
7
 import { getParticipantById } from '../../base/participants';
7
 import { getParticipantById } from '../../base/participants';
8
 import { connect } from '../../base/redux';
8
 import { connect } from '../../base/redux';
9
+import { getLocalVideoTrack } from '../../base/tracks';
10
+import { grant, deny } from '../actions';
9
 
11
 
10
 declare var APP: Object;
12
 declare var APP: Object;
11
 
13
 
21
      */
23
      */
22
     _displayName: string,
24
     _displayName: string,
23
 
25
 
26
+    _isScreenSharing: boolean,
27
+    _sourceType: string,
28
+
24
     /**
29
     /**
25
      * Used to show/hide the dialog on cancel.
30
      * Used to show/hide the dialog on cancel.
26
      */
31
      */
87
      * @returns {ReactElement}
92
      * @returns {ReactElement}
88
      */
93
      */
89
     _getAdditionalMessage() {
94
     _getAdditionalMessage() {
90
-        // FIXME: Once we have this information in redux we should
91
-        // start getting it from there.
92
-        if (APP.conference.isSharingScreen
93
-                && APP.conference.getDesktopSharingSourceType() === 'screen') {
95
+        const { _isScreenSharing, _sourceType } = this.props;
96
+
97
+        if (_isScreenSharing && _sourceType === 'screen') {
94
             return null;
98
             return null;
95
         }
99
         }
96
 
100
 
112
      * @returns {boolean} Returns true to close the dialog.
116
      * @returns {boolean} Returns true to close the dialog.
113
      */
117
      */
114
     _onCancel() {
118
     _onCancel() {
115
-        // FIXME: This should be action one day.
116
-        APP.remoteControl.receiver.deny(this.props.participantId);
119
+        const { dispatch, participantId } = this.props;
120
+
121
+        dispatch(deny(participantId));
117
 
122
 
118
         return true;
123
         return true;
119
     }
124
     }
131
      * picker window, the action will be ignored).
136
      * picker window, the action will be ignored).
132
      */
137
      */
133
     _onSubmit() {
138
     _onSubmit() {
134
-        this.props.dispatch(hideDialog());
139
+        const { dispatch, participantId } = this.props;
135
 
140
 
136
-        // FIXME: This should be action one day.
137
-        APP.remoteControl.receiver.grant(this.props.participantId);
141
+        dispatch(hideDialog());
142
+        dispatch(grant(participantId));
138
 
143
 
139
         return false;
144
         return false;
140
     }
145
     }
149
  * (instance of) RemoteControlAuthorizationDialog.
154
  * (instance of) RemoteControlAuthorizationDialog.
150
  * @private
155
  * @private
151
  * @returns {{
156
  * @returns {{
152
- *     _displayName: string
157
+ *     _displayName: string,
158
+ *     _isScreenSharing: boolean,
159
+ *     _sourceId: string,
160
+ *     _sourceType: string
153
  * }}
161
  * }}
154
  */
162
  */
155
 function _mapStateToProps(state, ownProps) {
163
 function _mapStateToProps(state, ownProps) {
156
     const { _displayName, participantId } = ownProps;
164
     const { _displayName, participantId } = ownProps;
157
     const participant = getParticipantById(state, participantId);
165
     const participant = getParticipantById(state, participantId);
166
+    const tracks = state['features/base/tracks'];
167
+    const track = getLocalVideoTrack(tracks);
168
+    const _isScreenSharing = track?.videoType === 'desktop';
169
+    const { sourceType } = track?.jitsiTrack || {};
158
 
170
 
159
     return {
171
     return {
160
-        _displayName: participant ? participant.name : _displayName
172
+        _displayName: participant ? participant.name : _displayName,
173
+        _isScreenSharing,
174
+        _sourceType: sourceType
161
     };
175
     };
162
 }
176
 }
163
 
177
 

service/remotecontrol/Constants.js → react/features/remote-control/constants.js Näytä tiedosto

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

+ 128
- 0
react/features/remote-control/functions.js Näytä tiedosto

1
+// @flow
2
+
3
+import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
4
+import JitsiMeetJS from '../base/lib-jitsi-meet';
5
+
6
+import { enableReceiver, stopReceiver } from './actions';
7
+import { REMOTE_CONTROL_MESSAGE_NAME, EVENTS } from './constants';
8
+import { keyboardEventToKey } from './keycodes';
9
+import logger from './logger';
10
+
11
+/**
12
+ * Checks if the remote contrrol is enabled.
13
+ *
14
+ * @param {*} state - The redux state.
15
+ * @returns {boolean} - True if the remote control is enabled and false otherwise.
16
+ */
17
+export function isRemoteControlEnabled(state: Object) {
18
+    return !state['features/base/config'].disableRemoteControl && JitsiMeetJS.isDesktopSharingEnabled();
19
+}
20
+
21
+/**
22
+ * Sends remote control message to other participant trough data channel.
23
+ *
24
+ * @param {JitsiConference} conference - The JitsiConference object.
25
+ * @param {string} to - The participant who will receive the event.
26
+ * @param {RemoteControlEvent} event - The remote control event.
27
+ * @returns {boolean} - True if the message was sent successfully and false otherwise.
28
+ */
29
+export function sendRemoteControlEndpointMessage(
30
+        conference: Object,
31
+        to: ?string,
32
+        event: Object) {
33
+    if (!to) {
34
+        logger.warn('Remote control: Skip sending remote control event. Params:', to);
35
+
36
+        return false;
37
+    }
38
+
39
+    try {
40
+        conference.sendEndpointMessage(to, {
41
+            name: REMOTE_CONTROL_MESSAGE_NAME,
42
+            ...event
43
+        });
44
+
45
+        return true;
46
+    } catch (error) {
47
+        logger.error('Failed to send EndpointMessage via the datachannels', error);
48
+
49
+        return false;
50
+    }
51
+}
52
+
53
+/**
54
+* Handles remote control events from the external app. Currently only
55
+* events with type EVENTS.supported and EVENTS.stop are
56
+* supported.
57
+*
58
+* @param {RemoteControlEvent} event - The remote control event.
59
+* @param {Store} store - The redux store.
60
+* @returns {void}
61
+*/
62
+export function onRemoteControlAPIEvent(event: Object, { getState, dispatch }: Object) {
63
+    switch (event.type) {
64
+    case EVENTS.supported:
65
+        logger.log('Remote Control supported.');
66
+        if (isRemoteControlEnabled(getState())) {
67
+            dispatch(enableReceiver());
68
+        } else {
69
+            logger.log('Remote Control disabled.');
70
+        }
71
+        break;
72
+    case EVENTS.stop: {
73
+        dispatch(stopReceiver());
74
+
75
+        break;
76
+    }
77
+    }
78
+}
79
+
80
+/**
81
+ * Returns the area used for capturing mouse and key events.
82
+ *
83
+ * @returns {JQuery} - A JQuery selector.
84
+ */
85
+export function getRemoteConrolEventCaptureArea() {
86
+    return VideoLayout.getLargeVideoWrapper();
87
+}
88
+
89
+
90
+/**
91
+ * Extract the keyboard key from the keyboard event.
92
+ *
93
+ * @param {KeyboardEvent} event - The event.
94
+ * @returns {KEYS} The key that is pressed or undefined.
95
+ */
96
+export function getKey(event: Object) {
97
+    return keyboardEventToKey(event);
98
+}
99
+
100
+/**
101
+ * Extract the modifiers from the keyboard event.
102
+ *
103
+ * @param {KeyboardEvent} event - The event.
104
+ * @returns {Array} With possible values: "shift", "control", "alt", "command".
105
+ */
106
+export function getModifiers(event: Object) {
107
+    const modifiers = [];
108
+
109
+    if (event.shiftKey) {
110
+        modifiers.push('shift');
111
+    }
112
+
113
+    if (event.ctrlKey) {
114
+        modifiers.push('control');
115
+    }
116
+
117
+
118
+    if (event.altKey) {
119
+        modifiers.push('alt');
120
+    }
121
+
122
+    if (event.metaKey) {
123
+        modifiers.push('command');
124
+    }
125
+
126
+    return modifiers;
127
+}
128
+

modules/keycode/keycode.js → react/features/remote-control/keycodes.js Näytä tiedosto

158
 
158
 
159
 /**
159
 /**
160
  * Returns key associated with the keyCode from the passed event.
160
  * Returns key associated with the keyCode from the passed event.
161
- * @param {KeyboardEvent} event the event
162
- * @returns {KEYS} the key on the keyboard.
161
+ *
162
+ * @param {KeyboardEvent} event - The event.
163
+ * @returns {KEYS} - The key on the keyboard.
163
  */
164
  */
164
 export function keyboardEventToKey(event) {
165
 export function keyboardEventToKey(event) {
165
     return keyCodeToKey[event.which];
166
     return keyCodeToKey[event.which];

+ 5
- 0
react/features/remote-control/logger.js Näytä tiedosto

1
+// @flow
2
+
3
+import { getLogger } from '../base/logging/functions';
4
+
5
+export default getLogger('features/remote-control');

+ 92
- 0
react/features/remote-control/middleware.js Näytä tiedosto

1
+// @flow
2
+import { PostMessageTransportBackend, Transport } from '@jitsi/js-utils/transport';
3
+
4
+import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app';
5
+import { CONFERENCE_JOINED } from '../base/conference';
6
+import { PARTICIPANT_LEFT } from '../base/participants';
7
+import { MiddlewareRegistry } from '../base/redux';
8
+
9
+import {
10
+    clearRequest, setReceiverTransport, setRemoteControlActive, stopController, stopReceiver
11
+} from './actions';
12
+import { REMOTE_CONTROL_MESSAGE_NAME } from './constants';
13
+import { onRemoteControlAPIEvent } from './functions';
14
+import './subscriber';
15
+
16
+/**
17
+ * The redux middleware for the remote control feature.
18
+ *
19
+ * @param {Store} store - The redux store.
20
+ * @returns {Function}
21
+ */
22
+MiddlewareRegistry.register(store => next => async action => {
23
+    switch (action.type) {
24
+    case APP_WILL_MOUNT: {
25
+        const { dispatch } = store;
26
+
27
+        dispatch(setReceiverTransport(new Transport({
28
+            backend: new PostMessageTransportBackend({
29
+                postisOptions: { scope: 'jitsi-remote-control' }
30
+            })
31
+        })));
32
+
33
+        break;
34
+    }
35
+    case APP_WILL_UNMOUNT: {
36
+        const { getState, dispatch } = store;
37
+        const { transport } = getState()['features/remote-control'].receiver;
38
+
39
+        if (transport) {
40
+            transport.dispose();
41
+            dispatch(setReceiverTransport());
42
+        }
43
+
44
+        break;
45
+    }
46
+    case CONFERENCE_JOINED: {
47
+        const result = next(action);
48
+        const { getState } = store;
49
+        const { transport } = getState()['features/remote-control'].receiver;
50
+
51
+        if (transport) {
52
+            // We expect here that even if we receive the supported event earlier
53
+            // it will be cached and we'll receive it.
54
+            transport.on('event', event => {
55
+                if (event.name === REMOTE_CONTROL_MESSAGE_NAME) {
56
+                    onRemoteControlAPIEvent(event, store);
57
+
58
+                    return true;
59
+                }
60
+
61
+                return false;
62
+            });
63
+        }
64
+
65
+        return result;
66
+    }
67
+    case PARTICIPANT_LEFT: {
68
+        const { getState, dispatch } = store;
69
+        const state = getState();
70
+        const { id } = action.participant;
71
+        const { receiver, controller } = state['features/remote-control'];
72
+        const { requestedParticipant, controlled } = controller;
73
+
74
+        if (id === controlled) {
75
+            dispatch(stopController());
76
+        }
77
+
78
+        if (id === requestedParticipant) {
79
+            dispatch(clearRequest());
80
+            dispatch(setRemoteControlActive(false));
81
+        }
82
+
83
+        if (receiver?.controller === id) {
84
+            dispatch(stopReceiver(false, true));
85
+        }
86
+
87
+        break;
88
+    }
89
+    }
90
+
91
+    return next(action);
92
+});

+ 68
- 0
react/features/remote-control/reducer.js Näytä tiedosto

1
+import { ReducerRegistry, set } from '../base/redux';
2
+
3
+import {
4
+    CAPTURE_EVENTS,
5
+    REMOTE_CONTROL_ACTIVE,
6
+    SET_CONTROLLED_PARTICIPANT,
7
+    SET_CONTROLLER,
8
+    SET_RECEIVER_ENABLED,
9
+    SET_RECEIVER_TRANSPORT,
10
+    SET_REQUESTED_PARTICIPANT
11
+} from './actionTypes';
12
+
13
+/**
14
+ * The default state.
15
+ */
16
+const DEFAULT_STATE = {
17
+    active: false,
18
+    controller: {
19
+        isCapturingEvents: false
20
+    },
21
+    receiver: {
22
+        enabled: false
23
+    }
24
+};
25
+
26
+/**
27
+ * Listen for actions that mutate the remote control state.
28
+ */
29
+ReducerRegistry.register(
30
+    'features/remote-control', (state = DEFAULT_STATE, action) => {
31
+        switch (action.type) {
32
+        case CAPTURE_EVENTS:
33
+            return {
34
+                ...state,
35
+                controller: set(state.controller, 'isCapturingEvents', action.isCapturingEvents)
36
+            };
37
+        case REMOTE_CONTROL_ACTIVE:
38
+            return set(state, 'active', action.active);
39
+        case SET_RECEIVER_TRANSPORT:
40
+            return {
41
+                ...state,
42
+                receiver: set(state.receiver, 'transport', action.transport)
43
+            };
44
+        case SET_RECEIVER_ENABLED:
45
+            return {
46
+                ...state,
47
+                receiver: set(state.receiver, 'enabled', action.enabled)
48
+            };
49
+        case SET_REQUESTED_PARTICIPANT:
50
+            return {
51
+                ...state,
52
+                controller: set(state.controller, 'requestedParticipant', action.requestedParticipant)
53
+            };
54
+        case SET_CONTROLLED_PARTICIPANT:
55
+            return {
56
+                ...state,
57
+                controller: set(state.controller, 'controlled', action.controlled)
58
+            };
59
+        case SET_CONTROLLER:
60
+            return {
61
+                ...state,
62
+                receiver: set(state.receiver, 'controller', action.controller)
63
+            };
64
+        }
65
+
66
+        return state;
67
+    },
68
+);

+ 33
- 0
react/features/remote-control/subscriber.js Näytä tiedosto

1
+// @flow
2
+
3
+import { StateListenerRegistry } from '../base/redux';
4
+
5
+import { resume, pause } from './actions';
6
+
7
+/**
8
+ * Listens for large video participant ID changes.
9
+ */
10
+StateListenerRegistry.register(
11
+    /* selector */ state => {
12
+        const { participantId } = state['features/large-video'];
13
+        const { controller } = state['features/remote-control'];
14
+        const { controlled } = controller;
15
+
16
+        if (!controlled) {
17
+            return undefined;
18
+        }
19
+
20
+        return controlled === participantId;
21
+    },
22
+    /* listener */ (isControlledParticipantOnStage, { dispatch }) => {
23
+        if (isControlledParticipantOnStage === true) {
24
+            dispatch(resume());
25
+        } else if (isControlledParticipantOnStage === false) {
26
+            dispatch(pause());
27
+        }
28
+
29
+        // else {
30
+        // isControlledParticipantOnStage === undefined. Ignore!
31
+        // }
32
+    }
33
+);

+ 76
- 26
react/features/remote-video-menu/components/web/RemoteVideoMenuTriggerButton.js Näytä tiedosto

4
 
4
 
5
 import { Icon, IconMenuThumb } from '../../../base/icons';
5
 import { Icon, IconMenuThumb } from '../../../base/icons';
6
 import { MEDIA_TYPE } from '../../../base/media';
6
 import { MEDIA_TYPE } from '../../../base/media';
7
-import { getLocalParticipant, PARTICIPANT_ROLE } from '../../../base/participants';
7
+import { getLocalParticipant, getParticipantById, PARTICIPANT_ROLE } from '../../../base/participants';
8
 import { Popover } from '../../../base/popover';
8
 import { Popover } from '../../../base/popover';
9
 import { connect } from '../../../base/redux';
9
 import { connect } from '../../../base/redux';
10
 import { isRemoteTrackMuted } from '../../../base/tracks';
10
 import { isRemoteTrackMuted } from '../../../base/tracks';
11
+import { requestRemoteControl, stopController } from '../../../remote-control';
12
+import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
11
 
13
 
12
 import MuteEveryoneElseButton from './MuteEveryoneElseButton';
14
 import MuteEveryoneElseButton from './MuteEveryoneElseButton';
15
+import { REMOTE_CONTROL_MENU_STATES } from './RemoteControlButton';
13
 
16
 
14
 import {
17
 import {
15
     GrantModeratorButton,
18
     GrantModeratorButton,
51
     _isModerator: boolean,
54
     _isModerator: boolean,
52
 
55
 
53
     /**
56
     /**
54
-     * A value between 0 and 1 indicating the volume of the participant's
55
-     * audio element.
57
+     * The position relative to the trigger the remote menu should display
58
+     * from. Valid values are those supported by AtlasKit
59
+     * {@code InlineDialog}.
56
      */
60
      */
57
-    initialVolumeValue: number,
61
+    _menuPosition: string,
58
 
62
 
59
     /**
63
     /**
60
-     * Callback to invoke when the popover has been displayed.
64
+     * The current state of the participant's remote control session.
61
      */
65
      */
62
-    onMenuDisplay: Function,
66
+    _remoteControlState: number,
67
+
63
 
68
 
64
     /**
69
     /**
65
-     * Callback to invoke choosing to start a remote control session with
66
-     * the participant.
70
+     * The redux dispatch function.
67
      */
71
      */
68
-    onRemoteControlToggle: Function,
72
+    dispatch: Function,
69
 
73
 
70
     /**
74
     /**
71
-     * Callback to invoke when changing the level of the participant's
75
+     * A value between 0 and 1 indicating the volume of the participant's
72
      * audio element.
76
      * audio element.
73
      */
77
      */
74
-    onVolumeChange: Function,
78
+    initialVolumeValue: number,
75
 
79
 
76
     /**
80
     /**
77
-     * The position relative to the trigger the remote menu should display
78
-     * from. Valid values are those supported by AtlasKit
79
-     * {@code InlineDialog}.
81
+     * Callback to invoke when the popover has been displayed.
80
      */
82
      */
81
-    menuPosition: string,
83
+    onMenuDisplay: Function,
82
 
84
 
83
     /**
85
     /**
84
-     * The ID for the participant on which the remote video menu will act.
86
+     * Callback to invoke when changing the level of the participant's
87
+     * audio element.
85
      */
88
      */
86
-    participantID: string,
89
+    onVolumeChange: Function,
87
 
90
 
88
     /**
91
     /**
89
-     * The current state of the participant's remote control session.
92
+     * The ID for the participant on which the remote video menu will act.
90
      */
93
      */
91
-    remoteControlState: number
94
+    participantID: string,
92
 };
95
 };
93
 
96
 
94
 /**
97
 /**
138
             <Popover
141
             <Popover
139
                 content = { content }
142
                 content = { content }
140
                 onPopoverOpen = { this._onShowRemoteMenu }
143
                 onPopoverOpen = { this._onShowRemoteMenu }
141
-                position = { this.props.menuPosition }>
144
+                position = { this.props._menuPosition }>
142
                 <span
145
                 <span
143
                     className = 'popover-trigger remote-video-menu-trigger'>
146
                     className = 'popover-trigger remote-video-menu-trigger'>
144
                     <Icon
147
                     <Icon
175
             _disableRemoteMute,
178
             _disableRemoteMute,
176
             _isAudioMuted,
179
             _isAudioMuted,
177
             _isModerator,
180
             _isModerator,
181
+            dispatch,
178
             initialVolumeValue,
182
             initialVolumeValue,
179
-            onRemoteControlToggle,
180
             onVolumeChange,
183
             onVolumeChange,
181
-            remoteControlState,
184
+            _remoteControlState,
182
             participantID
185
             participantID
183
         } = this.props;
186
         } = this.props;
184
 
187
 
214
             }
217
             }
215
         }
218
         }
216
 
219
 
217
-        if (remoteControlState) {
220
+        if (_remoteControlState) {
221
+            let onRemoteControlToggle = null;
222
+
223
+            if (_remoteControlState === REMOTE_CONTROL_MENU_STATES.STARTED) {
224
+                onRemoteControlToggle = () => dispatch(stopController(true));
225
+            } else if (_remoteControlState === REMOTE_CONTROL_MENU_STATES.NOT_STARTED) {
226
+                onRemoteControlToggle = () => dispatch(requestRemoteControl(participantID));
227
+            }
228
+
218
             buttons.push(
229
             buttons.push(
219
                 <RemoteControlButton
230
                 <RemoteControlButton
220
                     key = 'remote-control'
231
                     key = 'remote-control'
221
                     onClick = { onRemoteControlToggle }
232
                     onClick = { onRemoteControlToggle }
222
                     participantID = { participantID }
233
                     participantID = { participantID }
223
-                    remoteControlState = { remoteControlState } />
234
+                    remoteControlState = { _remoteControlState } />
224
             );
235
             );
225
         }
236
         }
226
 
237
 
258
  * @param {Object} ownProps - The own props of the component.
269
  * @param {Object} ownProps - The own props of the component.
259
  * @private
270
  * @private
260
  * @returns {{
271
  * @returns {{
261
- *     _isModerator: boolean
272
+ *     _isAudioMuted: boolean,
273
+ *     _isModerator: boolean,
274
+ *     _disableKick: boolean,
275
+ *     _disableRemoteMute: boolean,
276
+ *     _menuPosition: string,
277
+ *     _remoteControlState: number
262
  * }}
278
  * }}
263
  */
279
  */
264
 function _mapStateToProps(state, ownProps) {
280
 function _mapStateToProps(state, ownProps) {
267
     const localParticipant = getLocalParticipant(state);
283
     const localParticipant = getLocalParticipant(state);
268
     const { remoteVideoMenu = {}, disableRemoteMute } = state['features/base/config'];
284
     const { remoteVideoMenu = {}, disableRemoteMute } = state['features/base/config'];
269
     const { disableKick } = remoteVideoMenu;
285
     const { disableKick } = remoteVideoMenu;
286
+    let _remoteControlState = null;
287
+    const participant = getParticipantById(state, participantID);
288
+    const _isRemoteControlSessionActive = participant?.remoteControlSessionStatus ?? false;
289
+    const _supportsRemoteControl = participant?.supportsRemoteControl ?? false;
290
+    const { active, controller } = state['features/remote-control'];
291
+    const { requestedParticipant, controlled } = controller;
292
+    const activeParticipant = requestedParticipant || controlled;
293
+
294
+    if (_supportsRemoteControl
295
+            && ((!active && !_isRemoteControlSessionActive) || activeParticipant === participantID)) {
296
+        if (requestedParticipant === participantID) {
297
+            _remoteControlState = REMOTE_CONTROL_MENU_STATES.REQUESTING;
298
+        } else if (controlled) {
299
+            _remoteControlState = REMOTE_CONTROL_MENU_STATES.STARTED;
300
+        } else {
301
+            _remoteControlState = REMOTE_CONTROL_MENU_STATES.NOT_STARTED;
302
+        }
303
+    }
304
+
305
+    const currentLayout = getCurrentLayout(state);
306
+    let _menuPosition;
307
+
308
+    switch (currentLayout) {
309
+    case LAYOUTS.TILE_VIEW:
310
+        _menuPosition = 'left top';
311
+        break;
312
+    case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
313
+        _menuPosition = 'left bottom';
314
+        break;
315
+    default:
316
+        _menuPosition = 'top center';
317
+    }
270
 
318
 
271
     return {
319
     return {
272
         _isAudioMuted: isRemoteTrackMuted(tracks, MEDIA_TYPE.AUDIO, participantID) || false,
320
         _isAudioMuted: isRemoteTrackMuted(tracks, MEDIA_TYPE.AUDIO, participantID) || false,
273
         _isModerator: Boolean(localParticipant?.role === PARTICIPANT_ROLE.MODERATOR),
321
         _isModerator: Boolean(localParticipant?.role === PARTICIPANT_ROLE.MODERATOR),
274
         _disableKick: Boolean(disableKick),
322
         _disableKick: Boolean(disableKick),
275
-        _disableRemoteMute: Boolean(disableRemoteMute)
323
+        _disableRemoteMute: Boolean(disableRemoteMute),
324
+        _remoteControlState,
325
+        _menuPosition
276
     };
326
     };
277
 }
327
 }
278
 
328
 

+ 0
- 8
service/remotecontrol/RemoteControlEvents.js Näytä tiedosto

1
-/**
2
- * Events fired from the remote control module through the EventEmitter.
3
- */
4
-
5
-/**
6
- * Notifies about remote control active session status changes.
7
- */
8
-export const ACTIVE_CHANGED = 'active-changed';

Loading…
Peruuta
Tallenna