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

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