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

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