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