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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  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(userId: string, eventCaptureArea: Object) {
  95. if (!this._enabled) {
  96. return Promise.reject(new Error('Remote control is disabled!'));
  97. }
  98. this.emit(RemoteControlEvents.ACTIVE_CHANGED, true);
  99. this._area = eventCaptureArea;// $("#largeVideoWrapper")
  100. logger.log(`Requsting remote control permissions from: ${userId}`);
  101. return new Promise((resolve, reject) => {
  102. // eslint-disable-next-line prefer-const
  103. let onUserLeft, permissionsReplyListener;
  104. const clearRequest = () => {
  105. this._requestedParticipant = null;
  106. APP.conference.removeConferenceListener(
  107. JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
  108. permissionsReplyListener);
  109. APP.conference.removeConferenceListener(
  110. JitsiConferenceEvents.USER_LEFT,
  111. onUserLeft);
  112. };
  113. permissionsReplyListener = (participant, event) => {
  114. let result = null;
  115. try {
  116. result = this._handleReply(participant, event);
  117. } catch (e) {
  118. clearRequest();
  119. this.emit(RemoteControlEvents.ACTIVE_CHANGED, false);
  120. reject(e);
  121. }
  122. if (result !== null) {
  123. clearRequest();
  124. if (result === false) {
  125. this.emit(RemoteControlEvents.ACTIVE_CHANGED, false);
  126. }
  127. resolve(result);
  128. }
  129. };
  130. onUserLeft = id => {
  131. if (id === this._requestedParticipant) {
  132. clearRequest();
  133. this.emit(RemoteControlEvents.ACTIVE_CHANGED, false);
  134. resolve(null);
  135. }
  136. };
  137. APP.conference.addConferenceListener(
  138. JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
  139. permissionsReplyListener);
  140. APP.conference.addConferenceListener(
  141. JitsiConferenceEvents.USER_LEFT,
  142. onUserLeft);
  143. this._requestedParticipant = userId;
  144. this.sendRemoteControlEndpointMessage(userId, {
  145. type: EVENTS.permissions,
  146. action: PERMISSIONS_ACTIONS.request
  147. }, e => {
  148. clearRequest();
  149. reject(e);
  150. });
  151. });
  152. }
  153. /**
  154. * Handles the reply of the permissions request.
  155. *
  156. * @param {JitsiParticipant} participant - The participant that has sent the
  157. * reply.
  158. * @param {RemoteControlEvent} event - The remote control event.
  159. * @returns {void}
  160. */
  161. _handleReply(participant: Object, event: Object) {
  162. const userId = participant.getId();
  163. if (this._enabled
  164. && event.name === REMOTE_CONTROL_MESSAGE_NAME
  165. && event.type === EVENTS.permissions
  166. && userId === this._requestedParticipant) {
  167. if (event.action !== PERMISSIONS_ACTIONS.grant) {
  168. this._area = undefined;
  169. }
  170. switch (event.action) {
  171. case PERMISSIONS_ACTIONS.grant: {
  172. this._controlledParticipant = userId;
  173. logger.log('Remote control permissions granted to:', userId);
  174. this._start();
  175. return true;
  176. }
  177. case PERMISSIONS_ACTIONS.deny:
  178. return false;
  179. case PERMISSIONS_ACTIONS.error:
  180. throw new Error('Error occurred on receiver side');
  181. default:
  182. throw new Error('Unknown reply received!');
  183. }
  184. } else {
  185. // different message type or another user -> ignoring the message
  186. return null;
  187. }
  188. }
  189. /**
  190. * Handles remote control stopped.
  191. *
  192. * @param {JitsiParticipant} participant - The participant that has sent the
  193. * event.
  194. * @param {Object} event - EndpointMessage event from the data channels.
  195. * @property {string} type - The function process only events with
  196. * name REMOTE_CONTROL_MESSAGE_NAME.
  197. * @returns {void}
  198. */
  199. _handleRemoteControlStoppedEvent(participant: Object, event: Object) {
  200. if (this._enabled
  201. && event.name === REMOTE_CONTROL_MESSAGE_NAME
  202. && event.type === EVENTS.stop
  203. && participant.getId() === this._controlledParticipant) {
  204. this._stop();
  205. }
  206. }
  207. /**
  208. * Starts processing the mouse and keyboard events. Sets conference
  209. * listeners. Disables keyboard events.
  210. *
  211. * @returns {void}
  212. */
  213. _start() {
  214. logger.log('Starting remote control controller.');
  215. APP.UI.addListener(UIEvents.LARGE_VIDEO_ID_CHANGED,
  216. this._largeVideoChangedListener);
  217. APP.conference.addConferenceListener(
  218. JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
  219. this._stopListener);
  220. APP.conference.addConferenceListener(JitsiConferenceEvents.USER_LEFT,
  221. this._userLeftListener);
  222. this.resume();
  223. }
  224. /**
  225. * Disables the keyboatd shortcuts. Starts collecting remote control
  226. * events. It can be used to resume an active remote control session wchich
  227. * was paused with this.pause().
  228. *
  229. * @returns {void}
  230. */
  231. resume() {
  232. if (!this._enabled || this._isCollectingEvents || !this._area) {
  233. return;
  234. }
  235. logger.log('Resuming remote control controller.');
  236. this._isCollectingEvents = true;
  237. APP.keyboardshortcut.enable(false);
  238. // $FlowDisableNextLine: we are sure that this._area is not null.
  239. this._area.mousemove(event => {
  240. // $FlowDisableNextLine: we are sure that this._area is not null.
  241. const position = this._area.position();
  242. this.sendRemoteControlEndpointMessage(this._controlledParticipant, {
  243. type: EVENTS.mousemove,
  244. // $FlowDisableNextLine: we are sure that this._area is not null
  245. x: (event.pageX - position.left) / this._area.width(),
  246. // $FlowDisableNextLine: we are sure that this._area is not null
  247. y: (event.pageY - position.top) / this._area.height()
  248. });
  249. });
  250. // $FlowDisableNextLine: we are sure that this._area is not null.
  251. this._area.mousedown(this._onMouseClickHandler.bind(this,
  252. EVENTS.mousedown));
  253. // $FlowDisableNextLine: we are sure that this._area is not null.
  254. this._area.mouseup(this._onMouseClickHandler.bind(this,
  255. EVENTS.mouseup));
  256. // $FlowDisableNextLine: we are sure that this._area is not null.
  257. this._area.dblclick(
  258. this._onMouseClickHandler.bind(this, EVENTS.mousedblclick));
  259. // $FlowDisableNextLine: we are sure that this._area is not null.
  260. this._area.contextmenu(() => false);
  261. // $FlowDisableNextLine: we are sure that this._area is not null.
  262. this._area[0].onmousewheel = event => {
  263. event.preventDefault();
  264. event.stopPropagation();
  265. this.sendRemoteControlEndpointMessage(this._controlledParticipant, {
  266. type: EVENTS.mousescroll,
  267. x: event.deltaX,
  268. y: event.deltaY
  269. });
  270. return false;
  271. };
  272. $(window).keydown(this._onKeyPessHandler.bind(this,
  273. EVENTS.keydown));
  274. $(window).keyup(this._onKeyPessHandler.bind(this, EVENTS.keyup));
  275. }
  276. /**
  277. * Stops processing the mouse and keyboard events. Removes added listeners.
  278. * Enables the keyboard shortcuts. Displays dialog to notify the user that
  279. * remote control session has ended.
  280. *
  281. * @returns {void}
  282. */
  283. _stop() {
  284. if (!this._controlledParticipant) {
  285. return;
  286. }
  287. logger.log('Stopping remote control controller.');
  288. APP.UI.removeListener(UIEvents.LARGE_VIDEO_ID_CHANGED,
  289. this._largeVideoChangedListener);
  290. APP.conference.removeConferenceListener(
  291. JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
  292. this._stopListener);
  293. APP.conference.removeConferenceListener(JitsiConferenceEvents.USER_LEFT,
  294. this._userLeftListener);
  295. this.pause();
  296. this._controlledParticipant = null;
  297. this._area = undefined;
  298. this.emit(RemoteControlEvents.ACTIVE_CHANGED, false);
  299. APP.UI.messageHandler.notify(
  300. 'dialog.remoteControlTitle',
  301. 'dialog.remoteControlStopMessage'
  302. );
  303. }
  304. /**
  305. * Executes this._stop() mehtod which stops processing the mouse and
  306. * keyboard events, removes added listeners, enables the keyboard shortcuts,
  307. * displays dialog to notify the user that remote control session has ended.
  308. * In addition sends stop message to the controlled participant.
  309. *
  310. * @returns {void}
  311. */
  312. stop() {
  313. if (!this._controlledParticipant) {
  314. return;
  315. }
  316. this.sendRemoteControlEndpointMessage(this._controlledParticipant, {
  317. type: EVENTS.stop
  318. });
  319. this._stop();
  320. }
  321. /**
  322. * Pauses the collecting of events and enables the keyboard shortcus. But
  323. * it doesn't removes any other listeners. Basically the remote control
  324. * session will be still active after this.pause(), but no events from the
  325. * controller side will be captured and sent. You can resume the collecting
  326. * of the events with this.resume().
  327. *
  328. * @returns {void}
  329. */
  330. pause() {
  331. if (!this._controlledParticipant) {
  332. return;
  333. }
  334. logger.log('Pausing remote control controller.');
  335. this._isCollectingEvents = false;
  336. APP.keyboardshortcut.enable(true);
  337. // $FlowDisableNextLine: we are sure that this._area is not null.
  338. this._area.off('mousemove');
  339. // $FlowDisableNextLine: we are sure that this._area is not null.
  340. this._area.off('mousedown');
  341. // $FlowDisableNextLine: we are sure that this._area is not null.
  342. this._area.off('mouseup');
  343. // $FlowDisableNextLine: we are sure that this._area is not null.
  344. this._area.off('contextmenu');
  345. // $FlowDisableNextLine: we are sure that this._area is not null.
  346. this._area.off('dblclick');
  347. $(window).off('keydown');
  348. $(window).off('keyup');
  349. // $FlowDisableNextLine: we are sure that this._area is not null.
  350. this._area[0].onmousewheel = undefined;
  351. }
  352. /**
  353. * Handler for mouse click events.
  354. *
  355. * @param {string} type - The type of event ("mousedown"/"mouseup").
  356. * @param {Event} event - The mouse event.
  357. * @returns {void}
  358. */
  359. _onMouseClickHandler(type: string, event: Object) {
  360. this.sendRemoteControlEndpointMessage(this._controlledParticipant, {
  361. type,
  362. button: event.which
  363. });
  364. }
  365. /**
  366. * Returns true if the remote control session is started.
  367. *
  368. * @returns {boolean}
  369. */
  370. isStarted() {
  371. return this._controlledParticipant !== null;
  372. }
  373. /**
  374. * Returns the id of the requested participant.
  375. *
  376. * @returns {string} The id of the requested participant.
  377. * NOTE: This id should be the result of JitsiParticipant.getId() call.
  378. */
  379. getRequestedParticipant() {
  380. return this._requestedParticipant;
  381. }
  382. /**
  383. * Handler for key press events.
  384. *
  385. * @param {string} type - The type of event ("keydown"/"keyup").
  386. * @param {Event} event - The key event.
  387. * @returns {void}
  388. */
  389. _onKeyPessHandler(type: string, event: Object) {
  390. this.sendRemoteControlEndpointMessage(this._controlledParticipant, {
  391. type,
  392. key: getKey(event),
  393. modifiers: getModifiers(event)
  394. });
  395. }
  396. /**
  397. * Calls the stop method if the other side have left.
  398. *
  399. * @param {string} id - The user id for the participant that have left.
  400. * @returns {void}
  401. */
  402. _onUserLeft(id: string) {
  403. if (this._controlledParticipant === id) {
  404. this._stop();
  405. }
  406. }
  407. /**
  408. * Handles changes of the participant displayed on the large video.
  409. *
  410. * @param {string} id - The user id for the participant that is displayed.
  411. * @returns {void}
  412. */
  413. _onLargeVideoIdChanged(id: string) {
  414. if (!this._controlledParticipant) {
  415. return;
  416. }
  417. if (this._controlledParticipant === id) {
  418. this.resume();
  419. } else {
  420. this.pause();
  421. }
  422. }
  423. }