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

This commit is contained in:
Hristo Terezov 2020-11-13 22:09:25 -06:00
parent f88061db06
commit af6c794fda
30 changed files with 1348 additions and 1455 deletions

View File

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

2
app.js
View File

@ -17,7 +17,6 @@ import conference from './conference';
import API from './modules/API';
import UI from './modules/UI/UI';
import keyboardshortcut from './modules/keyboardshortcut/keyboardshortcut';
import remoteControl from './modules/remotecontrol/RemoteControl';
import translation from './modules/translation/translation';
// Initialize Olm as early as possible.
@ -49,7 +48,6 @@ window.APP = {
},
keyboardshortcut,
remoteControl,
translation,
UI
};

View File

@ -3,7 +3,6 @@
import EventEmitter from 'events';
import Logger from 'jitsi-meet-logger';
import * as JitsiMeetConferenceEvents from './ConferenceEvents';
import { openConnection } from './connection';
import { ENDPOINT_TEXT_MESSAGE_NAME } from './modules/API/constants';
import AuthHandler from './modules/UI/authentication/AuthHandler';
@ -86,7 +85,8 @@ import {
participantMutedUs,
participantPresenceChanged,
participantRoleChanged,
participantUpdated
participantUpdated,
updateRemoteParticipantFeatures
} from './react/features/base/participants';
import {
getUserSelectedCameraDeviceId,
@ -122,14 +122,13 @@ import {
isPrejoinPageVisible,
makePrecallTest
} from './react/features/prejoin';
import { disableReceiver, stopReceiver } from './react/features/remote-control';
import { toggleScreenshotCaptureEffect } from './react/features/screenshot-capture';
import { setSharedVideoStatus } from './react/features/shared-video';
import { AudioMixerEffect } from './react/features/stream-effects/audio-mixer/AudioMixerEffect';
import { createPresenterEffect } from './react/features/stream-effects/presenter';
import { endpointMessageReceived } from './react/features/subtitles';
import UIEvents from './service/UI/UIEvents';
import * as RemoteControlEvents
from './service/remotecontrol/RemoteControlEvents';
const logger = Logger.getLogger(__filename);
@ -680,7 +679,6 @@ export default {
APP.connection = connection = con;
this._createRoom(tracks);
APP.remoteControl.init();
// if user didn't give access to mic or camera or doesn't have
// them at all, we mark corresponding toolbar buttons as muted,
@ -1445,11 +1443,8 @@ export default {
async _turnScreenSharingOff(didHaveVideo) {
this._untoggleScreenSharing = null;
this.videoSwitchInProgress = true;
const { receiver } = APP.remoteControl;
if (receiver) {
receiver.stop();
}
APP.store.dispatch(stopReceiver());
this._stopProxyConnection();
if (config.enableScreenshotCapture) {
@ -1872,8 +1867,9 @@ export default {
(authEnabled, authLogin) =>
APP.store.dispatch(authStatusChanged(authEnabled, authLogin)));
room.on(JitsiConferenceEvents.PARTCIPANT_FEATURES_CHANGED,
user => APP.UI.onUserFeaturesChanged(user));
room.on(JitsiConferenceEvents.PARTCIPANT_FEATURES_CHANGED, user => {
APP.store.dispatch(updateRemoteParticipantFeatures(user));
});
room.on(JitsiConferenceEvents.USER_JOINED, (id, user) => {
// The logic shared between RN and web.
commonUserJoinedHandling(APP.store, room, user);
@ -1882,6 +1878,7 @@ export default {
return;
}
APP.store.dispatch(updateRemoteParticipantFeatures(user));
logger.log(`USER ${id} connnected:`, user);
APP.UI.addUser(user);
});
@ -2052,30 +2049,6 @@ export default {
JitsiConferenceEvents.LOCK_STATE_CHANGED,
(...args) => APP.store.dispatch(lockStateChanged(room, ...args)));
APP.remoteControl.on(RemoteControlEvents.ACTIVE_CHANGED, isActive => {
room.setLocalParticipantProperty(
'remoteControlSessionStatus',
isActive
);
APP.UI.setLocalRemoteControlActiveChanged();
});
/* eslint-disable max-params */
room.on(
JitsiConferenceEvents.PARTICIPANT_PROPERTY_CHANGED,
(participant, name, oldValue, newValue) => {
switch (name) {
case 'remoteControlSessionStatus':
APP.UI.setRemoteControlActiveStatus(
participant.getId(),
newValue);
break;
default:
// ignore
}
});
room.on(JitsiConferenceEvents.KICKED, participant => {
APP.UI.hideStats();
APP.store.dispatch(kickedOut(room, participant));
@ -2420,25 +2393,6 @@ export default {
APP.UI.changeDisplayName('localVideoContainer', displayName);
},
/**
* Adds any room listener.
* @param {string} eventName one of the JitsiConferenceEvents
* @param {Function} listener the function to be called when the event
* occurs
*/
addConferenceListener(eventName, listener) {
room.on(eventName, listener);
},
/**
* Removes any room listener.
* @param {string} eventName one of the JitsiConferenceEvents
* @param {Function} listener the listener to be removed.
*/
removeConferenceListener(eventName, listener) {
room.off(eventName, listener);
},
/**
* Updates the list of current devices.
* @param {boolean} setDeviceListChangeHandler - Whether to add the deviceList change handlers.
@ -2721,7 +2675,7 @@ export default {
* requested
*/
hangup(requestFeedback = false) {
eventEmitter.emit(JitsiMeetConferenceEvents.BEFORE_HANGUP);
APP.store.dispatch(disableReceiver());
this._stopProxyConnection();
@ -2738,7 +2692,6 @@ export default {
}
APP.UI.removeAllListeners();
APP.remoteControl.removeAllListeners();
let requestFeedbackPromise;
@ -2921,29 +2874,6 @@ export default {
}
},
/**
* Returns the desktop sharing source id or undefined if the desktop sharing
* is not active at the moment.
*
* @returns {string|undefined} - The source id. If the track is not desktop
* track or the source id is not available, undefined will be returned.
*/
getDesktopSharingSourceId() {
return this.localVideo.sourceId;
},
/**
* Returns the desktop sharing source type or undefined if the desktop
* sharing is not active at the moment.
*
* @returns {'screen'|'window'|undefined} - The source type. If the track is
* not desktop track or the source type is not available, undefined will be
* returned.
*/
getDesktopSharingSourceType() {
return this.localVideo.sourceType;
},
/**
* Callback invoked by the external api create or update a direct connection
* from the local client to an external client.

View File

@ -59,14 +59,6 @@ UI.isFullScreen = function() {
return UIUtil.isFullScreen();
};
/**
* Returns true if the etherpad window is currently visible.
* @returns {Boolean} - true if the etherpad window is currently visible.
*/
UI.isEtherpadVisible = function() {
return Boolean(etherpadManager && etherpadManager.isVisible());
};
/**
* Returns true if there is a shared video which is being shown (?).
* @returns {boolean} - true if there is a shared video which is being shown.
@ -305,45 +297,6 @@ UI.toggleFilmstrip = function() {
*/
UI.toggleChat = () => APP.store.dispatch(toggleChat());
/**
* Handle new user display name.
*/
UI.inputDisplayNameHandler = function(newDisplayName) {
eventEmitter.emit(UIEvents.NICKNAME_CHANGED, newDisplayName);
};
// FIXME check if someone user this
UI.showLoginPopup = function(callback) {
logger.log('password is required');
const message
= `<input name="username" type="text"
placeholder="user@domain.net"
data-i18n="[placeholder]dialog.user"
class="input-control" autofocus>
<input name="password" type="password"
data-i18n="[placeholder]dialog.userPassword"
class="input-control"
placeholder="user password">`
;
// eslint-disable-next-line max-params
const submitFunction = (e, v, m, f) => {
if (v && f.username && f.password) {
callback(f.username, f.password);
}
};
messageHandler.openTwoButtonDialog({
titleKey: 'dialog.passwordRequired',
msgString: message,
leftButtonKey: 'dialog.Ok',
submitFunction,
focus: ':input:first'
});
};
/**
* Sets muted audio state for participant
*/
@ -500,14 +453,6 @@ UI.notifyTokenAuthFailed = function() {
});
};
UI.notifyInternalError = function(error) {
messageHandler.showError({
descriptionArguments: { error },
descriptionKey: 'dialog.internalError',
titleKey: 'dialog.internalErrorTitle'
});
};
UI.notifyFocusDisconnected = function(focus, retrySec) {
messageHandler.participantNotification(
null, 'notify.focus',
@ -517,16 +462,6 @@ UI.notifyFocusDisconnected = function(focus, retrySec) {
);
};
/**
* Notifies interested listeners that the raise hand property has changed.
*
* @param {boolean} isRaisedHand indicates the current state of the
* "raised hand"
*/
UI.onLocalRaiseHandChanged = function(isRaisedHand) {
eventEmitter.emit(UIEvents.LOCAL_RAISE_HAND_CHANGED, isRaisedHand);
};
/**
* Update list of available physical devices.
*/
@ -586,38 +521,6 @@ UI.onSharedVideoStop = function(id, attributes) {
}
};
/**
* Handles user's features changes.
*/
UI.onUserFeaturesChanged = user => VideoLayout.onUserFeaturesChanged(user);
/**
* Returns the number of known remote videos.
*
* @returns {number} The number of remote videos.
*/
UI.getRemoteVideosCount = () => VideoLayout.getRemoteVideosCount();
/**
* Sets the remote control active status for a remote participant.
*
* @param {string} participantID - The id of the remote participant.
* @param {boolean} isActive - The new remote control active status.
* @returns {void}
*/
UI.setRemoteControlActiveStatus = function(participantID, isActive) {
VideoLayout.setRemoteControlActiveStatus(participantID, isActive);
};
/**
* Sets the remote control active status for the local participant.
*
* @returns {void}
*/
UI.setLocalRemoteControlActiveChanged = function() {
VideoLayout.setLocalRemoteControlActiveChanged();
};
// TODO: Export every function separately. For now there is no point of doing
// this because we are importing everything.
export default UI;

View File

@ -6,48 +6,6 @@ import UIUtil from '../util/UIUtil';
* Responsible for drawing audio levels.
*/
const AudioLevels = {
/**
* Fills the dot(s) with the specified "index", with as much opacity as
* indicated by "opacity".
*
* @param {string} elementID the parent audio indicator span element
* @param {number} index the index of the dots to fill, where 0 indicates
* the middle dot and the following increments point toward the
* corresponding pair of dots.
* @param {number} opacity the opacity to set for the specified dot.
*/
_setDotLevel(elementID, index, opacity) {
let audioSpan
= document.getElementById(elementID)
.getElementsByClassName('audioindicator');
// Make sure the audio span is still around.
if (audioSpan && audioSpan.length > 0) {
audioSpan = audioSpan[0];
} else {
return;
}
const audioTopDots
= audioSpan.getElementsByClassName('audiodot-top');
const audioDotMiddle
= audioSpan.getElementsByClassName('audiodot-middle');
const audioBottomDots
= audioSpan.getElementsByClassName('audiodot-bottom');
// First take care of the middle dot case.
if (index === 0) {
audioDotMiddle[0].style.opacity = opacity;
return;
}
// Index > 0 : we are setting non-middle dots.
index--;// eslint-disable-line no-param-reassign
audioBottomDots[index].style.opacity = opacity;
audioTopDots[this.sideDotsCount - index - 1].style.opacity = opacity;
},
/**
* Updates the audio level of the large video.
*

View File

@ -12,19 +12,12 @@ import { i18next } from '../../../react/features/base/i18n';
import {
JitsiParticipantConnectionStatus
} from '../../../react/features/base/lib-jitsi-meet';
import {
getParticipantById,
getPinnedParticipant,
pinParticipant
} from '../../../react/features/base/participants';
import { getParticipantById } from '../../../react/features/base/participants';
import { isTestModeEnabled } from '../../../react/features/base/testing';
import { updateLastTrackVideoMediaEvent } from '../../../react/features/base/tracks';
import { PresenceLabel } from '../../../react/features/presence-status';
import {
REMOTE_CONTROL_MENU_STATES,
RemoteVideoMenuTriggerButton
} from '../../../react/features/remote-video-menu';
import { LAYOUTS, getCurrentLayout } from '../../../react/features/video-layout';
import { stopController, requestRemoteControl } from '../../../react/features/remote-control';
import { RemoteVideoMenuTriggerButton } from '../../../react/features/remote-video-menu';
/* eslint-enable no-unused-vars */
import UIUtils from '../util/UIUtil';
@ -90,7 +83,6 @@ export default class RemoteVideo extends SmallVideo {
this.videoSpanId = `participant_${this.id}`;
this._audioStreamElement = null;
this._supportsRemoteControl = false;
this.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP ? 'left bottom' : 'top center';
this.addRemoteVideoContainer();
this.updateIndicators();
@ -98,7 +90,6 @@ export default class RemoteVideo extends SmallVideo {
this.bindHoverHandler();
this.flipX = false;
this.isLocal = false;
this._isRemoteControlSessionActive = false;
/**
* The flag is set to <tt>true</tt> after the 'canplay' event has been
@ -112,10 +103,7 @@ export default class RemoteVideo extends SmallVideo {
// Bind event handlers so they are only bound once for every instance.
// TODO The event handlers should be turned into actions so changes can be
// handled through reducers and middleware.
this._requestRemoteControlPermissions
= this._requestRemoteControlPermissions.bind(this);
this._setAudioVolume = this._setAudioVolume.bind(this);
this._stopRemoteControl = this._stopRemoteControl.bind(this);
this.container.onclick = this._onContainerClick;
}
@ -151,40 +139,11 @@ export default class RemoteVideo extends SmallVideo {
return;
}
const { controller } = APP.remoteControl;
let remoteControlState = null;
let onRemoteControlToggle;
if (this._supportsRemoteControl
&& ((!APP.remoteControl.active && !this._isRemoteControlSessionActive)
|| APP.remoteControl.controller.activeParticipant === this.id)) {
if (controller.getRequestedParticipant() === this.id) {
remoteControlState = REMOTE_CONTROL_MENU_STATES.REQUESTING;
} else if (controller.isStarted()) {
onRemoteControlToggle = this._stopRemoteControl;
remoteControlState = REMOTE_CONTROL_MENU_STATES.STARTED;
} else {
onRemoteControlToggle = this._requestRemoteControlPermissions;
remoteControlState = REMOTE_CONTROL_MENU_STATES.NOT_STARTED;
}
}
const initialVolumeValue = this._audioStreamElement && this._audioStreamElement.volume;
// hide volume when in silent mode
const onVolumeChange
= APP.store.getState()['features/base/config'].startSilent ? undefined : this._setAudioVolume;
const participantID = this.id;
const currentLayout = getCurrentLayout(APP.store.getState());
let remoteMenuPosition;
if (currentLayout === LAYOUTS.TILE_VIEW) {
remoteMenuPosition = 'left top';
} else if (currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
remoteMenuPosition = 'left bottom';
} else {
remoteMenuPosition = 'top center';
}
ReactDOM.render(
<Provider store = { APP.store }>
@ -192,13 +151,10 @@ export default class RemoteVideo extends SmallVideo {
<AtlasKitThemeProvider mode = 'dark'>
<RemoteVideoMenuTriggerButton
initialVolumeValue = { initialVolumeValue }
menuPosition = { remoteMenuPosition }
onMenuDisplay
= {this._onRemoteVideoMenuDisplay.bind(this)}
onRemoteControlToggle = { onRemoteControlToggle }
onVolumeChange = { onVolumeChange }
participantID = { participantID }
remoteControlState = { remoteControlState } />
participantID = { this.id } />
</AtlasKitThemeProvider>
</I18nextProvider>
</Provider>,
@ -212,76 +168,6 @@ export default class RemoteVideo extends SmallVideo {
this.updateRemoteVideoMenu();
}
/**
* Sets the remote control active status for the remote video.
*
* @param {boolean} isActive - The new remote control active status.
* @returns {void}
*/
setRemoteControlActiveStatus(isActive) {
this._isRemoteControlSessionActive = isActive;
this.updateRemoteVideoMenu();
}
/**
* Sets the remote control supported value and initializes or updates the menu
* depending on the remote control is supported or not.
* @param {boolean} isSupported
*/
setRemoteControlSupport(isSupported = false) {
if (this._supportsRemoteControl === isSupported) {
return;
}
this._supportsRemoteControl = isSupported;
this.updateRemoteVideoMenu();
}
/**
* Requests permissions for remote control session.
*/
_requestRemoteControlPermissions() {
APP.remoteControl.controller.requestPermissions(this.id, this.VideoLayout.getLargeVideoWrapper())
.then(result => {
if (result === null) {
return;
}
this.updateRemoteVideoMenu();
APP.UI.messageHandler.notify(
'dialog.remoteControlTitle',
result === false ? 'dialog.remoteControlDeniedMessage' : 'dialog.remoteControlAllowedMessage',
{ user: this.user.getDisplayName() || interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME }
);
if (result === true) {
// the remote control permissions has been granted
// pin the controlled participant
const pinnedParticipant = getPinnedParticipant(APP.store.getState()) || {};
const pinnedId = pinnedParticipant.id;
if (pinnedId !== this.id) {
APP.store.dispatch(pinParticipant(this.id));
}
}
}, error => {
logger.error(error);
this.updateRemoteVideoMenu();
APP.UI.messageHandler.notify(
'dialog.remoteControlTitle',
'dialog.remoteControlErrorMessage',
{ user: this.user.getDisplayName() || interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME }
);
});
this.updateRemoteVideoMenu();
}
/**
* Stops remote control session.
*/
_stopRemoteControl() {
// send message about stopping
APP.remoteControl.controller.stop();
this.updateRemoteVideoMenu();
}
/**
* Change the remote participant's volume level.
*

View File

@ -293,7 +293,6 @@ const VideoLayout = {
const jitsiParticipant = APP.conference.getParticipantById(id);
const remoteVideo = new RemoteVideo(jitsiParticipant, VideoLayout);
this._setRemoteControlProperties(jitsiParticipant, remoteVideo);
this.addRemoteVideoContainer(id, remoteVideo);
this.updateMutedForNoTracks(id, 'audio');
@ -645,33 +644,6 @@ const VideoLayout = {
this.localFlipX = val;
},
/**
* Handles user's features changes.
*/
onUserFeaturesChanged(user) {
const video = this.getSmallVideo(user.getId());
if (!video) {
return;
}
this._setRemoteControlProperties(user, video);
},
/**
* Sets the remote control properties (checks whether remote control
* is supported and executes remoteVideo.setRemoteControlSupport).
* @param {JitsiParticipant} user the user that will be checked for remote
* control support.
* @param {RemoteVideo} remoteVideo the remoteVideo on which the properties
* will be set.
*/
_setRemoteControlProperties(user, remoteVideo) {
APP.remoteControl.checkUserRemoteControlSupport(user)
.then(result => remoteVideo.setRemoteControlSupport(result))
.catch(error =>
logger.warn(`could not get remote control properties for: ${user.getJid()}`, error));
},
/**
* Returns the wrapper jquery selector for the largeVideo
* @returns {JQuerySelector} the wrapper jquery selector for the largeVideo
@ -689,28 +661,6 @@ const VideoLayout = {
return Object.keys(remoteVideos).length;
},
/**
* Sets the remote control active status for a remote participant.
*
* @param {string} participantID - The id of the remote participant.
* @param {boolean} isActive - The new remote control active status.
* @returns {void}
*/
setRemoteControlActiveStatus(participantID, isActive) {
remoteVideos[participantID].setRemoteControlActiveStatus(isActive);
},
/**
* Sets the remote control active status for the local participant.
*
* @returns {void}
*/
setLocalRemoteControlActiveChanged() {
Object.values(remoteVideos).forEach(
remoteVideo => remoteVideo.updateRemoteVideoMenu()
);
},
/**
* Helper method to invoke when the video layout has changed and elements
* have to be re-arranged and resized.

View File

@ -1,3 +0,0 @@
module.exports = {
'extends': '../../react/.eslintrc.js'
};

View File

@ -1,474 +0,0 @@
/* @flow */
import { getLogger } from 'jitsi-meet-logger';
import {
JitsiConferenceEvents
} from '../../react/features/base/lib-jitsi-meet';
import UIEvents from '../../service/UI/UIEvents';
import {
EVENTS,
PERMISSIONS_ACTIONS,
REMOTE_CONTROL_MESSAGE_NAME
} from '../../service/remotecontrol/Constants';
import * as RemoteControlEvents
from '../../service/remotecontrol/RemoteControlEvents';
import * as KeyCodes from '../keycode/keycode';
import RemoteControlParticipant from './RemoteControlParticipant';
declare var $: Function;
declare var APP: Object;
const logger = getLogger(__filename);
/**
* Extract the keyboard key from the keyboard event.
*
* @param {KeyboardEvent} event - The event.
* @returns {KEYS} The key that is pressed or undefined.
*/
function getKey(event) {
return KeyCodes.keyboardEventToKey(event);
}
/**
* Extract the modifiers from the keyboard event.
*
* @param {KeyboardEvent} event - The event.
* @returns {Array} With possible values: "shift", "control", "alt", "command".
*/
function getModifiers(event) {
const modifiers = [];
if (event.shiftKey) {
modifiers.push('shift');
}
if (event.ctrlKey) {
modifiers.push('control');
}
if (event.altKey) {
modifiers.push('alt');
}
if (event.metaKey) {
modifiers.push('command');
}
return modifiers;
}
/**
* This class represents the controller party for a remote controller session.
* It listens for mouse and keyboard events and sends them to the receiver
* party of the remote control session.
*/
export default class Controller extends RemoteControlParticipant {
_area: ?Object;
_controlledParticipant: string | null;
_isCollectingEvents: boolean;
_largeVideoChangedListener: Function;
_requestedParticipant: string | null;
_stopListener: Function;
_userLeftListener: Function;
/**
* Creates new instance.
*/
constructor() {
super();
this._isCollectingEvents = false;
this._controlledParticipant = null;
this._requestedParticipant = null;
this._stopListener = this._handleRemoteControlStoppedEvent.bind(this);
this._userLeftListener = this._onUserLeft.bind(this);
this._largeVideoChangedListener
= this._onLargeVideoIdChanged.bind(this);
}
/**
* Returns the current active participant's id.
*
* @returns {string|null} - The id of the current active participant.
*/
get activeParticipant(): string | null {
return this._requestedParticipant || this._controlledParticipant;
}
/**
* Requests permissions from the remote control receiver side.
*
* @param {string} userId - The user id of the participant that will be
* requested.
* @param {JQuerySelector} eventCaptureArea - The area that is going to be
* used mouse and keyboard event capture.
* @returns {Promise<boolean>} Resolve values - true(accept), false(deny),
* null(the participant has left).
*/
requestPermissions(
userId: string,
eventCaptureArea: Object
): Promise<boolean | null> {
if (!this._enabled) {
return Promise.reject(new Error('Remote control is disabled!'));
}
this.emit(RemoteControlEvents.ACTIVE_CHANGED, true);
this._area = eventCaptureArea;// $("#largeVideoWrapper")
logger.log(`Requsting remote control permissions from: ${userId}`);
return new Promise((resolve, reject) => {
// eslint-disable-next-line prefer-const
let onUserLeft, permissionsReplyListener;
const clearRequest = () => {
this._requestedParticipant = null;
APP.conference.removeConferenceListener(
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
permissionsReplyListener);
APP.conference.removeConferenceListener(
JitsiConferenceEvents.USER_LEFT,
onUserLeft);
};
permissionsReplyListener = (participant, event) => {
let result = null;
try {
result = this._handleReply(participant, event);
} catch (e) {
clearRequest();
this.emit(RemoteControlEvents.ACTIVE_CHANGED, false);
reject(e);
}
if (result !== null) {
clearRequest();
if (result === false) {
this.emit(RemoteControlEvents.ACTIVE_CHANGED, false);
}
resolve(result);
}
};
onUserLeft = id => {
if (id === this._requestedParticipant) {
clearRequest();
this.emit(RemoteControlEvents.ACTIVE_CHANGED, false);
resolve(null);
}
};
APP.conference.addConferenceListener(
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
permissionsReplyListener);
APP.conference.addConferenceListener(
JitsiConferenceEvents.USER_LEFT,
onUserLeft);
this._requestedParticipant = userId;
this.sendRemoteControlEndpointMessage(
userId,
{
type: EVENTS.permissions,
action: PERMISSIONS_ACTIONS.request
},
e => {
clearRequest();
reject(e);
});
});
}
/**
* Handles the reply of the permissions request.
*
* @param {JitsiParticipant} participant - The participant that has sent the
* reply.
* @param {RemoteControlEvent} event - The remote control event.
* @returns {boolean|null}
*/
_handleReply(participant: Object, event: Object) {
const userId = participant.getId();
if (this._enabled
&& event.name === REMOTE_CONTROL_MESSAGE_NAME
&& event.type === EVENTS.permissions
&& userId === this._requestedParticipant) {
if (event.action !== PERMISSIONS_ACTIONS.grant) {
this._area = undefined;
}
switch (event.action) {
case PERMISSIONS_ACTIONS.grant: {
this._controlledParticipant = userId;
logger.log('Remote control permissions granted to:', userId);
this._start();
return true;
}
case PERMISSIONS_ACTIONS.deny:
return false;
case PERMISSIONS_ACTIONS.error:
throw new Error('Error occurred on receiver side');
default:
throw new Error('Unknown reply received!');
}
} else {
// different message type or another user -> ignoring the message
return null;
}
}
/**
* Handles remote control stopped.
*
* @param {JitsiParticipant} participant - The participant that has sent the
* event.
* @param {Object} event - EndpointMessage event from the data channels.
* @property {string} type - The function process only events with
* name REMOTE_CONTROL_MESSAGE_NAME.
* @returns {void}
*/
_handleRemoteControlStoppedEvent(participant: Object, event: Object) {
if (this._enabled
&& event.name === REMOTE_CONTROL_MESSAGE_NAME
&& event.type === EVENTS.stop
&& participant.getId() === this._controlledParticipant) {
this._stop();
}
}
/**
* Starts processing the mouse and keyboard events. Sets conference
* listeners. Disables keyboard events.
*
* @returns {void}
*/
_start() {
logger.log('Starting remote control controller.');
APP.UI.addListener(UIEvents.LARGE_VIDEO_ID_CHANGED,
this._largeVideoChangedListener);
APP.conference.addConferenceListener(
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
this._stopListener);
APP.conference.addConferenceListener(JitsiConferenceEvents.USER_LEFT,
this._userLeftListener);
this.resume();
}
/**
* Disables the keyboatd shortcuts. Starts collecting remote control
* events. It can be used to resume an active remote control session wchich
* was paused with this.pause().
*
* @returns {void}
*/
resume() {
let area;
if (!this._enabled
|| this._isCollectingEvents
|| !(area = this._area)) {
return;
}
logger.log('Resuming remote control controller.');
this._isCollectingEvents = true;
APP.keyboardshortcut.enable(false);
area.mousemove(event => {
const area = this._area; // eslint-disable-line no-shadow
if (!area) {
return;
}
const position = area.position();
this.sendRemoteControlEndpointMessage(this._controlledParticipant, {
type: EVENTS.mousemove,
x: (event.pageX - position.left) / area.width(),
y: (event.pageY - position.top) / area.height()
});
});
area.mousedown(this._onMouseClickHandler.bind(this, EVENTS.mousedown));
area.mouseup(this._onMouseClickHandler.bind(this, EVENTS.mouseup));
area.dblclick(
this._onMouseClickHandler.bind(this, EVENTS.mousedblclick));
area.contextmenu(() => false);
area[0].onmousewheel = event => {
event.preventDefault();
event.stopPropagation();
this.sendRemoteControlEndpointMessage(this._controlledParticipant, {
type: EVENTS.mousescroll,
x: event.deltaX,
y: event.deltaY
});
return false;
};
$(window).keydown(this._onKeyPessHandler.bind(this,
EVENTS.keydown));
$(window).keyup(this._onKeyPessHandler.bind(this, EVENTS.keyup));
}
/**
* Stops processing the mouse and keyboard events. Removes added listeners.
* Enables the keyboard shortcuts. Displays dialog to notify the user that
* remote control session has ended.
*
* @returns {void}
*/
_stop() {
if (!this._controlledParticipant) {
return;
}
logger.log('Stopping remote control controller.');
APP.UI.removeListener(UIEvents.LARGE_VIDEO_ID_CHANGED,
this._largeVideoChangedListener);
APP.conference.removeConferenceListener(
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
this._stopListener);
APP.conference.removeConferenceListener(JitsiConferenceEvents.USER_LEFT,
this._userLeftListener);
this.pause();
this._controlledParticipant = null;
this._area = undefined;
this.emit(RemoteControlEvents.ACTIVE_CHANGED, false);
APP.UI.messageHandler.notify(
'dialog.remoteControlTitle',
'dialog.remoteControlStopMessage'
);
}
/**
* Executes this._stop() mehtod which stops processing the mouse and
* keyboard events, removes added listeners, enables the keyboard shortcuts,
* displays dialog to notify the user that remote control session has ended.
* In addition sends stop message to the controlled participant.
*
* @returns {void}
*/
stop() {
if (!this._controlledParticipant) {
return;
}
this.sendRemoteControlEndpointMessage(this._controlledParticipant, {
type: EVENTS.stop
});
this._stop();
}
/**
* Pauses the collecting of events and enables the keyboard shortcus. But
* it doesn't removes any other listeners. Basically the remote control
* session will be still active after this.pause(), but no events from the
* controller side will be captured and sent. You can resume the collecting
* of the events with this.resume().
*
* @returns {void}
*/
pause() {
if (!this._controlledParticipant) {
return;
}
logger.log('Pausing remote control controller.');
this._isCollectingEvents = false;
APP.keyboardshortcut.enable(true);
const area = this._area;
if (area) {
area.off('contextmenu');
area.off('dblclick');
area.off('mousedown');
area.off('mousemove');
area.off('mouseup');
area[0].onmousewheel = undefined;
}
$(window).off('keydown');
$(window).off('keyup');
}
/**
* Handler for mouse click events.
*
* @param {string} type - The type of event ("mousedown"/"mouseup").
* @param {Event} event - The mouse event.
* @returns {void}
*/
_onMouseClickHandler(type: string, event: Object) {
this.sendRemoteControlEndpointMessage(this._controlledParticipant, {
type,
button: event.which
});
}
/**
* Returns true if the remote control session is started.
*
* @returns {boolean}
*/
isStarted() {
return this._controlledParticipant !== null;
}
/**
* Returns the id of the requested participant.
*
* @returns {string} The id of the requested participant.
* NOTE: This id should be the result of JitsiParticipant.getId() call.
*/
getRequestedParticipant() {
return this._requestedParticipant;
}
/**
* Handler for key press events.
*
* @param {string} type - The type of event ("keydown"/"keyup").
* @param {Event} event - The key event.
* @returns {void}
*/
_onKeyPessHandler(type: string, event: Object) {
this.sendRemoteControlEndpointMessage(this._controlledParticipant, {
type,
key: getKey(event),
modifiers: getModifiers(event)
});
}
/**
* Calls the stop method if the other side have left.
*
* @param {string} id - The user id for the participant that have left.
* @returns {void}
*/
_onUserLeft(id: string) {
if (this._controlledParticipant === id) {
this._stop();
}
}
/**
* Handles changes of the participant displayed on the large video.
*
* @param {string} id - The user id for the participant that is displayed.
* @returns {void}
*/
_onLargeVideoIdChanged(id: string) {
if (!this._controlledParticipant) {
return;
}
if (this._controlledParticipant === id) {
this.resume();
} else {
this.pause();
}
}
}

View File

@ -1,331 +0,0 @@
/* @flow */
import { getLogger } from 'jitsi-meet-logger';
import * as JitsiMeetConferenceEvents from '../../ConferenceEvents';
import {
JitsiConferenceEvents
} from '../../react/features/base/lib-jitsi-meet';
import {
openRemoteControlAuthorizationDialog
} from '../../react/features/remote-control';
import {
DISCO_REMOTE_CONTROL_FEATURE,
EVENTS,
PERMISSIONS_ACTIONS,
REMOTE_CONTROL_MESSAGE_NAME,
REQUESTS
} from '../../service/remotecontrol/Constants';
import * as RemoteControlEvents
from '../../service/remotecontrol/RemoteControlEvents';
import { Transport, PostMessageTransportBackend } from '../transport';
import RemoteControlParticipant from './RemoteControlParticipant';
declare var APP: Object;
declare var config: Object;
declare var interfaceConfig: Object;
const logger = getLogger(__filename);
/**
* The transport instance used for communication with external apps.
*
* @type {Transport}
*/
const transport = new Transport({
backend: new PostMessageTransportBackend({
postisOptions: { scope: 'jitsi-remote-control' }
})
});
/**
* This class represents the receiver party for a remote controller session.
* It handles "remote-control-event" events and sends them to the
* API module. From there the events can be received from wrapper application
* and executed.
*/
export default class Receiver extends RemoteControlParticipant {
_controller: ?string;
_enabled: boolean;
_hangupListener: Function;
_remoteControlEventsListener: Function;
_userLeftListener: Function;
/**
* Creates new instance.
*/
constructor() {
super();
this._controller = null;
this._remoteControlEventsListener
= this._onRemoteControlMessage.bind(this);
this._userLeftListener = this._onUserLeft.bind(this);
this._hangupListener = this._onHangup.bind(this);
// We expect here that even if we receive the supported event earlier
// it will be cached and we'll receive it.
transport.on('event', event => {
if (event.name === REMOTE_CONTROL_MESSAGE_NAME) {
this._onRemoteControlAPIEvent(event);
return true;
}
return false;
});
}
/**
* Enables / Disables the remote control.
*
* @param {boolean} enabled - The new state.
* @returns {void}
*/
_enable(enabled: boolean) {
if (this._enabled === enabled) {
return;
}
this._enabled = enabled;
if (enabled === true) {
logger.log('Remote control receiver enabled.');
// Announce remote control support.
APP.connection.addFeature(DISCO_REMOTE_CONTROL_FEATURE, true);
APP.conference.addConferenceListener(
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
this._remoteControlEventsListener);
APP.conference.addListener(JitsiMeetConferenceEvents.BEFORE_HANGUP,
this._hangupListener);
} else {
logger.log('Remote control receiver disabled.');
this._stop(true);
APP.connection.removeFeature(DISCO_REMOTE_CONTROL_FEATURE);
APP.conference.removeConferenceListener(
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
this._remoteControlEventsListener);
APP.conference.removeListener(
JitsiMeetConferenceEvents.BEFORE_HANGUP,
this._hangupListener);
}
}
/**
* Removes the listener for JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED
* events. Sends stop message to the wrapper application. Optionally
* displays dialog for informing the user that remote control session
* ended.
*
* @param {boolean} [dontNotify] - If true - a notification about stopping
* the remote control won't be displayed.
* @returns {void}
*/
_stop(dontNotify: boolean = false) {
if (!this._controller) {
return;
}
logger.log('Remote control receiver stop.');
this._controller = null;
APP.conference.removeConferenceListener(
JitsiConferenceEvents.USER_LEFT,
this._userLeftListener);
transport.sendEvent({
name: REMOTE_CONTROL_MESSAGE_NAME,
type: EVENTS.stop
});
this.emit(RemoteControlEvents.ACTIVE_CHANGED, false);
if (!dontNotify) {
APP.UI.messageHandler.notify(
'dialog.remoteControlTitle',
'dialog.remoteControlStopMessage'
);
}
}
/**
* Calls this._stop() and sends stop message to the controller participant.
*
* @returns {void}
*/
stop() {
if (!this._controller) {
return;
}
this.sendRemoteControlEndpointMessage(this._controller, {
type: EVENTS.stop
});
this._stop();
}
/**
* Listens for data channel EndpointMessage. Handles only remote control
* messages. Sends the remote control messages to the external app that
* will execute them.
*
* @param {JitsiParticipant} participant - The controller participant.
* @param {Object} message - EndpointMessage from the data channels.
* @param {string} message.name - The function processes only messages with
* name REMOTE_CONTROL_MESSAGE_NAME.
* @returns {void}
*/
_onRemoteControlMessage(participant: Object, message: Object) {
if (message.name !== REMOTE_CONTROL_MESSAGE_NAME) {
return;
}
if (this._enabled) {
if (this._controller === null
&& message.type === EVENTS.permissions
&& message.action === PERMISSIONS_ACTIONS.request) {
const userId = participant.getId();
this.emit(RemoteControlEvents.ACTIVE_CHANGED, true);
APP.store.dispatch(
openRemoteControlAuthorizationDialog(userId));
} else if (this._controller === participant.getId()) {
if (message.type === EVENTS.stop) {
this._stop();
} else { // forward the message
transport.sendEvent(message);
}
} // else ignore
} else {
logger.log('Remote control message is ignored because remote '
+ 'control is disabled', message);
}
}
/**
* Denies remote control access for user associated with the passed user id.
*
* @param {string} userId - The id associated with the user who sent the
* request for remote control authorization.
* @returns {void}
*/
deny(userId: string) {
this.emit(RemoteControlEvents.ACTIVE_CHANGED, false);
this.sendRemoteControlEndpointMessage(userId, {
type: EVENTS.permissions,
action: PERMISSIONS_ACTIONS.deny
});
}
/**
* Grants remote control access to user associated with the passed user id.
*
* @param {string} userId - The id associated with the user who sent the
* request for remote control authorization.
* @returns {void}
*/
grant(userId: string) {
APP.conference.addConferenceListener(JitsiConferenceEvents.USER_LEFT,
this._userLeftListener);
this._controller = userId;
logger.log(`Remote control permissions granted to: ${userId}`);
let promise;
if (APP.conference.isSharingScreen
&& APP.conference.getDesktopSharingSourceType() === 'screen') {
promise = this._sendStartRequest();
} else {
promise = APP.conference.toggleScreenSharing(
true,
{
desktopSharingSources: [ 'screen' ]
})
.then(() => this._sendStartRequest());
}
promise
.then(() =>
this.sendRemoteControlEndpointMessage(userId, {
type: EVENTS.permissions,
action: PERMISSIONS_ACTIONS.grant
})
)
.catch(error => {
logger.error(error);
this.sendRemoteControlEndpointMessage(userId, {
type: EVENTS.permissions,
action: PERMISSIONS_ACTIONS.error
});
APP.UI.messageHandler.notify(
'dialog.remoteControlTitle',
'dialog.startRemoteControlErrorMessage'
);
this._stop(true);
});
}
/**
* Sends remote control start request.
*
* @returns {Promise}
*/
_sendStartRequest() {
return transport.sendRequest({
name: REMOTE_CONTROL_MESSAGE_NAME,
type: REQUESTS.start,
sourceId: APP.conference.getDesktopSharingSourceId()
});
}
/**
* Handles remote control events from the external app. Currently only
* events with type EVENTS.supported and EVENTS.stop are
* supported.
*
* @param {RemoteControlEvent} event - The remote control event.
* @returns {void}
*/
_onRemoteControlAPIEvent(event: Object) {
switch (event.type) {
case EVENTS.supported:
this._onRemoteControlSupported();
break;
case EVENTS.stop:
this.stop();
break;
}
}
/**
* Handles events for support for executing remote control events into
* the wrapper application.
*
* @returns {void}
*/
_onRemoteControlSupported() {
logger.log('Remote Control supported.');
if (config.disableRemoteControl) {
logger.log('Remote Control disabled.');
} else {
this._enable(true);
}
}
/**
* Calls the stop method if the other side have left.
*
* @param {string} id - The user id for the participant that have left.
* @returns {void}
*/
_onUserLeft(id: string) {
if (this._controller === id) {
this._stop();
}
}
/**
* Handles hangup events. Disables the receiver.
*
* @returns {void}
*/
_onHangup() {
this._enable(false);
}
}

View File

@ -1,98 +0,0 @@
/* @flow */
import EventEmitter from 'events';
import { getLogger } from 'jitsi-meet-logger';
import JitsiMeetJS from '../../react/features/base/lib-jitsi-meet';
import { DISCO_REMOTE_CONTROL_FEATURE }
from '../../service/remotecontrol/Constants';
import * as RemoteControlEvents
from '../../service/remotecontrol/RemoteControlEvents';
import Controller from './Controller';
import Receiver from './Receiver';
const logger = getLogger(__filename);
declare var APP: Object;
declare var config: Object;
/**
* Implements the remote control functionality.
*/
class RemoteControl extends EventEmitter {
_active: boolean;
_initialized: boolean;
controller: Controller;
receiver: Receiver;
/**
* Constructs new instance. Creates controller and receiver properties.
*/
constructor() {
super();
this.controller = new Controller();
this._active = false;
this._initialized = false;
this.controller.on(RemoteControlEvents.ACTIVE_CHANGED, active => {
this.active = active;
});
}
/**
* Sets the remote control session active status.
*
* @param {boolean} isActive - True - if the controller or the receiver is
* currently in remote control session and false otherwise.
* @returns {void}
*/
set active(isActive: boolean) {
this._active = isActive;
this.emit(RemoteControlEvents.ACTIVE_CHANGED, isActive);
}
/**
* Returns the remote control session active status.
*
* @returns {boolean} - True - if the controller or the receiver is
* currently in remote control session and false otherwise.
*/
get active(): boolean {
return this._active;
}
/**
* Initializes the remote control - checks if the remote control should be
* enabled or not.
*
* @returns {void}
*/
init() {
if (config.disableRemoteControl || this._initialized || !JitsiMeetJS.isDesktopSharingEnabled()) {
return;
}
logger.log('Initializing remote control.');
this._initialized = true;
this.controller.enable(true);
this.receiver = new Receiver();
this.receiver.on(RemoteControlEvents.ACTIVE_CHANGED, active => {
this.active = active;
});
}
/**
* Checks whether the passed user supports remote control or not.
*
* @param {JitsiParticipant} user - The user to be tested.
* @returns {Promise<boolean>} The promise will be resolved with true if
* the user supports remote control and with false if not.
*/
checkUserRemoteControlSupport(user: Object) {
return user.getFeatures()
.then(features => features.has(DISCO_REMOTE_CONTROL_FEATURE));
}
}
export default new RemoteControl();

View File

@ -1,72 +0,0 @@
/* @flow */
import EventEmitter from 'events';
import { getLogger } from 'jitsi-meet-logger';
import {
REMOTE_CONTROL_MESSAGE_NAME
} from '../../service/remotecontrol/Constants';
const logger = getLogger(__filename);
declare var APP: Object;
/**
* Implements common logic for Receiver class and Controller class.
*/
export default class RemoteControlParticipant extends EventEmitter {
_enabled: boolean;
/**
* Creates new instance.
*/
constructor() {
super();
this._enabled = false;
}
/**
* Enables / Disables the remote control.
*
* @param {boolean} enabled - The new state.
* @returns {void}
*/
enable(enabled: boolean) {
this._enabled = enabled;
}
/**
* Sends remote control message to other participant trough data channel.
*
* @param {string} to - The participant who will receive the event.
* @param {RemoteControlEvent} event - The remote control event.
* @param {Function} onDataChannelFail - Handler for data channel failure.
* @returns {void}
*/
sendRemoteControlEndpointMessage(
to: ?string,
event: Object,
onDataChannelFail: ?Function) {
if (!this._enabled || !to) {
logger.warn(
'Remote control: Skip sending remote control event. Params:',
this.enable,
to);
return;
}
try {
APP.conference.sendEndpointMessage(to, {
name: REMOTE_CONTROL_MESSAGE_NAME,
...event
});
} catch (e) {
logger.error(
'Failed to send EndpointMessage via the datachannels',
e);
if (typeof onDataChannelFail === 'function') {
onDataChannelFail(e);
}
}
}
}

View File

@ -37,6 +37,7 @@ import '../overlay/middleware';
import '../recent-list/middleware';
import '../recording/middleware';
import '../rejoin/middleware';
import '../remote-control/middleware';
import '../room-lock/middleware';
import '../rtcstats/middleware';
import '../subtitles/middleware';

View File

@ -43,6 +43,7 @@ import '../notifications/reducer';
import '../overlay/reducer';
import '../recent-list/reducer';
import '../recording/reducer';
import '../remote-control/reducer';
import '../settings/reducer';
import '../subtitles/reducer';
import '../toolbox/reducer';

View File

@ -16,11 +16,14 @@ import {
PIN_PARTICIPANT,
SET_LOADABLE_AVATAR_URL
} from './actionTypes';
import { DISCO_REMOTE_CONTROL_FEATURE } from './constants';
import {
getLocalParticipant,
getNormalizedDisplayName,
getParticipantDisplayName
getParticipantDisplayName,
getParticipantById
} from './functions';
import logger from './logger';
/**
* Create an action for when dominant speaker changes.
@ -272,6 +275,48 @@ export function participantJoined(participant) {
};
}
/**
* Updates the features of a remote participant.
*
* @param {JitsiParticipant} jitsiParticipant - The ID of the participant.
* @returns {{
* type: PARTICIPANT_UPDATED,
* participant: Participant
* }}
*/
export function updateRemoteParticipantFeatures(jitsiParticipant) {
return (dispatch, getState) => {
if (!jitsiParticipant) {
return;
}
const id = jitsiParticipant.getId();
jitsiParticipant.getFeatures()
.then(features => {
const supportsRemoteControl = features.has(DISCO_REMOTE_CONTROL_FEATURE);
const participant = getParticipantById(getState(), id);
if (!participant || participant.local) {
return;
}
if (participant?.supportsRemoteControl !== supportsRemoteControl) {
return dispatch({
type: PARTICIPANT_UPDATED,
participant: {
id,
supportsRemoteControl
}
});
}
})
.catch(error => {
logger.error(`Failed to get participant features for ${id}!`, error);
});
};
}
/**
* Action to signal that a hidden participant has joined the conference.
*
@ -495,3 +540,4 @@ export function setLoadableAvatarUrl(participantId, url) {
}
};
}

View File

@ -17,6 +17,11 @@ import { IconPhone } from '../icons';
*/
export const DEFAULT_AVATAR_RELATIVE_PATH = 'images/avatar.png';
/**
* The value for the "var" attribute of feature tag in disco-info packets.
*/
export const DISCO_REMOTE_CONTROL_FEATURE = 'http://jitsi.org/meet/remotecontrol';
/**
* Icon URL for jigasi participants.
*

View File

@ -0,0 +1,5 @@
// @flow
import { getLogger } from '../logging/functions';
export default getLogger('features/base/participants');

View File

@ -239,6 +239,13 @@ StateListenerRegistry.register(
_raiseHandUpdated(store, conference, participant.getId(), newValue);
break;
}
case 'remoteControlSessionStatus':
store.dispatch(participantUpdated({
conference,
id: participant.getId(),
remoteControlSessionStatus: newValue
}));
break;
default:
// Ignore for now.

View File

@ -0,0 +1,70 @@
// @flow
/**
* The type of (redux) action which signals that the controller is capturing mouse and keyboard events.
*
* {
* type: CAPTURE_EVENTS,
* isCapturingEvents: boolean
* }
*/
export const CAPTURE_EVENTS = 'CAPTURE_EVENTS';
/**
* The type of (redux) action which signals that a remote control active state has changed.
*
* {
* type: REMOTE_CONTROL_ACTIVE,
* active: boolean
* }
*/
export const REMOTE_CONTROL_ACTIVE = 'REMOTE_CONTROL_ACTIVE';
/**
* The type of (redux) action which sets the receiver transport object.
*
* {
* type: SET_RECEIVER_TRANSPORT,
* transport: Transport
* }
*/
export const SET_RECEIVER_TRANSPORT = 'SET_RECEIVER_TRANSPORT';
/**
* The type of (redux) action which enables the receiver.
*
* {
* type: SET_RECEIVER_ENABLED,
* enabled: boolean
* }
*/
export const SET_RECEIVER_ENABLED = 'SET_RECEIVER_ENABLED';
/**
* The type of (redux) action which sets the controller participant on the receiver side.
* {
* type: SET_CONTROLLER,
* controller: string
* }
*/
export const SET_CONTROLLER = 'SET_CONTROLLER';
/**
* The type of (redux) action which sets the controlled participant on the controller side.
* {
* type: SET_CONTROLLED_PARTICIPANT,
* controlled: string
* }
*/
export const SET_CONTROLLED_PARTICIPANT = 'SET_CONTROLLED_PARTICIPANT';
/**
* The type of (redux) action which sets the requested participant on the controller side.
* {
* type: SET_REQUESTED_PARTICIPANT,
* requestedParticipant: string
* }
*/
export const SET_REQUESTED_PARTICIPANT = 'SET_REQUESTED_PARTICIPANT';

View File

@ -1,6 +1,44 @@
import { openDialog } from '../base/dialog';
// @flow
import { openDialog } from '../base/dialog';
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
import { getParticipantDisplayName, getPinnedParticipant, pinParticipant } from '../base/participants';
import { getLocalVideoTrack } from '../base/tracks';
import { showNotification } from '../notifications';
import {
CAPTURE_EVENTS,
REMOTE_CONTROL_ACTIVE,
SET_REQUESTED_PARTICIPANT,
SET_CONTROLLER,
SET_RECEIVER_ENABLED,
SET_RECEIVER_TRANSPORT,
SET_CONTROLLED_PARTICIPANT
} from './actionTypes';
import { RemoteControlAuthorizationDialog } from './components';
import {
DISCO_REMOTE_CONTROL_FEATURE,
EVENTS,
REMOTE_CONTROL_MESSAGE_NAME,
PERMISSIONS_ACTIONS,
REQUESTS
} from './constants';
import {
getKey,
getModifiers,
getRemoteConrolEventCaptureArea,
isRemoteControlEnabled,
sendRemoteControlEndpointMessage
} from './functions';
import logger from './logger';
/**
* Listeners.
*/
let permissionsReplyListener, receiverEndpointMessageListener, stopListener;
declare var APP: Object;
declare var $: Function;
/**
* Signals that the remote control authorization dialog should be displayed.
@ -16,6 +54,700 @@ import { RemoteControlAuthorizationDialog } from './components';
* }}
* @public
*/
export function openRemoteControlAuthorizationDialog(participantId) {
export function openRemoteControlAuthorizationDialog(participantId: string) {
return openDialog(RemoteControlAuthorizationDialog, { participantId });
}
/**
* Sets the remote control active property.
*
* @param {boolean} active - The new value for the active property.
* @returns {Function}
*/
export function setRemoteControlActive(active: boolean) {
return (dispatch: Function, getState: Function) => {
const state = getState();
const { active: oldActive } = state['features/remote-control'];
const { conference } = state['features/base/conference'];
if (active !== oldActive) {
dispatch({
type: REMOTE_CONTROL_ACTIVE,
active
});
conference.setLocalParticipantProperty('remoteControlSessionStatus', active);
}
};
}
/**
* Requests permissions from the remote control receiver side.
*
* @param {string} userId - The user id of the participant that will be
* requested.
* @returns {Function}
*/
export function requestRemoteControl(userId: string) {
return (dispatch: Function, getState: Function) => {
const state = getState();
const enabled = isRemoteControlEnabled(state);
if (!enabled) {
return Promise.reject(new Error('Remote control is disabled!'));
}
dispatch(setRemoteControlActive(true));
logger.log(`Requsting remote control permissions from: ${userId}`);
const { conference } = state['features/base/conference'];
permissionsReplyListener = (participant, event) => {
dispatch(processPermissionRequestReply(participant.getId(), event));
};
conference.on(JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, permissionsReplyListener);
dispatch({
type: SET_REQUESTED_PARTICIPANT,
requestedParticipant: userId
});
if (!sendRemoteControlEndpointMessage(
conference,
userId,
{
type: EVENTS.permissions,
action: PERMISSIONS_ACTIONS.request
})) {
dispatch(clearRequest());
}
};
}
/**
* Handles permission request replies on the controller side.
*
* @param {string} participantId - The participant that sent the request.
* @param {EndpointMessage} event - The permission request event.
* @returns {Function}
*/
export function processPermissionRequestReply(participantId: string, event: Object) {
return (dispatch: Function, getState: Function) => {
const state = getState();
const { action, name, type } = event;
const { requestedParticipant } = state['features/remote-control'].controller;
if (isRemoteControlEnabled(state) && name === REMOTE_CONTROL_MESSAGE_NAME && type === EVENTS.permissions
&& participantId === requestedParticipant) {
let descriptionKey, permissionGranted = false;
switch (action) {
case PERMISSIONS_ACTIONS.grant: {
dispatch({
type: SET_CONTROLLED_PARTICIPANT,
controlled: participantId
});
logger.log('Remote control permissions granted!', participantId);
logger.log('Starting remote control controller.');
const { conference } = state['features/base/conference'];
stopListener = (participant, stopEvent) => {
dispatch(handleRemoteControlStoppedEvent(participant.getId(), stopEvent));
};
conference.on(JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, stopListener);
dispatch(resume());
permissionGranted = true;
descriptionKey = 'dialog.remoteControlAllowedMessage';
break;
}
case PERMISSIONS_ACTIONS.deny:
logger.log('Remote control permissions denied!', participantId);
descriptionKey = 'dialog.remoteControlDeniedMessage';
break;
case PERMISSIONS_ACTIONS.error:
logger.error('Error occurred on receiver side');
descriptionKey = 'dialog.remoteControlErrorMessage';
break;
default:
logger.error('Unknown reply received!');
descriptionKey = 'dialog.remoteControlErrorMessage';
}
dispatch(clearRequest());
if (!permissionGranted) {
dispatch(setRemoteControlActive(false));
}
dispatch(showNotification({
descriptionArguments: { user: getParticipantDisplayName(state, participantId) },
descriptionKey,
titleKey: 'dialog.remoteControlTitle'
}));
if (permissionGranted) {
// the remote control permissions has been granted
// pin the controlled participant
const pinnedParticipant = getPinnedParticipant(state);
const pinnedId = pinnedParticipant?.id;
if (pinnedId !== participantId) {
dispatch(pinParticipant(participantId));
}
}
} else {
// different message type or another user -> ignoring the message
}
};
}
/**
* Handles remote control stopped.
*
* @param {string} participantId - The ID of the participant that has sent the event.
* @param {EndpointMessage} event - EndpointMessage event from the data channels.
* @property {string} type - The function process only events with name REMOTE_CONTROL_MESSAGE_NAME.
* @returns {void}
*/
export function handleRemoteControlStoppedEvent(participantId: Object, event: Object) {
return (dispatch: Function, getState: Function) => {
const state = getState();
const { name, type } = event;
const { controlled } = state['features/remote-control'].controller;
if (isRemoteControlEnabled(state) && name === REMOTE_CONTROL_MESSAGE_NAME && type === EVENTS.stop
&& participantId === controlled) {
dispatch(stopController());
}
};
}
/**
* Stops processing the mouse and keyboard events. Removes added listeners.
* Enables the keyboard shortcuts. Displays dialog to notify the user that remote control session has ended.
*
* @param {boolean} notifyRemoteParty - If true a endpoint message to the controlled participant will be sent.
* @returns {void}
*/
export function stopController(notifyRemoteParty: boolean = false) {
return (dispatch: Function, getState: Function) => {
const state = getState();
const { controlled } = state['features/remote-control'].controller;
if (!controlled) {
return;
}
const { conference } = state['features/base/conference'];
if (notifyRemoteParty) {
sendRemoteControlEndpointMessage(conference, controlled, {
type: EVENTS.stop
});
}
logger.log('Stopping remote control controller.');
conference.off(JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, stopListener);
stopListener = undefined;
dispatch(pause());
dispatch({
type: SET_CONTROLLED_PARTICIPANT,
controlled: undefined
});
dispatch(setRemoteControlActive(false));
dispatch(showNotification({
descriptionKey: 'dialog.remoteControlStopMessage',
titleKey: 'dialog.remoteControlTitle'
}));
};
}
/**
* Clears a pending permission request.
*
* @returns {Function}
*/
export function clearRequest() {
return (dispatch: Function, getState: Function) => {
const { conference } = getState()['features/base/conference'];
dispatch({
type: SET_REQUESTED_PARTICIPANT,
requestedParticipant: undefined
});
conference.off(JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, permissionsReplyListener);
permissionsReplyListener = undefined;
};
}
/**
* Sets that trasnport object that is used by the receiver to communicate with the native part of the remote control
* implementation.
*
* @param {Transport} transport - The transport to be set.
* @returns {{
* type: SET_RECEIVER_TRANSPORT,
* transport: Transport
* }}
*/
export function setReceiverTransport(transport: Object) {
return {
type: SET_RECEIVER_TRANSPORT,
transport
};
}
/**
* Enables the receiver functionality.
*
* @returns {Function}
*/
export function enableReceiver() {
return (dispatch: Function, getState: Function) => {
const state = getState();
const { enabled } = state['features/remote-control'].receiver;
if (enabled) {
return;
}
const { connection } = state['features/base/connection'];
const { conference } = state['features/base/conference'];
if (!connection || !conference) {
logger.error('Couldn\'t enable the remote receiver! The connection or conference instance is undefined!');
return;
}
dispatch({
type: SET_RECEIVER_ENABLED,
enabled: true
});
connection.addFeature(DISCO_REMOTE_CONTROL_FEATURE, true);
receiverEndpointMessageListener = (participant, message) => {
dispatch(endpointMessageReceived(participant.getId(), message));
};
conference.on(JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, receiverEndpointMessageListener);
};
}
/**
* Disables the receiver functionality.
*
* @returns {Function}
*/
export function disableReceiver() {
return (dispatch: Function, getState: Function) => {
const state = getState();
const { enabled } = state['features/remote-control'].receiver;
if (!enabled) {
return;
}
const { connection } = state['features/base/connection'];
const { conference } = state['features/base/conference'];
if (!connection || !conference) {
logger.error('Couldn\'t enable the remote receiver! The connection or conference instance is undefined!');
return;
}
logger.log('Remote control receiver disabled.');
dispatch({
type: SET_RECEIVER_ENABLED,
enabled: false
});
dispatch(stopReceiver(true));
connection.removeFeature(DISCO_REMOTE_CONTROL_FEATURE);
conference.off(JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, receiverEndpointMessageListener);
};
}
/**
* Stops a remote control session on the receiver side.
*
* @param {boolean} [dontNotifyLocalParty] - If true - a notification about stopping
* the remote control won't be displayed.
* @param {boolean} [dontNotifyRemoteParty] - If true a endpoint message to the controller participant will be sent.
* @returns {Function}
*/
export function stopReceiver(dontNotifyLocalParty: boolean = false, dontNotifyRemoteParty: boolean = false) {
return (dispatch: Function, getState: Function) => {
const state = getState();
const { receiver } = state['features/remote-control'];
const { controller, transport } = receiver;
if (!controller) {
return;
}
const { conference } = state['features/base/conference'];
if (!dontNotifyRemoteParty) {
sendRemoteControlEndpointMessage(conference, controller, {
type: EVENTS.stop
});
}
dispatch({
type: SET_CONTROLLER,
controller: undefined
});
transport.sendEvent({
name: REMOTE_CONTROL_MESSAGE_NAME,
type: EVENTS.stop
});
dispatch(setRemoteControlActive(false));
if (!dontNotifyLocalParty) {
dispatch(showNotification({
descriptionKey: 'dialog.remoteControlStopMessage',
titleKey: 'dialog.remoteControlTitle'
}));
}
};
}
/**
* Handles only remote control endpoint messages.
*
* @param {string} participantId - The controller participant ID.
* @param {Object} message - EndpointMessage from the data channels.
* @param {string} message.name - The function processes only messages with
* name REMOTE_CONTROL_MESSAGE_NAME.
* @returns {Function}
*/
export function endpointMessageReceived(participantId: string, message: Object) {
return (dispatch: Function, getState: Function) => {
const { action, name, type } = message;
if (name !== REMOTE_CONTROL_MESSAGE_NAME) {
return;
}
const state = getState();
const { receiver } = state['features/remote-control'];
const { enabled, transport } = receiver;
if (enabled) {
const { controller } = receiver;
if (!controller && type === EVENTS.permissions && action === PERMISSIONS_ACTIONS.request) {
dispatch(setRemoteControlActive(true));
dispatch(openRemoteControlAuthorizationDialog(participantId));
} else if (controller === participantId) {
if (type === EVENTS.stop) {
dispatch(stopReceiver(false, true));
} else { // forward the message
transport.sendEvent(message);
}
} // else ignore
} else {
logger.log('Remote control message is ignored because remote control is disabled', message);
}
};
}
/**
* Denies remote control access for user associated with the passed user id.
*
* @param {string} participantId - The id associated with the user who sent the
* request for remote control authorization.
* @returns {Function}
*/
export function deny(participantId: string) {
return (dispatch: Function, getState: Function) => {
const state = getState();
const { conference } = state['features/base/conference'];
dispatch(setRemoteControlActive(false));
sendRemoteControlEndpointMessage(conference, participantId, {
type: EVENTS.permissions,
action: PERMISSIONS_ACTIONS.deny
});
};
}
/**
* Sends start remote control request to the native implementation.
*
* @returns {Function}
*/
export function sendStartRequest() {
return (dispatch: Function, getState: Function) => {
const state = getState();
const tracks = state['features/base/tracks'];
const track = getLocalVideoTrack(tracks);
const { sourceId } = track?.jitsiTrack || {};
const { transport } = state['features/remote-control'].receiver;
return transport.sendRequest({
name: REMOTE_CONTROL_MESSAGE_NAME,
type: REQUESTS.start,
sourceId
});
};
}
/**
* Grants remote control access to user associated with the passed user id.
*
* @param {string} participantId - The id associated with the user who sent the
* request for remote control authorization.
* @returns {Function}
*/
export function grant(participantId: string) {
return (dispatch: Function, getState: Function) => {
dispatch({
type: SET_CONTROLLER,
controller: participantId
});
logger.log(`Remote control permissions granted to: ${participantId}`);
let promise;
const state = getState();
const tracks = state['features/base/tracks'];
const track = getLocalVideoTrack(tracks);
const isScreenSharing = track?.videoType === 'desktop';
const { sourceType } = track?.jitsiTrack || {};
if (isScreenSharing && sourceType === 'screen') {
promise = dispatch(sendStartRequest());
} else {
// FIXME: Use action here once toggleScreenSharing is moved to redux.
promise = APP.conference.toggleScreenSharing(
true,
{
desktopSharingSources: [ 'screen' ]
})
.then(() => dispatch(sendStartRequest()));
}
const { conference } = state['features/base/conference'];
promise
.then(() => sendRemoteControlEndpointMessage(conference, participantId, {
type: EVENTS.permissions,
action: PERMISSIONS_ACTIONS.grant
}))
.catch(error => {
logger.error(error);
sendRemoteControlEndpointMessage(conference, participantId, {
type: EVENTS.permissions,
action: PERMISSIONS_ACTIONS.error
});
dispatch(showNotification({
descriptionKey: 'dialog.startRemoteControlErrorMessage',
titleKey: 'dialog.remoteControlTitle'
}));
dispatch(stopReceiver(true));
});
};
}
/**
* Handler for mouse click events on the controller side.
*
* @param {string} type - The type of event ("mousedown"/"mouseup").
* @param {Event} event - The mouse event.
* @returns {Function}
*/
export function mouseClicked(type: string, event: Object) {
return (dispatch: Function, getState: Function) => {
const state = getState();
const { conference } = state['features/base/conference'];
const { controller } = state['features/remote-control'];
sendRemoteControlEndpointMessage(conference, controller.controlled, {
type,
button: event.which
});
};
}
/**
* Handles mouse moved events on the controller side.
*
* @param {Event} event - The mouse event.
* @returns {Function}
*/
export function mouseMoved(event: Object) {
return (dispatch: Function, getState: Function) => {
const area = getRemoteConrolEventCaptureArea();
if (!area) {
return;
}
const position = area.position();
const state = getState();
const { conference } = state['features/base/conference'];
const { controller } = state['features/remote-control'];
sendRemoteControlEndpointMessage(conference, controller.controlled, {
type: EVENTS.mousemove,
x: (event.pageX - position.left) / area.width(),
y: (event.pageY - position.top) / area.height()
});
};
}
/**
* Handles mouse scroll events on the controller side.
*
* @param {Event} event - The mouse event.
* @returns {Function}
*/
export function mouseScrolled(event: Object) {
return (dispatch: Function, getState: Function) => {
const state = getState();
const { conference } = state['features/base/conference'];
const { controller } = state['features/remote-control'];
sendRemoteControlEndpointMessage(conference, controller.controlled, {
type: EVENTS.mousescroll,
x: event.deltaX,
y: event.deltaY
});
};
}
/**
* Handles key press events on the controller side..
*
* @param {string} type - The type of event ("keydown"/"keyup").
* @param {Event} event - The key event.
* @returns {Function}
*/
export function keyPressed(type: string, event: Object) {
return (dispatch: Function, getState: Function) => {
const state = getState();
const { conference } = state['features/base/conference'];
const { controller } = state['features/remote-control'];
sendRemoteControlEndpointMessage(conference, controller.controlled, {
type,
key: getKey(event),
modifiers: getModifiers(event)
});
};
}
/**
* Disables the keyboatd shortcuts. Starts collecting remote control
* events. It can be used to resume an active remote control session which
* was paused with the pause action.
*
* @returns {Function}
*/
export function resume() {
return (dispatch: Function, getState: Function) => {
const area = getRemoteConrolEventCaptureArea();
const state = getState();
const { controller } = state['features/remote-control'];
const { controlled, isCapturingEvents } = controller;
if (!isRemoteControlEnabled(state) || !area || !controlled || isCapturingEvents) {
return;
}
logger.log('Resuming remote control controller.');
// FIXME: Once the keyboard shortcuts are using react/redux.
APP.keyboardshortcut.enable(false);
area.mousemove(event => {
dispatch(mouseMoved(event));
});
area.mousedown(event => dispatch(mouseClicked(EVENTS.mousedown, event)));
area.mouseup(event => dispatch(mouseClicked(EVENTS.mouseup, event)));
area.dblclick(event => dispatch(mouseClicked(EVENTS.mousedblclick, event)));
area.contextmenu(() => false);
area[0].onmousewheel = event => {
event.preventDefault();
event.stopPropagation();
dispatch(mouseScrolled(event));
return false;
};
$(window).keydown(event => dispatch(keyPressed(EVENTS.keydown, event)));
$(window).keyup(event => dispatch(keyPressed(EVENTS.keyup, event)));
dispatch({
type: CAPTURE_EVENTS,
isCapturingEvents: true
});
};
}
/**
* Pauses the collecting of events and enables the keyboard shortcus. But
* it doesn't removes any other listeners. Basically the remote control
* session will be still active after the pause action, but no events from the
* controller side will be captured and sent. You can resume the collecting
* of the events with the resume action.
*
* @returns {Function}
*/
export function pause() {
return (dispatch: Function, getState: Function) => {
const state = getState();
const { controller } = state['features/remote-control'];
const { controlled, isCapturingEvents } = controller;
if (!isRemoteControlEnabled(state) || !controlled || !isCapturingEvents) {
return;
}
logger.log('Pausing remote control controller.');
// FIXME: Once the keyboard shortcuts are using react/redux.
APP.keyboardshortcut.enable(true);
const area = getRemoteConrolEventCaptureArea();
if (area) {
area.off('contextmenu');
area.off('dblclick');
area.off('mousedown');
area.off('mousemove');
area.off('mouseup');
area[0].onmousewheel = undefined;
}
$(window).off('keydown');
$(window).off('keyup');
dispatch({
type: CAPTURE_EVENTS,
isCapturingEvents: false
});
};
}

View File

@ -6,6 +6,8 @@ import { Dialog, hideDialog } from '../../base/dialog';
import { translate } from '../../base/i18n';
import { getParticipantById } from '../../base/participants';
import { connect } from '../../base/redux';
import { getLocalVideoTrack } from '../../base/tracks';
import { grant, deny } from '../actions';
declare var APP: Object;
@ -21,6 +23,9 @@ type Props = {
*/
_displayName: string,
_isScreenSharing: boolean,
_sourceType: string,
/**
* Used to show/hide the dialog on cancel.
*/
@ -87,10 +92,9 @@ class RemoteControlAuthorizationDialog extends Component<Props> {
* @returns {ReactElement}
*/
_getAdditionalMessage() {
// FIXME: Once we have this information in redux we should
// start getting it from there.
if (APP.conference.isSharingScreen
&& APP.conference.getDesktopSharingSourceType() === 'screen') {
const { _isScreenSharing, _sourceType } = this.props;
if (_isScreenSharing && _sourceType === 'screen') {
return null;
}
@ -112,8 +116,9 @@ class RemoteControlAuthorizationDialog extends Component<Props> {
* @returns {boolean} Returns true to close the dialog.
*/
_onCancel() {
// FIXME: This should be action one day.
APP.remoteControl.receiver.deny(this.props.participantId);
const { dispatch, participantId } = this.props;
dispatch(deny(participantId));
return true;
}
@ -131,10 +136,10 @@ class RemoteControlAuthorizationDialog extends Component<Props> {
* picker window, the action will be ignored).
*/
_onSubmit() {
this.props.dispatch(hideDialog());
const { dispatch, participantId } = this.props;
// FIXME: This should be action one day.
APP.remoteControl.receiver.grant(this.props.participantId);
dispatch(hideDialog());
dispatch(grant(participantId));
return false;
}
@ -149,15 +154,24 @@ class RemoteControlAuthorizationDialog extends Component<Props> {
* (instance of) RemoteControlAuthorizationDialog.
* @private
* @returns {{
* _displayName: string
* _displayName: string,
* _isScreenSharing: boolean,
* _sourceId: string,
* _sourceType: string
* }}
*/
function _mapStateToProps(state, ownProps) {
const { _displayName, participantId } = ownProps;
const participant = getParticipantById(state, participantId);
const tracks = state['features/base/tracks'];
const track = getLocalVideoTrack(tracks);
const _isScreenSharing = track?.videoType === 'desktop';
const { sourceType } = track?.jitsiTrack || {};
return {
_displayName: participant ? participant.name : _displayName
_displayName: participant ? participant.name : _displayName,
_isScreenSharing,
_sourceType: sourceType
};
}

View File

@ -1,8 +1,41 @@
/**
* The type of remote control messages.
*/
export const REMOTE_CONTROL_MESSAGE_NAME = 'remote-control';
/**
* The value for the "var" attribute of feature tag in disco-info packets.
*/
export const DISCO_REMOTE_CONTROL_FEATURE
= 'http://jitsi.org/meet/remotecontrol';
export const DISCO_REMOTE_CONTROL_FEATURE = 'http://jitsi.org/meet/remotecontrol';
/**
* The remote control event.
* @typedef {object} RemoteControlEvent
* @property {EVENTS | REQUESTS} type - the type of the message
* @property {number} x - avaibale for type === mousemove only. The new x
* coordinate of the mouse
* @property {number} y - For mousemove type - the new y
* coordinate of the mouse and for mousescroll - represents the vertical
* scrolling diff value
* @property {number} button - 1(left), 2(middle) or 3 (right). Supported by
* mousedown, mouseup and mousedblclick types.
* @property {KEYS} key - Represents the key related to the event. Supported by
* keydown and keyup types.
* @property {KEYS[]} modifiers - Represents the modifier related to the event.
* Supported by keydown and keyup types.
* @property {PERMISSIONS_ACTIONS} action - Supported by type === permissions.
* Represents the action related to the permissions event.
*
* Optional properties. Supported for permissions event for action === request:
* @property {string} userId - The user id of the participant that has sent the
* request.
* @property {string} userJID - The full JID in the MUC of the user that has
* sent the request.
* @property {string} displayName - the displayName of the participant that has
* sent the request.
* @property {boolean} screenSharing - true if the SS is started for the local
* participant and false if not.
*/
/**
* Types of remote-control events.
@ -44,36 +77,3 @@ export const PERMISSIONS_ACTIONS = {
error: 'error'
};
/**
* The type of remote control messages.
*/
export const REMOTE_CONTROL_MESSAGE_NAME = 'remote-control';
/**
* The remote control event.
* @typedef {object} RemoteControlEvent
* @property {EVENTS | REQUESTS} type - the type of the message
* @property {number} x - avaibale for type === mousemove only. The new x
* coordinate of the mouse
* @property {number} y - For mousemove type - the new y
* coordinate of the mouse and for mousescroll - represents the vertical
* scrolling diff value
* @property {number} button - 1(left), 2(middle) or 3 (right). Supported by
* mousedown, mouseup and mousedblclick types.
* @property {KEYS} key - Represents the key related to the event. Supported by
* keydown and keyup types.
* @property {KEYS[]} modifiers - Represents the modifier related to the event.
* Supported by keydown and keyup types.
* @property {PERMISSIONS_ACTIONS} action - Supported by type === permissions.
* Represents the action related to the permissions event.
*
* Optional properties. Supported for permissions event for action === request:
* @property {string} userId - The user id of the participant that has sent the
* request.
* @property {string} userJID - The full JID in the MUC of the user that has
* sent the request.
* @property {string} displayName - the displayName of the participant that has
* sent the request.
* @property {boolean} screenSharing - true if the SS is started for the local
* participant and false if not.
*/

View File

@ -0,0 +1,128 @@
// @flow
import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
import JitsiMeetJS from '../base/lib-jitsi-meet';
import { enableReceiver, stopReceiver } from './actions';
import { REMOTE_CONTROL_MESSAGE_NAME, EVENTS } from './constants';
import { keyboardEventToKey } from './keycodes';
import logger from './logger';
/**
* Checks if the remote contrrol is enabled.
*
* @param {*} state - The redux state.
* @returns {boolean} - True if the remote control is enabled and false otherwise.
*/
export function isRemoteControlEnabled(state: Object) {
return !state['features/base/config'].disableRemoteControl && JitsiMeetJS.isDesktopSharingEnabled();
}
/**
* Sends remote control message to other participant trough data channel.
*
* @param {JitsiConference} conference - The JitsiConference object.
* @param {string} to - The participant who will receive the event.
* @param {RemoteControlEvent} event - The remote control event.
* @returns {boolean} - True if the message was sent successfully and false otherwise.
*/
export function sendRemoteControlEndpointMessage(
conference: Object,
to: ?string,
event: Object) {
if (!to) {
logger.warn('Remote control: Skip sending remote control event. Params:', to);
return false;
}
try {
conference.sendEndpointMessage(to, {
name: REMOTE_CONTROL_MESSAGE_NAME,
...event
});
return true;
} catch (error) {
logger.error('Failed to send EndpointMessage via the datachannels', error);
return false;
}
}
/**
* Handles remote control events from the external app. Currently only
* events with type EVENTS.supported and EVENTS.stop are
* supported.
*
* @param {RemoteControlEvent} event - The remote control event.
* @param {Store} store - The redux store.
* @returns {void}
*/
export function onRemoteControlAPIEvent(event: Object, { getState, dispatch }: Object) {
switch (event.type) {
case EVENTS.supported:
logger.log('Remote Control supported.');
if (isRemoteControlEnabled(getState())) {
dispatch(enableReceiver());
} else {
logger.log('Remote Control disabled.');
}
break;
case EVENTS.stop: {
dispatch(stopReceiver());
break;
}
}
}
/**
* Returns the area used for capturing mouse and key events.
*
* @returns {JQuery} - A JQuery selector.
*/
export function getRemoteConrolEventCaptureArea() {
return VideoLayout.getLargeVideoWrapper();
}
/**
* Extract the keyboard key from the keyboard event.
*
* @param {KeyboardEvent} event - The event.
* @returns {KEYS} The key that is pressed or undefined.
*/
export function getKey(event: Object) {
return keyboardEventToKey(event);
}
/**
* Extract the modifiers from the keyboard event.
*
* @param {KeyboardEvent} event - The event.
* @returns {Array} With possible values: "shift", "control", "alt", "command".
*/
export function getModifiers(event: Object) {
const modifiers = [];
if (event.shiftKey) {
modifiers.push('shift');
}
if (event.ctrlKey) {
modifiers.push('control');
}
if (event.altKey) {
modifiers.push('alt');
}
if (event.metaKey) {
modifiers.push('command');
}
return modifiers;
}

View File

@ -158,8 +158,9 @@ for (let i = 0; i < 26; i++) {
/**
* Returns key associated with the keyCode from the passed event.
* @param {KeyboardEvent} event the event
* @returns {KEYS} the key on the keyboard.
*
* @param {KeyboardEvent} event - The event.
* @returns {KEYS} - The key on the keyboard.
*/
export function keyboardEventToKey(event) {
return keyCodeToKey[event.which];

View File

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

View File

@ -0,0 +1,92 @@
// @flow
import { PostMessageTransportBackend, Transport } from '@jitsi/js-utils/transport';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app';
import { CONFERENCE_JOINED } from '../base/conference';
import { PARTICIPANT_LEFT } from '../base/participants';
import { MiddlewareRegistry } from '../base/redux';
import {
clearRequest, setReceiverTransport, setRemoteControlActive, stopController, stopReceiver
} from './actions';
import { REMOTE_CONTROL_MESSAGE_NAME } from './constants';
import { onRemoteControlAPIEvent } from './functions';
import './subscriber';
/**
* The redux middleware for the remote control feature.
*
* @param {Store} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => async action => {
switch (action.type) {
case APP_WILL_MOUNT: {
const { dispatch } = store;
dispatch(setReceiverTransport(new Transport({
backend: new PostMessageTransportBackend({
postisOptions: { scope: 'jitsi-remote-control' }
})
})));
break;
}
case APP_WILL_UNMOUNT: {
const { getState, dispatch } = store;
const { transport } = getState()['features/remote-control'].receiver;
if (transport) {
transport.dispose();
dispatch(setReceiverTransport());
}
break;
}
case CONFERENCE_JOINED: {
const result = next(action);
const { getState } = store;
const { transport } = getState()['features/remote-control'].receiver;
if (transport) {
// We expect here that even if we receive the supported event earlier
// it will be cached and we'll receive it.
transport.on('event', event => {
if (event.name === REMOTE_CONTROL_MESSAGE_NAME) {
onRemoteControlAPIEvent(event, store);
return true;
}
return false;
});
}
return result;
}
case PARTICIPANT_LEFT: {
const { getState, dispatch } = store;
const state = getState();
const { id } = action.participant;
const { receiver, controller } = state['features/remote-control'];
const { requestedParticipant, controlled } = controller;
if (id === controlled) {
dispatch(stopController());
}
if (id === requestedParticipant) {
dispatch(clearRequest());
dispatch(setRemoteControlActive(false));
}
if (receiver?.controller === id) {
dispatch(stopReceiver(false, true));
}
break;
}
}
return next(action);
});

View File

@ -0,0 +1,68 @@
import { ReducerRegistry, set } from '../base/redux';
import {
CAPTURE_EVENTS,
REMOTE_CONTROL_ACTIVE,
SET_CONTROLLED_PARTICIPANT,
SET_CONTROLLER,
SET_RECEIVER_ENABLED,
SET_RECEIVER_TRANSPORT,
SET_REQUESTED_PARTICIPANT
} from './actionTypes';
/**
* The default state.
*/
const DEFAULT_STATE = {
active: false,
controller: {
isCapturingEvents: false
},
receiver: {
enabled: false
}
};
/**
* Listen for actions that mutate the remote control state.
*/
ReducerRegistry.register(
'features/remote-control', (state = DEFAULT_STATE, action) => {
switch (action.type) {
case CAPTURE_EVENTS:
return {
...state,
controller: set(state.controller, 'isCapturingEvents', action.isCapturingEvents)
};
case REMOTE_CONTROL_ACTIVE:
return set(state, 'active', action.active);
case SET_RECEIVER_TRANSPORT:
return {
...state,
receiver: set(state.receiver, 'transport', action.transport)
};
case SET_RECEIVER_ENABLED:
return {
...state,
receiver: set(state.receiver, 'enabled', action.enabled)
};
case SET_REQUESTED_PARTICIPANT:
return {
...state,
controller: set(state.controller, 'requestedParticipant', action.requestedParticipant)
};
case SET_CONTROLLED_PARTICIPANT:
return {
...state,
controller: set(state.controller, 'controlled', action.controlled)
};
case SET_CONTROLLER:
return {
...state,
receiver: set(state.receiver, 'controller', action.controller)
};
}
return state;
},
);

View File

@ -0,0 +1,33 @@
// @flow
import { StateListenerRegistry } from '../base/redux';
import { resume, pause } from './actions';
/**
* Listens for large video participant ID changes.
*/
StateListenerRegistry.register(
/* selector */ state => {
const { participantId } = state['features/large-video'];
const { controller } = state['features/remote-control'];
const { controlled } = controller;
if (!controlled) {
return undefined;
}
return controlled === participantId;
},
/* listener */ (isControlledParticipantOnStage, { dispatch }) => {
if (isControlledParticipantOnStage === true) {
dispatch(resume());
} else if (isControlledParticipantOnStage === false) {
dispatch(pause());
}
// else {
// isControlledParticipantOnStage === undefined. Ignore!
// }
}
);

View File

@ -4,12 +4,15 @@ import React, { Component } from 'react';
import { Icon, IconMenuThumb } from '../../../base/icons';
import { MEDIA_TYPE } from '../../../base/media';
import { getLocalParticipant, PARTICIPANT_ROLE } from '../../../base/participants';
import { getLocalParticipant, getParticipantById, PARTICIPANT_ROLE } from '../../../base/participants';
import { Popover } from '../../../base/popover';
import { connect } from '../../../base/redux';
import { isRemoteTrackMuted } from '../../../base/tracks';
import { requestRemoteControl, stopController } from '../../../remote-control';
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import MuteEveryoneElseButton from './MuteEveryoneElseButton';
import { REMOTE_CONTROL_MENU_STATES } from './RemoteControlButton';
import {
GrantModeratorButton,
@ -50,6 +53,24 @@ type Props = {
*/
_isModerator: boolean,
/**
* The position relative to the trigger the remote menu should display
* from. Valid values are those supported by AtlasKit
* {@code InlineDialog}.
*/
_menuPosition: string,
/**
* The current state of the participant's remote control session.
*/
_remoteControlState: number,
/**
* The redux dispatch function.
*/
dispatch: Function,
/**
* A value between 0 and 1 indicating the volume of the participant's
* audio element.
@ -61,34 +82,16 @@ type Props = {
*/
onMenuDisplay: Function,
/**
* Callback to invoke choosing to start a remote control session with
* the participant.
*/
onRemoteControlToggle: Function,
/**
* Callback to invoke when changing the level of the participant's
* audio element.
*/
onVolumeChange: Function,
/**
* The position relative to the trigger the remote menu should display
* from. Valid values are those supported by AtlasKit
* {@code InlineDialog}.
*/
menuPosition: string,
/**
* The ID for the participant on which the remote video menu will act.
*/
participantID: string,
/**
* The current state of the participant's remote control session.
*/
remoteControlState: number
};
/**
@ -138,7 +141,7 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
<Popover
content = { content }
onPopoverOpen = { this._onShowRemoteMenu }
position = { this.props.menuPosition }>
position = { this.props._menuPosition }>
<span
className = 'popover-trigger remote-video-menu-trigger'>
<Icon
@ -175,10 +178,10 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
_disableRemoteMute,
_isAudioMuted,
_isModerator,
dispatch,
initialVolumeValue,
onRemoteControlToggle,
onVolumeChange,
remoteControlState,
_remoteControlState,
participantID
} = this.props;
@ -214,13 +217,21 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
}
}
if (remoteControlState) {
if (_remoteControlState) {
let onRemoteControlToggle = null;
if (_remoteControlState === REMOTE_CONTROL_MENU_STATES.STARTED) {
onRemoteControlToggle = () => dispatch(stopController(true));
} else if (_remoteControlState === REMOTE_CONTROL_MENU_STATES.NOT_STARTED) {
onRemoteControlToggle = () => dispatch(requestRemoteControl(participantID));
}
buttons.push(
<RemoteControlButton
key = 'remote-control'
onClick = { onRemoteControlToggle }
participantID = { participantID }
remoteControlState = { remoteControlState } />
remoteControlState = { _remoteControlState } />
);
}
@ -258,7 +269,12 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
* @param {Object} ownProps - The own props of the component.
* @private
* @returns {{
* _isModerator: boolean
* _isAudioMuted: boolean,
* _isModerator: boolean,
* _disableKick: boolean,
* _disableRemoteMute: boolean,
* _menuPosition: string,
* _remoteControlState: number
* }}
*/
function _mapStateToProps(state, ownProps) {
@ -267,12 +283,46 @@ function _mapStateToProps(state, ownProps) {
const localParticipant = getLocalParticipant(state);
const { remoteVideoMenu = {}, disableRemoteMute } = state['features/base/config'];
const { disableKick } = remoteVideoMenu;
let _remoteControlState = null;
const participant = getParticipantById(state, participantID);
const _isRemoteControlSessionActive = participant?.remoteControlSessionStatus ?? false;
const _supportsRemoteControl = participant?.supportsRemoteControl ?? false;
const { active, controller } = state['features/remote-control'];
const { requestedParticipant, controlled } = controller;
const activeParticipant = requestedParticipant || controlled;
if (_supportsRemoteControl
&& ((!active && !_isRemoteControlSessionActive) || activeParticipant === participantID)) {
if (requestedParticipant === participantID) {
_remoteControlState = REMOTE_CONTROL_MENU_STATES.REQUESTING;
} else if (controlled) {
_remoteControlState = REMOTE_CONTROL_MENU_STATES.STARTED;
} else {
_remoteControlState = REMOTE_CONTROL_MENU_STATES.NOT_STARTED;
}
}
const currentLayout = getCurrentLayout(state);
let _menuPosition;
switch (currentLayout) {
case LAYOUTS.TILE_VIEW:
_menuPosition = 'left top';
break;
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
_menuPosition = 'left bottom';
break;
default:
_menuPosition = 'top center';
}
return {
_isAudioMuted: isRemoteTrackMuted(tracks, MEDIA_TYPE.AUDIO, participantID) || false,
_isModerator: Boolean(localParticipant?.role === PARTICIPANT_ROLE.MODERATOR),
_disableKick: Boolean(disableKick),
_disableRemoteMute: Boolean(disableRemoteMute)
_disableRemoteMute: Boolean(disableRemoteMute),
_remoteControlState,
_menuPosition
};
}

View File

@ -1,8 +0,0 @@
/**
* Events fired from the remote control module through the EventEmitter.
*/
/**
* Notifies about remote control active session status changes.
*/
export const ACTIVE_CHANGED = 'active-changed';