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

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