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

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