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 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618
  1. // @flow
  2. import * as JitsiMeetConferenceEvents from '../../ConferenceEvents';
  3. import {
  4. createApiEvent,
  5. sendAnalytics
  6. } from '../../react/features/analytics';
  7. import { setSubject } from '../../react/features/base/conference';
  8. import { parseJWTFromURLParams } from '../../react/features/base/jwt';
  9. import { invite } from '../../react/features/invite';
  10. import { getJitsiMeetTransport } from '../transport';
  11. import { API_ID } from './constants';
  12. const logger = require('jitsi-meet-logger').getLogger(__filename);
  13. declare var APP: Object;
  14. /**
  15. * List of the available commands.
  16. */
  17. let commands = {};
  18. /**
  19. * The state of screen sharing(started/stopped) before the screen sharing is
  20. * enabled and initialized.
  21. * NOTE: This flag help us to cache the state and use it if toggle-share-screen
  22. * was received before the initialization.
  23. */
  24. let initialScreenSharingState = false;
  25. /**
  26. * The transport instance used for communication with external apps.
  27. *
  28. * @type {Transport}
  29. */
  30. const transport = getJitsiMeetTransport();
  31. /**
  32. * The current audio availability.
  33. *
  34. * @type {boolean}
  35. */
  36. let audioAvailable = true;
  37. /**
  38. * The current video availability.
  39. *
  40. * @type {boolean}
  41. */
  42. let videoAvailable = true;
  43. /**
  44. * Initializes supported commands.
  45. *
  46. * @returns {void}
  47. */
  48. function initCommands() {
  49. commands = {
  50. 'display-name': displayName => {
  51. sendAnalytics(createApiEvent('display.name.changed'));
  52. APP.conference.changeLocalDisplayName(displayName);
  53. },
  54. 'proxy-connection-event': event => {
  55. APP.conference.onProxyConnectionEvent(event);
  56. },
  57. 'subject': subject => {
  58. sendAnalytics(createApiEvent('subject.changed'));
  59. APP.store.dispatch(setSubject(subject));
  60. },
  61. 'submit-feedback': feedback => {
  62. sendAnalytics(createApiEvent('submit.feedback'));
  63. APP.conference.submitFeedback(feedback.score, feedback.message);
  64. },
  65. 'toggle-audio': () => {
  66. sendAnalytics(createApiEvent('toggle-audio'));
  67. logger.log('Audio toggle: API command received');
  68. APP.conference.toggleAudioMuted(false /* no UI */);
  69. },
  70. 'toggle-video': () => {
  71. sendAnalytics(createApiEvent('toggle-video'));
  72. logger.log('Video toggle: API command received');
  73. APP.conference.toggleVideoMuted(false /* no UI */);
  74. },
  75. 'toggle-film-strip': () => {
  76. sendAnalytics(createApiEvent('film.strip.toggled'));
  77. APP.UI.toggleFilmstrip();
  78. },
  79. 'toggle-chat': () => {
  80. sendAnalytics(createApiEvent('chat.toggled'));
  81. APP.UI.toggleChat();
  82. },
  83. 'toggle-share-screen': () => {
  84. sendAnalytics(createApiEvent('screen.sharing.toggled'));
  85. toggleScreenSharing();
  86. },
  87. 'video-hangup': () => {
  88. sendAnalytics(createApiEvent('video.hangup'));
  89. APP.conference.hangup(true);
  90. },
  91. 'email': email => {
  92. sendAnalytics(createApiEvent('email.changed'));
  93. APP.conference.changeLocalEmail(email);
  94. },
  95. 'avatar-url': avatarUrl => {
  96. sendAnalytics(createApiEvent('avatar.url.changed'));
  97. APP.conference.changeLocalAvatarUrl(avatarUrl);
  98. }
  99. };
  100. transport.on('event', ({ data, name }) => {
  101. if (name && commands[name]) {
  102. commands[name](...data);
  103. return true;
  104. }
  105. return false;
  106. });
  107. transport.on('request', (request, callback) => {
  108. const { name } = request;
  109. switch (name) {
  110. case 'invite': {
  111. const { invitees } = request;
  112. if (!Array.isArray(invitees) || invitees.length === 0) {
  113. callback({
  114. error: new Error('Unexpected format of invitees')
  115. });
  116. break;
  117. }
  118. // The store should be already available because API.init is called
  119. // on appWillMount action.
  120. APP.store.dispatch(
  121. invite(invitees, true))
  122. .then(failedInvitees => {
  123. let error;
  124. let result;
  125. if (failedInvitees.length) {
  126. error = new Error('One or more invites failed!');
  127. } else {
  128. result = true;
  129. }
  130. callback({
  131. error,
  132. result
  133. });
  134. });
  135. break;
  136. }
  137. case 'is-audio-muted':
  138. callback(APP.conference.isLocalAudioMuted());
  139. break;
  140. case 'is-video-muted':
  141. callback(APP.conference.isLocalVideoMuted());
  142. break;
  143. case 'is-audio-available':
  144. callback(audioAvailable);
  145. break;
  146. case 'is-video-available':
  147. callback(videoAvailable);
  148. break;
  149. default:
  150. return false;
  151. }
  152. return true;
  153. });
  154. }
  155. /**
  156. * Listens for desktop/screen sharing enabled events and toggles the screen
  157. * sharing if needed.
  158. *
  159. * @param {boolean} enabled - Current screen sharing enabled status.
  160. * @returns {void}
  161. */
  162. function onDesktopSharingEnabledChanged(enabled = false) {
  163. if (enabled && initialScreenSharingState) {
  164. toggleScreenSharing();
  165. }
  166. }
  167. /**
  168. * Check whether the API should be enabled or not.
  169. *
  170. * @returns {boolean}
  171. */
  172. function shouldBeEnabled() {
  173. return (
  174. typeof API_ID === 'number'
  175. // XXX Enable the API when a JSON Web Token (JWT) is specified in
  176. // the location/URL because then it is very likely that the Jitsi
  177. // Meet (Web) app is being used by an external/wrapping (Web) app
  178. // and, consequently, the latter will need to communicate with the
  179. // former. (The described logic is merely a heuristic though.)
  180. || parseJWTFromURLParams());
  181. }
  182. /**
  183. * Executes on toggle-share-screen command.
  184. *
  185. * @returns {void}
  186. */
  187. function toggleScreenSharing() {
  188. if (APP.conference.isDesktopSharingEnabled) {
  189. // eslint-disable-next-line no-empty-function
  190. APP.conference.toggleScreenSharing().catch(() => {});
  191. } else {
  192. initialScreenSharingState = !initialScreenSharingState;
  193. }
  194. }
  195. /**
  196. * Implements API class that communicates with external API class and provides
  197. * interface to access Jitsi Meet features by external applications that embed
  198. * Jitsi Meet.
  199. */
  200. class API {
  201. _enabled: boolean;
  202. /**
  203. * Initializes the API. Setups message event listeners that will receive
  204. * information from external applications that embed Jitsi Meet. It also
  205. * sends a message to the external application that API is initialized.
  206. *
  207. * @param {Object} options - Optional parameters.
  208. * @returns {void}
  209. */
  210. init() {
  211. if (!shouldBeEnabled()) {
  212. return;
  213. }
  214. /**
  215. * Current status (enabled/disabled) of API.
  216. *
  217. * @private
  218. * @type {boolean}
  219. */
  220. this._enabled = true;
  221. APP.conference.addListener(
  222. JitsiMeetConferenceEvents.DESKTOP_SHARING_ENABLED_CHANGED,
  223. onDesktopSharingEnabledChanged);
  224. initCommands();
  225. }
  226. /**
  227. * Notify external application (if API is enabled) that the large video
  228. * visibility changed.
  229. *
  230. * @param {boolean} isHidden - True if the large video is hidden and false
  231. * otherwise.
  232. * @returns {void}
  233. */
  234. notifyLargeVideoVisibilityChanged(isHidden: boolean) {
  235. this._sendEvent({
  236. name: 'large-video-visibility-changed',
  237. isVisible: !isHidden
  238. });
  239. }
  240. /**
  241. * Notifies the external application (spot) that the local jitsi-participant
  242. * has a status update.
  243. *
  244. * @param {Object} event - The message to pass onto spot.
  245. * @returns {void}
  246. */
  247. sendProxyConnectionEvent(event: Object) {
  248. this._sendEvent({
  249. name: 'proxy-connection-event',
  250. ...event
  251. });
  252. }
  253. /**
  254. * Sends event to the external application.
  255. *
  256. * @param {Object} event - The event to be sent.
  257. * @returns {void}
  258. */
  259. _sendEvent(event: Object = {}) {
  260. if (this._enabled) {
  261. transport.sendEvent(event);
  262. }
  263. }
  264. /**
  265. * Notify external application (if API is enabled) that message was sent.
  266. *
  267. * @param {string} message - Message body.
  268. * @returns {void}
  269. */
  270. notifySendingChatMessage(message: string) {
  271. this._sendEvent({
  272. name: 'outgoing-message',
  273. message
  274. });
  275. }
  276. /**
  277. * Notify external application (if API is enabled) that message was
  278. * received.
  279. *
  280. * @param {Object} options - Object with the message properties.
  281. * @returns {void}
  282. */
  283. notifyReceivedChatMessage(
  284. { body, id, nick, ts }: {
  285. body: *, id: string, nick: string, ts: *
  286. } = {}) {
  287. if (APP.conference.isLocalId(id)) {
  288. return;
  289. }
  290. this._sendEvent({
  291. name: 'incoming-message',
  292. from: id,
  293. message: body,
  294. nick,
  295. stamp: ts
  296. });
  297. }
  298. /**
  299. * Notify external application (if API is enabled) that user joined the
  300. * conference.
  301. *
  302. * @param {string} id - User id.
  303. * @param {Object} props - The display name of the user.
  304. * @returns {void}
  305. */
  306. notifyUserJoined(id: string, props: Object) {
  307. this._sendEvent({
  308. name: 'participant-joined',
  309. id,
  310. ...props
  311. });
  312. }
  313. /**
  314. * Notify external application (if API is enabled) that user left the
  315. * conference.
  316. *
  317. * @param {string} id - User id.
  318. * @returns {void}
  319. */
  320. notifyUserLeft(id: string) {
  321. this._sendEvent({
  322. name: 'participant-left',
  323. id
  324. });
  325. }
  326. /**
  327. * Notify external application (if API is enabled) that user changed their
  328. * avatar.
  329. *
  330. * @param {string} id - User id.
  331. * @param {string} avatarURL - The new avatar URL of the participant.
  332. * @returns {void}
  333. */
  334. notifyAvatarChanged(id: string, avatarURL: string) {
  335. this._sendEvent({
  336. name: 'avatar-changed',
  337. avatarURL,
  338. id
  339. });
  340. }
  341. /**
  342. * Notify external application (if API is enabled) that user changed their
  343. * nickname.
  344. *
  345. * @param {string} id - User id.
  346. * @param {string} displayname - User nickname.
  347. * @param {string} formattedDisplayName - The display name shown in Jitsi
  348. * meet's UI for the user.
  349. * @returns {void}
  350. */
  351. notifyDisplayNameChanged(
  352. id: string,
  353. { displayName, formattedDisplayName }: Object) {
  354. this._sendEvent({
  355. name: 'display-name-change',
  356. displayname: displayName,
  357. formattedDisplayName,
  358. id
  359. });
  360. }
  361. /**
  362. * Notify external application (if API is enabled) that user changed their
  363. * email.
  364. *
  365. * @param {string} id - User id.
  366. * @param {string} email - The new email of the participant.
  367. * @returns {void}
  368. */
  369. notifyEmailChanged(
  370. id: string,
  371. { email }: Object) {
  372. this._sendEvent({
  373. name: 'email-change',
  374. email,
  375. id
  376. });
  377. }
  378. /**
  379. * Notify external application (if API is enabled) that the conference has
  380. * been joined.
  381. *
  382. * @param {string} roomName - The room name.
  383. * @param {string} id - The id of the local user.
  384. * @param {Object} props - The display name and avatar URL of the local
  385. * user.
  386. * @returns {void}
  387. */
  388. notifyConferenceJoined(roomName: string, id: string, props: Object) {
  389. this._sendEvent({
  390. name: 'video-conference-joined',
  391. roomName,
  392. id,
  393. ...props
  394. });
  395. }
  396. /**
  397. * Notify external application (if API is enabled) that user changed their
  398. * nickname.
  399. *
  400. * @param {string} roomName - User id.
  401. * @returns {void}
  402. */
  403. notifyConferenceLeft(roomName: string) {
  404. this._sendEvent({
  405. name: 'video-conference-left',
  406. roomName
  407. });
  408. }
  409. /**
  410. * Notify external application (if API is enabled) that we are ready to be
  411. * closed.
  412. *
  413. * @returns {void}
  414. */
  415. notifyReadyToClose() {
  416. this._sendEvent({ name: 'video-ready-to-close' });
  417. }
  418. /**
  419. * Notify external application (if API is enabled) for audio muted status
  420. * changed.
  421. *
  422. * @param {boolean} muted - The new muted status.
  423. * @returns {void}
  424. */
  425. notifyAudioMutedStatusChanged(muted: boolean) {
  426. this._sendEvent({
  427. name: 'audio-mute-status-changed',
  428. muted
  429. });
  430. }
  431. /**
  432. * Notify external application (if API is enabled) for video muted status
  433. * changed.
  434. *
  435. * @param {boolean} muted - The new muted status.
  436. * @returns {void}
  437. */
  438. notifyVideoMutedStatusChanged(muted: boolean) {
  439. this._sendEvent({
  440. name: 'video-mute-status-changed',
  441. muted
  442. });
  443. }
  444. /**
  445. * Notify external application (if API is enabled) for audio availability
  446. * changed.
  447. *
  448. * @param {boolean} available - True if available and false otherwise.
  449. * @returns {void}
  450. */
  451. notifyAudioAvailabilityChanged(available: boolean) {
  452. audioAvailable = available;
  453. this._sendEvent({
  454. name: 'audio-availability-changed',
  455. available
  456. });
  457. }
  458. /**
  459. * Notify external application (if API is enabled) for video available
  460. * status changed.
  461. *
  462. * @param {boolean} available - True if available and false otherwise.
  463. * @returns {void}
  464. */
  465. notifyVideoAvailabilityChanged(available: boolean) {
  466. videoAvailable = available;
  467. this._sendEvent({
  468. name: 'video-availability-changed',
  469. available
  470. });
  471. }
  472. /**
  473. * Notify external application (if API is enabled) that the on stage
  474. * participant has changed.
  475. *
  476. * @param {string} id - User id of the new on stage participant.
  477. * @returns {void}
  478. */
  479. notifyOnStageParticipantChanged(id: string) {
  480. this._sendEvent({
  481. name: 'on-stage-participant-changed',
  482. id
  483. });
  484. }
  485. /**
  486. * Notify external application (if API is enabled) that conference feedback
  487. * has been submitted. Intended to be used in conjunction with the
  488. * submit-feedback command to get notified if feedback was submitted.
  489. *
  490. * @returns {void}
  491. */
  492. notifyFeedbackSubmitted() {
  493. this._sendEvent({ name: 'feedback-submitted' });
  494. }
  495. /**
  496. * Notify external application (if API is enabled) that the feedback prompt
  497. * has been displayed.
  498. *
  499. * @returns {void}
  500. */
  501. notifyFeedbackPromptDisplayed() {
  502. this._sendEvent({ name: 'feedback-prompt-displayed' });
  503. }
  504. /**
  505. * Notify external application (if API is enabled) that the display
  506. * configuration of the filmstrip has been changed.
  507. *
  508. * @param {boolean} visible - Whether or not the filmstrip has been set to
  509. * be displayed or hidden.
  510. * @returns {void}
  511. */
  512. notifyFilmstripDisplayChanged(visible: boolean) {
  513. this._sendEvent({
  514. name: 'filmstrip-display-changed',
  515. visible
  516. });
  517. }
  518. /**
  519. * Notify external application (if API is enabled) that the screen sharing
  520. * has been turned on/off.
  521. *
  522. * @param {boolean} on - True if screen sharing is enabled.
  523. * @param {Object} details - Additional information about the screen
  524. * sharing.
  525. * @param {string} details.sourceType - Type of device or window the screen
  526. * share is capturing.
  527. * @returns {void}
  528. */
  529. notifyScreenSharingStatusChanged(on: boolean, details: Object) {
  530. this._sendEvent({
  531. name: 'screen-sharing-status-changed',
  532. on,
  533. details
  534. });
  535. }
  536. /**
  537. * Notify external application (if API is enabled) that the conference
  538. * changed their subject.
  539. *
  540. * @param {string} subject - Conference subject.
  541. * @returns {void}
  542. */
  543. notifySubjectChanged(subject: string) {
  544. this._sendEvent({
  545. name: 'subject-change',
  546. subject
  547. });
  548. }
  549. /**
  550. * Disposes the allocated resources.
  551. *
  552. * @returns {void}
  553. */
  554. dispose() {
  555. if (this._enabled) {
  556. this._enabled = false;
  557. APP.conference.removeListener(
  558. JitsiMeetConferenceEvents.DESKTOP_SHARING_ENABLED_CHANGED,
  559. onDesktopSharingEnabledChanged);
  560. }
  561. }
  562. }
  563. export default new API();