You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Controller.js 15KB

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