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.

API.js 40KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332
  1. // @flow
  2. import Logger from 'jitsi-meet-logger';
  3. import {
  4. createApiEvent,
  5. sendAnalytics
  6. } from '../../react/features/analytics';
  7. import {
  8. getCurrentConference,
  9. sendTones,
  10. setFollowMe,
  11. setPassword,
  12. setSubject
  13. } from '../../react/features/base/conference';
  14. import { overwriteConfig, getWhitelistedJSON } from '../../react/features/base/config';
  15. import { toggleDialog } from '../../react/features/base/dialog/actions';
  16. import { parseJWTFromURLParams } from '../../react/features/base/jwt';
  17. import JitsiMeetJS, { JitsiRecordingConstants } from '../../react/features/base/lib-jitsi-meet';
  18. import { MEDIA_TYPE } from '../../react/features/base/media';
  19. import {
  20. getLocalParticipant,
  21. getParticipantById,
  22. pinParticipant,
  23. kickParticipant,
  24. raiseHand
  25. } from '../../react/features/base/participants';
  26. import { updateSettings } from '../../react/features/base/settings';
  27. import { isToggleCameraEnabled, toggleCamera } from '../../react/features/base/tracks';
  28. import {
  29. sendMessage,
  30. setPrivateMessageRecipient,
  31. toggleChat
  32. } from '../../react/features/chat/actions';
  33. import { openChat } from '../../react/features/chat/actions.web';
  34. import {
  35. processExternalDeviceRequest
  36. } from '../../react/features/device-selection/functions';
  37. import { isEnabled as isDropboxEnabled } from '../../react/features/dropbox';
  38. import { toggleE2EE } from '../../react/features/e2ee/actions';
  39. import { invite } from '../../react/features/invite';
  40. import {
  41. selectParticipantInLargeVideo
  42. } from '../../react/features/large-video/actions.any';
  43. import {
  44. captureLargeVideoScreenshot,
  45. resizeLargeVideo
  46. } from '../../react/features/large-video/actions.web';
  47. import { toggleLobbyMode } from '../../react/features/lobby/actions';
  48. import { RECORDING_TYPES } from '../../react/features/recording/constants';
  49. import { getActiveSession } from '../../react/features/recording/functions';
  50. import { isScreenAudioSupported } from '../../react/features/screen-share';
  51. import { startScreenShareFlow, startAudioScreenShareFlow } from '../../react/features/screen-share/actions';
  52. import { playSharedVideo, stopSharedVideo } from '../../react/features/shared-video/actions.any';
  53. import { toggleTileView, setTileView } from '../../react/features/video-layout';
  54. import { muteAllParticipants } from '../../react/features/video-menu/actions';
  55. import { setVideoQuality } from '../../react/features/video-quality';
  56. import VirtualBackgroundDialog from '../../react/features/virtual-background/components/VirtualBackgroundDialog';
  57. import { getJitsiMeetTransport } from '../transport';
  58. import { API_ID, ENDPOINT_TEXT_MESSAGE_NAME } from './constants';
  59. const logger = Logger.getLogger(__filename);
  60. declare var APP: Object;
  61. /**
  62. * List of the available commands.
  63. */
  64. let commands = {};
  65. /**
  66. * The transport instance used for communication with external apps.
  67. *
  68. * @type {Transport}
  69. */
  70. const transport = getJitsiMeetTransport();
  71. /**
  72. * The current audio availability.
  73. *
  74. * @type {boolean}
  75. */
  76. let audioAvailable = true;
  77. /**
  78. * The current video availability.
  79. *
  80. * @type {boolean}
  81. */
  82. let videoAvailable = true;
  83. /**
  84. * Initializes supported commands.
  85. *
  86. * @returns {void}
  87. */
  88. function initCommands() {
  89. commands = {
  90. 'display-name': displayName => {
  91. sendAnalytics(createApiEvent('display.name.changed'));
  92. APP.conference.changeLocalDisplayName(displayName);
  93. },
  94. 'mute-everyone': mediaType => {
  95. const muteMediaType = mediaType ? mediaType : MEDIA_TYPE.AUDIO;
  96. sendAnalytics(createApiEvent('muted-everyone'));
  97. const participants = APP.store.getState()['features/base/participants'];
  98. const localIds = participants
  99. .filter(participant => participant.local)
  100. .filter(participant => participant.role === 'moderator')
  101. .map(participant => participant.id);
  102. APP.store.dispatch(muteAllParticipants(localIds, muteMediaType));
  103. },
  104. 'toggle-lobby': isLobbyEnabled => {
  105. APP.store.dispatch(toggleLobbyMode(isLobbyEnabled));
  106. },
  107. 'password': password => {
  108. const { conference, passwordRequired }
  109. = APP.store.getState()['features/base/conference'];
  110. if (passwordRequired) {
  111. sendAnalytics(createApiEvent('submit.password'));
  112. APP.store.dispatch(setPassword(
  113. passwordRequired,
  114. passwordRequired.join,
  115. password
  116. ));
  117. } else {
  118. sendAnalytics(createApiEvent('password.changed'));
  119. APP.store.dispatch(setPassword(
  120. conference,
  121. conference.lock,
  122. password
  123. ));
  124. }
  125. },
  126. 'pin-participant': id => {
  127. logger.debug('Pin participant command received');
  128. sendAnalytics(createApiEvent('participant.pinned'));
  129. APP.store.dispatch(pinParticipant(id));
  130. },
  131. 'proxy-connection-event': event => {
  132. APP.conference.onProxyConnectionEvent(event);
  133. },
  134. 'resize-large-video': (width, height) => {
  135. logger.debug('Resize large video command received');
  136. sendAnalytics(createApiEvent('largevideo.resized'));
  137. APP.store.dispatch(resizeLargeVideo(width, height));
  138. },
  139. 'send-tones': (options = {}) => {
  140. const { duration, tones, pause } = options;
  141. APP.store.dispatch(sendTones(tones, duration, pause));
  142. },
  143. 'set-follow-me': value => {
  144. logger.debug('Set follow me command received');
  145. if (value) {
  146. sendAnalytics(createApiEvent('follow.me.set'));
  147. } else {
  148. sendAnalytics(createApiEvent('follow.me.unset'));
  149. }
  150. APP.store.dispatch(setFollowMe(value));
  151. },
  152. 'set-large-video-participant': participantId => {
  153. logger.debug('Set large video participant command received');
  154. sendAnalytics(createApiEvent('largevideo.participant.set'));
  155. APP.store.dispatch(selectParticipantInLargeVideo(participantId));
  156. },
  157. 'subject': subject => {
  158. sendAnalytics(createApiEvent('subject.changed'));
  159. APP.store.dispatch(setSubject(subject));
  160. },
  161. 'submit-feedback': feedback => {
  162. sendAnalytics(createApiEvent('submit.feedback'));
  163. APP.conference.submitFeedback(feedback.score, feedback.message);
  164. },
  165. 'toggle-audio': () => {
  166. sendAnalytics(createApiEvent('toggle-audio'));
  167. logger.log('Audio toggle: API command received');
  168. APP.conference.toggleAudioMuted(false /* no UI */);
  169. },
  170. 'toggle-video': () => {
  171. sendAnalytics(createApiEvent('toggle-video'));
  172. logger.log('Video toggle: API command received');
  173. APP.conference.toggleVideoMuted(false /* no UI */);
  174. },
  175. 'toggle-film-strip': () => {
  176. sendAnalytics(createApiEvent('film.strip.toggled'));
  177. APP.UI.toggleFilmstrip();
  178. },
  179. 'toggle-camera': () => {
  180. if (!isToggleCameraEnabled(APP.store.getState())) {
  181. return;
  182. }
  183. APP.store.dispatch(toggleCamera());
  184. },
  185. 'toggle-camera-mirror': () => {
  186. const state = APP.store.getState();
  187. const { localFlipX: currentFlipX } = state['features/base/settings'];
  188. APP.store.dispatch(updateSettings({ localFlipX: !currentFlipX }));
  189. },
  190. 'toggle-chat': () => {
  191. sendAnalytics(createApiEvent('chat.toggled'));
  192. APP.store.dispatch(toggleChat());
  193. },
  194. 'toggle-raise-hand': () => {
  195. const localParticipant = getLocalParticipant(APP.store.getState());
  196. if (!localParticipant) {
  197. return;
  198. }
  199. const { raisedHand } = localParticipant;
  200. sendAnalytics(createApiEvent('raise-hand.toggled'));
  201. APP.store.dispatch(raiseHand(!raisedHand));
  202. },
  203. 'toggle-share-audio': () => {
  204. sendAnalytics(createApiEvent('audio.screen.sharing.toggled'));
  205. if (isScreenAudioSupported()) {
  206. APP.store.dispatch(startAudioScreenShareFlow());
  207. return;
  208. }
  209. logger.error('Audio screen sharing is not supported by the current platform!');
  210. },
  211. /**
  212. * Callback to invoke when the "toggle-share-screen" command is received.
  213. *
  214. * @param {Object} options - Additional details of how to perform
  215. * the action. Note this parameter is undocumented and experimental.
  216. * @param {boolean} options.enable - Whether trying to enable screen
  217. * sharing or to turn it off.
  218. * @returns {void}
  219. */
  220. 'toggle-share-screen': (options = {}) => {
  221. sendAnalytics(createApiEvent('screen.sharing.toggled'));
  222. toggleScreenSharing(options.enable);
  223. },
  224. 'toggle-tile-view': () => {
  225. sendAnalytics(createApiEvent('tile-view.toggled'));
  226. APP.store.dispatch(toggleTileView());
  227. },
  228. 'set-tile-view': enabled => {
  229. APP.store.dispatch(setTileView(enabled));
  230. },
  231. 'video-hangup': (showFeedbackDialog = true) => {
  232. sendAnalytics(createApiEvent('video.hangup'));
  233. APP.conference.hangup(showFeedbackDialog);
  234. },
  235. 'email': email => {
  236. sendAnalytics(createApiEvent('email.changed'));
  237. APP.conference.changeLocalEmail(email);
  238. },
  239. 'avatar-url': avatarUrl => {
  240. sendAnalytics(createApiEvent('avatar.url.changed'));
  241. APP.conference.changeLocalAvatarUrl(avatarUrl);
  242. },
  243. 'send-chat-message': (message, to, ignorePrivacy = false) => {
  244. logger.debug('Send chat message command received');
  245. if (to) {
  246. const participant = getParticipantById(APP.store.getState(), to);
  247. if (participant) {
  248. APP.store.dispatch(setPrivateMessageRecipient(participant));
  249. } else {
  250. logger.error(`Participant with id ${to} not found!`);
  251. return;
  252. }
  253. } else {
  254. APP.store.dispatch(setPrivateMessageRecipient());
  255. }
  256. APP.store.dispatch(sendMessage(message, ignorePrivacy));
  257. },
  258. 'send-endpoint-text-message': (to, text) => {
  259. logger.debug('Send endpoint message command received');
  260. try {
  261. APP.conference.sendEndpointMessage(to, {
  262. name: ENDPOINT_TEXT_MESSAGE_NAME,
  263. text
  264. });
  265. } catch (err) {
  266. logger.error('Failed sending endpoint text message', err);
  267. }
  268. },
  269. 'toggle-e2ee': enabled => {
  270. logger.debug('Toggle E2EE key command received');
  271. APP.store.dispatch(toggleE2EE(enabled));
  272. },
  273. 'set-video-quality': frameHeight => {
  274. logger.debug('Set video quality command received');
  275. sendAnalytics(createApiEvent('set.video.quality'));
  276. APP.store.dispatch(setVideoQuality(frameHeight));
  277. },
  278. 'start-share-video': url => {
  279. logger.debug('Share video command received');
  280. sendAnalytics(createApiEvent('share.video.start'));
  281. APP.store.dispatch(playSharedVideo(url));
  282. },
  283. 'stop-share-video': () => {
  284. logger.debug('Share video command received');
  285. sendAnalytics(createApiEvent('share.video.stop'));
  286. APP.store.dispatch(stopSharedVideo());
  287. },
  288. /**
  289. * Starts a file recording or streaming session depending on the passed on params.
  290. * For RTMP streams, `rtmpStreamKey` must be passed on. `rtmpBroadcastID` is optional.
  291. * For youtube streams, `youtubeStreamKey` must be passed on. `youtubeBroadcastID` is optional.
  292. * For dropbox recording, recording `mode` should be `file` and a dropbox oauth2 token must be provided.
  293. * For file recording, recording `mode` should be `file` and optionally `shouldShare` could be passed on.
  294. * No other params should be passed.
  295. *
  296. * @param { string } arg.mode - Recording mode, either `file` or `stream`.
  297. * @param { string } arg.dropboxToken - Dropbox oauth2 token.
  298. * @param { string } arg.rtmpStreamKey - The RTMP stream key.
  299. * @param { string } arg.rtmpBroadcastID - The RTMP braodcast ID.
  300. * @param { boolean } arg.shouldShare - Whether the recording should be shared with the participants or not.
  301. * Only applies to certain jitsi meet deploys.
  302. * @param { string } arg.youtubeStreamKey - The youtube stream key.
  303. * @param { string } arg.youtubeBroadcastID - The youtube broacast ID.
  304. * @returns {void}
  305. */
  306. 'start-recording': ({
  307. mode,
  308. dropboxToken,
  309. shouldShare,
  310. rtmpStreamKey,
  311. rtmpBroadcastID,
  312. youtubeStreamKey,
  313. youtubeBroadcastID
  314. }) => {
  315. const state = APP.store.getState();
  316. const conference = getCurrentConference(state);
  317. if (!conference) {
  318. logger.error('Conference is not defined');
  319. return;
  320. }
  321. if (dropboxToken && !isDropboxEnabled(state)) {
  322. logger.error('Failed starting recording: dropbox is not enabled on this deployment');
  323. return;
  324. }
  325. if (mode === JitsiRecordingConstants.mode.STREAM && !(youtubeStreamKey || rtmpStreamKey)) {
  326. logger.error('Failed starting recording: missing youtube or RTMP stream key');
  327. return;
  328. }
  329. let recordingConfig;
  330. if (mode === JitsiRecordingConstants.mode.FILE) {
  331. if (dropboxToken) {
  332. recordingConfig = {
  333. mode: JitsiRecordingConstants.mode.FILE,
  334. appData: JSON.stringify({
  335. 'file_recording_metadata': {
  336. 'upload_credentials': {
  337. 'service_name': RECORDING_TYPES.DROPBOX,
  338. 'token': dropboxToken
  339. }
  340. }
  341. })
  342. };
  343. } else {
  344. recordingConfig = {
  345. mode: JitsiRecordingConstants.mode.FILE,
  346. appData: JSON.stringify({
  347. 'file_recording_metadata': {
  348. 'share': shouldShare
  349. }
  350. })
  351. };
  352. }
  353. } else if (mode === JitsiRecordingConstants.mode.STREAM) {
  354. recordingConfig = {
  355. broadcastId: youtubeBroadcastID || rtmpBroadcastID,
  356. mode: JitsiRecordingConstants.mode.STREAM,
  357. streamId: youtubeStreamKey || rtmpStreamKey
  358. };
  359. } else {
  360. logger.error('Invalid recording mode provided');
  361. return;
  362. }
  363. conference.startRecording(recordingConfig);
  364. },
  365. /**
  366. * Stops a recording or streaming in progress.
  367. *
  368. * @param {string} mode - `file` or `stream`.
  369. * @returns {void}
  370. */
  371. 'stop-recording': mode => {
  372. const state = APP.store.getState();
  373. const conference = getCurrentConference(state);
  374. if (!conference) {
  375. logger.error('Conference is not defined');
  376. return;
  377. }
  378. if (![ JitsiRecordingConstants.mode.FILE, JitsiRecordingConstants.mode.STREAM ].includes(mode)) {
  379. logger.error('Invalid recording mode provided!');
  380. return;
  381. }
  382. const activeSession = getActiveSession(state, mode);
  383. if (activeSession && activeSession.id) {
  384. conference.stopRecording(activeSession.id);
  385. } else {
  386. logger.error('No recording or streaming session found');
  387. }
  388. },
  389. 'initiate-private-chat': participantId => {
  390. const state = APP.store.getState();
  391. const participant = getParticipantById(state, participantId);
  392. if (participant) {
  393. const { isOpen: isChatOpen } = state['features/chat'];
  394. if (!isChatOpen) {
  395. APP.store.dispatch(toggleChat());
  396. }
  397. APP.store.dispatch(openChat(participant));
  398. } else {
  399. logger.error('No participant found for the given participantId');
  400. }
  401. },
  402. 'cancel-private-chat': () => {
  403. APP.store.dispatch(setPrivateMessageRecipient());
  404. },
  405. 'kick-participant': participantId => {
  406. APP.store.dispatch(kickParticipant(participantId));
  407. },
  408. 'overwrite-config': config => {
  409. const whitelistedConfig = getWhitelistedJSON('config', config);
  410. APP.store.dispatch(overwriteConfig(whitelistedConfig));
  411. },
  412. 'toggle-virtual-background': () => {
  413. APP.store.dispatch(toggleDialog(VirtualBackgroundDialog));
  414. }
  415. };
  416. transport.on('event', ({ data, name }) => {
  417. if (name && commands[name]) {
  418. commands[name](...data);
  419. return true;
  420. }
  421. return false;
  422. });
  423. transport.on('request', (request, callback) => {
  424. const { dispatch, getState } = APP.store;
  425. if (processExternalDeviceRequest(dispatch, getState, request, callback)) {
  426. return true;
  427. }
  428. const { name } = request;
  429. switch (name) {
  430. case 'capture-largevideo-screenshot' :
  431. APP.store.dispatch(captureLargeVideoScreenshot())
  432. .then(dataURL => {
  433. let error;
  434. if (!dataURL) {
  435. error = new Error('No large video found!');
  436. }
  437. callback({
  438. error,
  439. dataURL
  440. });
  441. });
  442. break;
  443. case 'invite': {
  444. const { invitees } = request;
  445. if (!Array.isArray(invitees) || invitees.length === 0) {
  446. callback({
  447. error: new Error('Unexpected format of invitees')
  448. });
  449. break;
  450. }
  451. // The store should be already available because API.init is called
  452. // on appWillMount action.
  453. APP.store.dispatch(
  454. invite(invitees, true))
  455. .then(failedInvitees => {
  456. let error;
  457. let result;
  458. if (failedInvitees.length) {
  459. error = new Error('One or more invites failed!');
  460. } else {
  461. result = true;
  462. }
  463. callback({
  464. error,
  465. result
  466. });
  467. });
  468. break;
  469. }
  470. case 'is-audio-muted':
  471. callback(APP.conference.isLocalAudioMuted());
  472. break;
  473. case 'is-video-muted':
  474. callback(APP.conference.isLocalVideoMuted());
  475. break;
  476. case 'is-audio-available':
  477. callback(audioAvailable);
  478. break;
  479. case 'is-video-available':
  480. callback(videoAvailable);
  481. break;
  482. case 'is-sharing-screen':
  483. callback(Boolean(APP.conference.isSharingScreen));
  484. break;
  485. case 'get-content-sharing-participants': {
  486. const tracks = getState()['features/base/tracks'];
  487. const sharingParticipantIds = tracks.filter(tr => tr.videoType === 'desktop').map(t => t.participantId);
  488. callback({
  489. sharingParticipantIds
  490. });
  491. break;
  492. }
  493. case 'get-livestream-url': {
  494. const state = APP.store.getState();
  495. const conference = getCurrentConference(state);
  496. let livestreamUrl;
  497. if (conference) {
  498. const activeSession = getActiveSession(state, JitsiRecordingConstants.mode.STREAM);
  499. livestreamUrl = activeSession?.liveStreamViewURL;
  500. } else {
  501. logger.error('Conference is not defined');
  502. }
  503. callback({
  504. livestreamUrl
  505. });
  506. break;
  507. }
  508. default:
  509. return false;
  510. }
  511. return true;
  512. });
  513. }
  514. /**
  515. * Check whether the API should be enabled or not.
  516. *
  517. * @returns {boolean}
  518. */
  519. function shouldBeEnabled() {
  520. return (
  521. typeof API_ID === 'number'
  522. // XXX Enable the API when a JSON Web Token (JWT) is specified in
  523. // the location/URL because then it is very likely that the Jitsi
  524. // Meet (Web) app is being used by an external/wrapping (Web) app
  525. // and, consequently, the latter will need to communicate with the
  526. // former. (The described logic is merely a heuristic though.)
  527. || parseJWTFromURLParams());
  528. }
  529. /**
  530. * Executes on toggle-share-screen command.
  531. *
  532. * @param {boolean} [enable] - Whether this toggle is to explicitly enable or
  533. * disable screensharing. If not defined, the application will automatically
  534. * attempt to toggle between enabled and disabled. This boolean is useful for
  535. * explicitly setting desired screensharing state.
  536. * @returns {void}
  537. */
  538. function toggleScreenSharing(enable) {
  539. if (JitsiMeetJS.isDesktopSharingEnabled()) {
  540. APP.store.dispatch(startScreenShareFlow(enable));
  541. }
  542. }
  543. /**
  544. * Removes sensitive data from a mouse event.
  545. *
  546. * @param {MouseEvent} event - The mouse event to sanitize.
  547. * @returns {Object}
  548. */
  549. function sanitizeMouseEvent(event: MouseEvent) {
  550. const {
  551. clientX,
  552. clientY,
  553. movementX,
  554. movementY,
  555. offsetX,
  556. offsetY,
  557. pageX,
  558. pageY,
  559. x,
  560. y,
  561. screenX,
  562. screenY
  563. } = event;
  564. return {
  565. clientX,
  566. clientY,
  567. movementX,
  568. movementY,
  569. offsetX,
  570. offsetY,
  571. pageX,
  572. pageY,
  573. x,
  574. y,
  575. screenX,
  576. screenY
  577. };
  578. }
  579. /**
  580. * Implements API class that communicates with external API class and provides
  581. * interface to access Jitsi Meet features by external applications that embed
  582. * Jitsi Meet.
  583. */
  584. class API {
  585. _enabled: boolean;
  586. /**
  587. * Initializes the API. Setups message event listeners that will receive
  588. * information from external applications that embed Jitsi Meet. It also
  589. * sends a message to the external application that API is initialized.
  590. *
  591. * @param {Object} options - Optional parameters.
  592. * @returns {void}
  593. */
  594. init() {
  595. if (!shouldBeEnabled()) {
  596. return;
  597. }
  598. /**
  599. * Current status (enabled/disabled) of API.
  600. *
  601. * @private
  602. * @type {boolean}
  603. */
  604. this._enabled = true;
  605. initCommands();
  606. }
  607. /**
  608. * Notify external application (if API is enabled) that the large video
  609. * visibility changed.
  610. *
  611. * @param {boolean} isHidden - True if the large video is hidden and false
  612. * otherwise.
  613. * @returns {void}
  614. */
  615. notifyLargeVideoVisibilityChanged(isHidden: boolean) {
  616. this._sendEvent({
  617. name: 'large-video-visibility-changed',
  618. isVisible: !isHidden
  619. });
  620. }
  621. /**
  622. * Notifies the external application (spot) that the local jitsi-participant
  623. * has a status update.
  624. *
  625. * @param {Object} event - The message to pass onto spot.
  626. * @returns {void}
  627. */
  628. sendProxyConnectionEvent(event: Object) {
  629. this._sendEvent({
  630. name: 'proxy-connection-event',
  631. ...event
  632. });
  633. }
  634. /**
  635. * Sends event to the external application.
  636. *
  637. * @param {Object} event - The event to be sent.
  638. * @returns {void}
  639. */
  640. _sendEvent(event: Object = {}) {
  641. if (this._enabled) {
  642. transport.sendEvent(event);
  643. }
  644. }
  645. /**
  646. * Notify external application (if API is enabled) that the chat state has been updated.
  647. *
  648. * @param {number} unreadCount - The unread messages counter.
  649. * @param {boolean} isOpen - True if the chat panel is open.
  650. * @returns {void}
  651. */
  652. notifyChatUpdated(unreadCount: number, isOpen: boolean) {
  653. this._sendEvent({
  654. name: 'chat-updated',
  655. unreadCount,
  656. isOpen
  657. });
  658. }
  659. /**
  660. * Notify external application (if API is enabled) that message was sent.
  661. *
  662. * @param {string} message - Message body.
  663. * @param {boolean} privateMessage - True if the message was a private message.
  664. * @returns {void}
  665. */
  666. notifySendingChatMessage(message: string, privateMessage: boolean) {
  667. this._sendEvent({
  668. name: 'outgoing-message',
  669. message,
  670. privateMessage
  671. });
  672. }
  673. /**
  674. * Notify external application (if API is enabled) that the mouse has entered inside the iframe.
  675. *
  676. * @param {MouseEvent} event - The mousemove event.
  677. * @returns {void}
  678. */
  679. notifyMouseEnter(event: MouseEvent) {
  680. this._sendEvent({
  681. name: 'mouse-enter',
  682. event: sanitizeMouseEvent(event)
  683. });
  684. }
  685. /**
  686. * Notify external application (if API is enabled) that the mouse has entered inside the iframe.
  687. *
  688. * @param {MouseEvent} event - The mousemove event.
  689. * @returns {void}
  690. */
  691. notifyMouseLeave(event: MouseEvent) {
  692. this._sendEvent({
  693. name: 'mouse-leave',
  694. event: sanitizeMouseEvent(event)
  695. });
  696. }
  697. /**
  698. * Notify external application (if API is enabled) that the mouse has moved inside the iframe.
  699. *
  700. * @param {MouseEvent} event - The mousemove event.
  701. * @returns {void}
  702. */
  703. notifyMouseMove(event: MouseEvent) {
  704. this._sendEvent({
  705. name: 'mouse-move',
  706. event: sanitizeMouseEvent(event)
  707. });
  708. }
  709. /**
  710. * Notify external application that the video quality setting has changed.
  711. *
  712. * @param {number} videoQuality - The video quality. The number represents the maximum height of the video streams.
  713. * @returns {void}
  714. */
  715. notifyVideoQualityChanged(videoQuality: number) {
  716. this._sendEvent({
  717. name: 'video-quality-changed',
  718. videoQuality
  719. });
  720. }
  721. /**
  722. * Notify external application (if API is enabled) that message was
  723. * received.
  724. *
  725. * @param {Object} options - Object with the message properties.
  726. * @returns {void}
  727. */
  728. notifyReceivedChatMessage(
  729. { body, id, nick, privateMessage, ts }: {
  730. body: *, id: string, nick: string, privateMessage: boolean, ts: *
  731. } = {}) {
  732. if (APP.conference.isLocalId(id)) {
  733. return;
  734. }
  735. this._sendEvent({
  736. name: 'incoming-message',
  737. from: id,
  738. message: body,
  739. nick,
  740. privateMessage,
  741. stamp: ts
  742. });
  743. }
  744. /**
  745. * Notify external application (if API is enabled) that user joined the
  746. * conference.
  747. *
  748. * @param {string} id - User id.
  749. * @param {Object} props - The display name of the user.
  750. * @returns {void}
  751. */
  752. notifyUserJoined(id: string, props: Object) {
  753. this._sendEvent({
  754. name: 'participant-joined',
  755. id,
  756. ...props
  757. });
  758. }
  759. /**
  760. * Notify external application (if API is enabled) that user left the
  761. * conference.
  762. *
  763. * @param {string} id - User id.
  764. * @returns {void}
  765. */
  766. notifyUserLeft(id: string) {
  767. this._sendEvent({
  768. name: 'participant-left',
  769. id
  770. });
  771. }
  772. /**
  773. * Notify external application (if API is enabled) that the user role
  774. * has changed.
  775. *
  776. * @param {string} id - User id.
  777. * @param {string} role - The new user role.
  778. * @returns {void}
  779. */
  780. notifyUserRoleChanged(id: string, role: string) {
  781. this._sendEvent({
  782. name: 'participant-role-changed',
  783. id,
  784. role
  785. });
  786. }
  787. /**
  788. * Notify external application (if API is enabled) that user changed their
  789. * avatar.
  790. *
  791. * @param {string} id - User id.
  792. * @param {string} avatarURL - The new avatar URL of the participant.
  793. * @returns {void}
  794. */
  795. notifyAvatarChanged(id: string, avatarURL: string) {
  796. this._sendEvent({
  797. name: 'avatar-changed',
  798. avatarURL,
  799. id
  800. });
  801. }
  802. /**
  803. * Notify external application (if API is enabled) that user received
  804. * a text message through datachannels.
  805. *
  806. * @param {Object} data - The event data.
  807. * @returns {void}
  808. */
  809. notifyEndpointTextMessageReceived(data: Object) {
  810. this._sendEvent({
  811. name: 'endpoint-text-message-received',
  812. data
  813. });
  814. }
  815. /**
  816. * Notify external application (if API is enabled) that the list of sharing participants changed.
  817. *
  818. * @param {Object} data - The event data.
  819. * @returns {void}
  820. */
  821. notifySharingParticipantsChanged(data: Object) {
  822. this._sendEvent({
  823. name: 'content-sharing-participants-changed',
  824. data
  825. });
  826. }
  827. /**
  828. * Notify external application (if API is enabled) that the device list has
  829. * changed.
  830. *
  831. * @param {Object} devices - The new device list.
  832. * @returns {void}
  833. */
  834. notifyDeviceListChanged(devices: Object) {
  835. this._sendEvent({
  836. name: 'device-list-changed',
  837. devices
  838. });
  839. }
  840. /**
  841. * Notify external application (if API is enabled) that user changed their
  842. * nickname.
  843. *
  844. * @param {string} id - User id.
  845. * @param {string} displayname - User nickname.
  846. * @param {string} formattedDisplayName - The display name shown in Jitsi
  847. * meet's UI for the user.
  848. * @returns {void}
  849. */
  850. notifyDisplayNameChanged(
  851. id: string,
  852. { displayName, formattedDisplayName }: Object) {
  853. this._sendEvent({
  854. name: 'display-name-change',
  855. displayname: displayName,
  856. formattedDisplayName,
  857. id
  858. });
  859. }
  860. /**
  861. * Notify external application (if API is enabled) that user changed their
  862. * email.
  863. *
  864. * @param {string} id - User id.
  865. * @param {string} email - The new email of the participant.
  866. * @returns {void}
  867. */
  868. notifyEmailChanged(
  869. id: string,
  870. { email }: Object) {
  871. this._sendEvent({
  872. name: 'email-change',
  873. email,
  874. id
  875. });
  876. }
  877. /**
  878. * Notify external application (if API is enabled) that the an error has been logged.
  879. *
  880. * @param {string} logLevel - The message log level.
  881. * @param {Array} args - Array of strings composing the log message.
  882. * @returns {void}
  883. */
  884. notifyLog(logLevel: string, args: Array<string>) {
  885. this._sendEvent({
  886. name: 'log',
  887. logLevel,
  888. args
  889. });
  890. }
  891. /**
  892. * Notify external application (if API is enabled) that the conference has
  893. * been joined.
  894. *
  895. * @param {string} roomName - The room name.
  896. * @param {string} id - The id of the local user.
  897. * @param {Object} props - The display name and avatar URL of the local
  898. * user.
  899. * @returns {void}
  900. */
  901. notifyConferenceJoined(roomName: string, id: string, props: Object) {
  902. this._sendEvent({
  903. name: 'video-conference-joined',
  904. roomName,
  905. id,
  906. ...props
  907. });
  908. }
  909. /**
  910. * Notify external application (if API is enabled) that local user has left the conference.
  911. *
  912. * @param {string} roomName - User id.
  913. * @returns {void}
  914. */
  915. notifyConferenceLeft(roomName: string) {
  916. this._sendEvent({
  917. name: 'video-conference-left',
  918. roomName
  919. });
  920. }
  921. /**
  922. * Notify external application (if API is enabled) that we are ready to be
  923. * closed.
  924. *
  925. * @returns {void}
  926. */
  927. notifyReadyToClose() {
  928. this._sendEvent({ name: 'video-ready-to-close' });
  929. }
  930. /**
  931. * Notify external application (if API is enabled) that a suspend event in host computer.
  932. *
  933. * @returns {void}
  934. */
  935. notifySuspendDetected() {
  936. this._sendEvent({ name: 'suspend-detected' });
  937. }
  938. /**
  939. * Notify external application (if API is enabled) for audio muted status
  940. * changed.
  941. *
  942. * @param {boolean} muted - The new muted status.
  943. * @returns {void}
  944. */
  945. notifyAudioMutedStatusChanged(muted: boolean) {
  946. this._sendEvent({
  947. name: 'audio-mute-status-changed',
  948. muted
  949. });
  950. }
  951. /**
  952. * Notify external application (if API is enabled) for video muted status
  953. * changed.
  954. *
  955. * @param {boolean} muted - The new muted status.
  956. * @returns {void}
  957. */
  958. notifyVideoMutedStatusChanged(muted: boolean) {
  959. this._sendEvent({
  960. name: 'video-mute-status-changed',
  961. muted
  962. });
  963. }
  964. /**
  965. * Notify external application (if API is enabled) for audio availability
  966. * changed.
  967. *
  968. * @param {boolean} available - True if available and false otherwise.
  969. * @returns {void}
  970. */
  971. notifyAudioAvailabilityChanged(available: boolean) {
  972. audioAvailable = available;
  973. this._sendEvent({
  974. name: 'audio-availability-changed',
  975. available
  976. });
  977. }
  978. /**
  979. * Notify external application (if API is enabled) for video available
  980. * status changed.
  981. *
  982. * @param {boolean} available - True if available and false otherwise.
  983. * @returns {void}
  984. */
  985. notifyVideoAvailabilityChanged(available: boolean) {
  986. videoAvailable = available;
  987. this._sendEvent({
  988. name: 'video-availability-changed',
  989. available
  990. });
  991. }
  992. /**
  993. * Notify external application (if API is enabled) that the on stage
  994. * participant has changed.
  995. *
  996. * @param {string} id - User id of the new on stage participant.
  997. * @returns {void}
  998. */
  999. notifyOnStageParticipantChanged(id: string) {
  1000. this._sendEvent({
  1001. name: 'on-stage-participant-changed',
  1002. id
  1003. });
  1004. }
  1005. /**
  1006. * Notify external application of an unexpected camera-related error having
  1007. * occurred.
  1008. *
  1009. * @param {string} type - The type of the camera error.
  1010. * @param {string} message - Additional information about the error.
  1011. * @returns {void}
  1012. */
  1013. notifyOnCameraError(type: string, message: string) {
  1014. this._sendEvent({
  1015. name: 'camera-error',
  1016. type,
  1017. message
  1018. });
  1019. }
  1020. /**
  1021. * Notify external application of an unexpected mic-related error having
  1022. * occurred.
  1023. *
  1024. * @param {string} type - The type of the mic error.
  1025. * @param {string} message - Additional information about the error.
  1026. * @returns {void}
  1027. */
  1028. notifyOnMicError(type: string, message: string) {
  1029. this._sendEvent({
  1030. name: 'mic-error',
  1031. type,
  1032. message
  1033. });
  1034. }
  1035. /**
  1036. * Notify external application (if API is enabled) that conference feedback
  1037. * has been submitted. Intended to be used in conjunction with the
  1038. * submit-feedback command to get notified if feedback was submitted.
  1039. *
  1040. * @param {string} error - A failure message, if any.
  1041. * @returns {void}
  1042. */
  1043. notifyFeedbackSubmitted(error: string) {
  1044. this._sendEvent({
  1045. name: 'feedback-submitted',
  1046. error
  1047. });
  1048. }
  1049. /**
  1050. * Notify external application (if API is enabled) that the feedback prompt
  1051. * has been displayed.
  1052. *
  1053. * @returns {void}
  1054. */
  1055. notifyFeedbackPromptDisplayed() {
  1056. this._sendEvent({ name: 'feedback-prompt-displayed' });
  1057. }
  1058. /**
  1059. * Notify external application (if API is enabled) that the display
  1060. * configuration of the filmstrip has been changed.
  1061. *
  1062. * @param {boolean} visible - Whether or not the filmstrip has been set to
  1063. * be displayed or hidden.
  1064. * @returns {void}
  1065. */
  1066. notifyFilmstripDisplayChanged(visible: boolean) {
  1067. this._sendEvent({
  1068. name: 'filmstrip-display-changed',
  1069. visible
  1070. });
  1071. }
  1072. /**
  1073. * Notify external application of a participant, remote or local, being
  1074. * removed from the conference by another participant.
  1075. *
  1076. * @param {string} kicked - The ID of the participant removed from the
  1077. * conference.
  1078. * @param {string} kicker - The ID of the participant that removed the
  1079. * other participant.
  1080. * @returns {void}
  1081. */
  1082. notifyKickedOut(kicked: Object, kicker: Object) {
  1083. this._sendEvent({
  1084. name: 'participant-kicked-out',
  1085. kicked,
  1086. kicker
  1087. });
  1088. }
  1089. /**
  1090. * Notify external application of the current meeting requiring a password
  1091. * to join.
  1092. *
  1093. * @returns {void}
  1094. */
  1095. notifyOnPasswordRequired() {
  1096. this._sendEvent({ name: 'password-required' });
  1097. }
  1098. /**
  1099. * Notify external application (if API is enabled) that the screen sharing
  1100. * has been turned on/off.
  1101. *
  1102. * @param {boolean} on - True if screen sharing is enabled.
  1103. * @param {Object} details - Additional information about the screen
  1104. * sharing.
  1105. * @param {string} details.sourceType - Type of device or window the screen
  1106. * share is capturing.
  1107. * @returns {void}
  1108. */
  1109. notifyScreenSharingStatusChanged(on: boolean, details: Object) {
  1110. this._sendEvent({
  1111. name: 'screen-sharing-status-changed',
  1112. on,
  1113. details
  1114. });
  1115. }
  1116. /**
  1117. * Notify external application (if API is enabled) that the dominant speaker
  1118. * has been turned on/off.
  1119. *
  1120. * @param {string} id - Id of the dominant participant.
  1121. * @returns {void}
  1122. */
  1123. notifyDominantSpeakerChanged(id: string) {
  1124. this._sendEvent({
  1125. name: 'dominant-speaker-changed',
  1126. id
  1127. });
  1128. }
  1129. /**
  1130. * Notify external application (if API is enabled) that the conference
  1131. * changed their subject.
  1132. *
  1133. * @param {string} subject - Conference subject.
  1134. * @returns {void}
  1135. */
  1136. notifySubjectChanged(subject: string) {
  1137. this._sendEvent({
  1138. name: 'subject-change',
  1139. subject
  1140. });
  1141. }
  1142. /**
  1143. * Notify external application (if API is enabled) that tile view has been
  1144. * entered or exited.
  1145. *
  1146. * @param {string} enabled - True if tile view is currently displayed, false
  1147. * otherwise.
  1148. * @returns {void}
  1149. */
  1150. notifyTileViewChanged(enabled: boolean) {
  1151. this._sendEvent({
  1152. name: 'tile-view-changed',
  1153. enabled
  1154. });
  1155. }
  1156. /**
  1157. * Notify external application (if API is enabled) that the localStorage has changed.
  1158. *
  1159. * @param {string} localStorageContent - The new localStorageContent.
  1160. * @returns {void}
  1161. */
  1162. notifyLocalStorageChanged(localStorageContent: string) {
  1163. this._sendEvent({
  1164. name: 'local-storage-changed',
  1165. localStorageContent
  1166. });
  1167. }
  1168. /**
  1169. * Notify external application (if API is enabled) that user updated their hand raised.
  1170. *
  1171. * @param {string} id - User id.
  1172. * @param {boolean} handRaised - Whether user has raised hand.
  1173. * @returns {void}
  1174. */
  1175. notifyRaiseHandUpdated(id: string, handRaised: boolean) {
  1176. this._sendEvent({
  1177. name: 'raise-hand-updated',
  1178. handRaised,
  1179. id
  1180. });
  1181. }
  1182. /**
  1183. * Notify external application (if API is enabled) that recording has started or stopped.
  1184. *
  1185. * @param {boolean} on - True if recording is on, false otherwise.
  1186. * @param {string} mode - Stream or file.
  1187. * @param {string} error - Error type or null if success.
  1188. * @returns {void}
  1189. */
  1190. notifyRecordingStatusChanged(on: boolean, mode: string, error?: string) {
  1191. this._sendEvent({
  1192. name: 'recording-status-changed',
  1193. on,
  1194. mode,
  1195. error
  1196. });
  1197. }
  1198. /**
  1199. * Disposes the allocated resources.
  1200. *
  1201. * @returns {void}
  1202. */
  1203. dispose() {
  1204. if (this._enabled) {
  1205. this._enabled = false;
  1206. }
  1207. }
  1208. }
  1209. export default new API();