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.

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