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.

ScreenObtainer.js 24KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732
  1. /* global chrome, $, alert */
  2. import JitsiTrackError from '../../JitsiTrackError';
  3. import * as JitsiTrackErrors from '../../JitsiTrackErrors';
  4. import browser from '../browser';
  5. const logger = require('jitsi-meet-logger').getLogger(__filename);
  6. const GlobalOnErrorHandler = require('../util/GlobalOnErrorHandler');
  7. /**
  8. * Indicates whether the Chrome desktop sharing extension is installed.
  9. * @type {boolean}
  10. */
  11. let chromeExtInstalled = false;
  12. /**
  13. * Indicates whether an update of the Chrome desktop sharing extension is
  14. * required.
  15. * @type {boolean}
  16. */
  17. let chromeExtUpdateRequired = false;
  18. let gumFunction = null;
  19. /**
  20. * The error returned by chrome when trying to start inline installation from
  21. * popup.
  22. */
  23. const CHROME_EXTENSION_POPUP_ERROR
  24. = 'Inline installs can not be initiated from pop-up windows.';
  25. /**
  26. * The error returned by chrome when trying to start inline installation from
  27. * iframe.
  28. */
  29. const CHROME_EXTENSION_IFRAME_ERROR
  30. = 'Chrome Web Store installations can only be started by the top frame.';
  31. /**
  32. * The error returned by chrome when trying to start inline installation
  33. * not from the "main" whitelisted site.
  34. * @type {string}
  35. */
  36. const CHROME_EXTENSION_INLINE_ERROR
  37. = 'Installs can only be initiated by one of'
  38. + ' the Chrome Web Store item\'s verified sites.';
  39. /**
  40. * The error returned by chrome when trying to start inline installation
  41. * with extension that doesn't support inline installation.
  42. *
  43. * @type {string}
  44. */
  45. const CHROME_EXTENSION_INLINE_NOT_SUPPORTED_ERROR
  46. = 'Inline installation is not supported for this item. '
  47. + 'The user will be redirected to the Chrome Web Store.';
  48. /**
  49. * The error message returned by chrome when the extension is installed.
  50. */
  51. const CHROME_NO_EXTENSION_ERROR_MSG // eslint-disable-line no-unused-vars
  52. = 'Could not establish connection. Receiving end does not exist.';
  53. /**
  54. * The error message returned by chrome when the extension install action needs
  55. * to be initiated by a user gesture.
  56. * @type {string}
  57. */
  58. const CHROME_USER_GESTURE_REQ_ERROR
  59. = 'Chrome Web Store installations can only be initated by a user gesture.';
  60. /**
  61. * The error message returned by chrome when the extension install has been
  62. * cancelled by the user.
  63. * @type {string}
  64. */
  65. const CHROME_USER_CANCEL_EXTENSION_INSTALL = 'User cancelled install';
  66. /**
  67. * Handles obtaining a stream from a screen capture on different browsers.
  68. */
  69. const ScreenObtainer = {
  70. /**
  71. * If not <tt>null</tt> it means that the initialization process is still in
  72. * progress. It is used to make desktop stream request wait and continue
  73. * after it's done.
  74. * {@type Promise|null}
  75. */
  76. intChromeExtPromise: null,
  77. obtainStream: null,
  78. /**
  79. * Initializes the function used to obtain a screen capture
  80. * (this.obtainStream).
  81. *
  82. * @param {object} options
  83. * @param {boolean} [options.desktopSharingChromeDisabled]
  84. * @param {boolean} [options.desktopSharingChromeExtId]
  85. * @param {boolean} [options.desktopSharingFirefoxDisabled]
  86. * @param {Function} gum GUM method
  87. */
  88. init(options = {
  89. desktopSharingChromeDisabled: false,
  90. desktopSharingChromeExtId: null,
  91. desktopSharingFirefoxDisabled: false
  92. }, gum) {
  93. this.options = options;
  94. gumFunction = gum;
  95. this.obtainStream = this._createObtainStreamMethod(options);
  96. if (!this.obtainStream) {
  97. logger.info('Desktop sharing disabled');
  98. }
  99. },
  100. /**
  101. * Returns a method which will be used to obtain the screen sharing stream
  102. * (based on the browser type).
  103. *
  104. * @param {object} options passed from {@link init} - check description
  105. * there
  106. * @returns {Function}
  107. * @private
  108. */
  109. _createObtainStreamMethod(options) {
  110. if (browser.isNWJS()) {
  111. return (_, onSuccess, onFailure) => {
  112. window.JitsiMeetNW.obtainDesktopStream(
  113. onSuccess,
  114. (error, constraints) => {
  115. let jitsiError;
  116. // FIXME:
  117. // This is very very dirty fix for recognising that the
  118. // user have clicked the cancel button from the Desktop
  119. // sharing pick window. The proper solution would be to
  120. // detect this in the NWJS application by checking the
  121. // streamId === "". Even better solution would be to
  122. // stop calling GUM from the NWJS app and just pass the
  123. // streamId to lib-jitsi-meet. This way the desktop
  124. // sharing implementation for NWJS and chrome extension
  125. // will be the same and lib-jitsi-meet will be able to
  126. // control the constraints, check the streamId, etc.
  127. //
  128. // I cannot find documentation about "InvalidStateError"
  129. // but this is what we are receiving from GUM when the
  130. // streamId for the desktop sharing is "".
  131. if (error && error.name === 'InvalidStateError') {
  132. jitsiError = new JitsiTrackError(
  133. JitsiTrackErrors.CHROME_EXTENSION_USER_CANCELED
  134. );
  135. } else {
  136. jitsiError = new JitsiTrackError(
  137. error, constraints, [ 'desktop' ]);
  138. }
  139. (typeof onFailure === 'function')
  140. && onFailure(jitsiError);
  141. });
  142. };
  143. } else if (browser.isElectron()) {
  144. return this.obtainScreenOnElectron;
  145. } else if (browser.isChrome() || browser.isOpera()) {
  146. if (browser.isVersionLessThan('34')) {
  147. logger.info('Chrome extension not supported until ver 34');
  148. return null;
  149. } else if (browser.supportsGetDisplayMedia()
  150. && !options.desktopSharingChromeDisabled) {
  151. return this.obtainScreenFromGetDisplayMedia;
  152. } else if (options.desktopSharingChromeDisabled
  153. || !options.desktopSharingChromeExtId) {
  154. return null;
  155. }
  156. logger.info('Using Chrome extension for desktop sharing');
  157. this.intChromeExtPromise
  158. = initChromeExtension(options).then(() => {
  159. this.intChromeExtPromise = null;
  160. });
  161. return this.obtainScreenFromExtension;
  162. } else if (browser.isFirefox()) {
  163. if (options.desktopSharingFirefoxDisabled) {
  164. return null;
  165. } else if (window.location.protocol === 'http:') {
  166. logger.log('Screen sharing is not supported over HTTP. '
  167. + 'Use of HTTPS is required.');
  168. return null;
  169. }
  170. return this.obtainScreenOnFirefox;
  171. } else if (browser.isEdge() && browser.supportsGetDisplayMedia()) {
  172. return this.obtainScreenFromGetDisplayMedia;
  173. }
  174. logger.log(
  175. 'Screen sharing not supported by the current browser: ',
  176. browser.getName());
  177. return null;
  178. },
  179. /**
  180. * Checks whether obtaining a screen capture is supported in the current
  181. * environment.
  182. * @returns {boolean}
  183. */
  184. isSupported() {
  185. return this.obtainStream !== null;
  186. },
  187. /**
  188. * Obtains a screen capture stream on Firefox.
  189. * @param callback
  190. * @param errorCallback
  191. */
  192. obtainScreenOnFirefox(options, callback, errorCallback) {
  193. obtainWebRTCScreen(options.gumOptions, callback, errorCallback);
  194. },
  195. /**
  196. * Obtains a screen capture stream on Electron.
  197. *
  198. * @param {Object} [options] - Screen sharing options.
  199. * @param {Array<string>} [options.desktopSharingSources] - Array with the
  200. * sources that have to be displayed in the desktop picker window ('screen',
  201. * 'window', etc.).
  202. * @param onSuccess - Success callback.
  203. * @param onFailure - Failure callback.
  204. */
  205. obtainScreenOnElectron(options = {}, onSuccess, onFailure) {
  206. if (window.JitsiMeetScreenObtainer
  207. && window.JitsiMeetScreenObtainer.openDesktopPicker) {
  208. const { desktopSharingSources, gumOptions } = options;
  209. window.JitsiMeetScreenObtainer.openDesktopPicker(
  210. {
  211. desktopSharingSources: desktopSharingSources
  212. || this.options.desktopSharingChromeSources
  213. },
  214. (streamId, streamType) =>
  215. onGetStreamResponse(
  216. {
  217. response: {
  218. streamId,
  219. streamType
  220. },
  221. gumOptions
  222. },
  223. onSuccess,
  224. onFailure
  225. ),
  226. err => onFailure(new JitsiTrackError(
  227. JitsiTrackErrors.ELECTRON_DESKTOP_PICKER_ERROR,
  228. err
  229. ))
  230. );
  231. } else {
  232. onFailure(new JitsiTrackError(
  233. JitsiTrackErrors.ELECTRON_DESKTOP_PICKER_NOT_FOUND));
  234. }
  235. },
  236. /**
  237. * Asks Chrome extension to call chooseDesktopMedia and gets chrome
  238. * 'desktop' stream for returned stream token.
  239. */
  240. obtainScreenFromExtension(options, streamCallback, failCallback) {
  241. if (this.intChromeExtPromise !== null) {
  242. this.intChromeExtPromise.then(() => {
  243. this.obtainScreenFromExtension(
  244. options, streamCallback, failCallback);
  245. });
  246. return;
  247. }
  248. const {
  249. desktopSharingChromeExtId,
  250. desktopSharingChromeSources
  251. } = this.options;
  252. const {
  253. gumOptions
  254. } = options;
  255. const doGetStreamFromExtensionOptions = {
  256. desktopSharingChromeExtId,
  257. desktopSharingChromeSources:
  258. options.desktopSharingSources || desktopSharingChromeSources,
  259. gumOptions
  260. };
  261. if (chromeExtInstalled) {
  262. doGetStreamFromExtension(
  263. doGetStreamFromExtensionOptions,
  264. streamCallback,
  265. failCallback);
  266. } else {
  267. if (chromeExtUpdateRequired) {
  268. /* eslint-disable no-alert */
  269. alert(
  270. 'Jitsi Desktop Streamer requires update. '
  271. + 'Changes will take effect after next Chrome restart.');
  272. /* eslint-enable no-alert */
  273. }
  274. // for opera there is no inline install
  275. // extension "Download Chrome Extension" allows us to open
  276. // the chrome webstore and install from there and then activate our
  277. // extension
  278. if (browser.isOpera()) {
  279. this.handleExternalInstall(options, streamCallback,
  280. failCallback);
  281. return;
  282. }
  283. try {
  284. chrome.webstore.install(
  285. getWebStoreInstallUrl(this.options),
  286. arg => {
  287. logger.log('Extension installed successfully', arg);
  288. chromeExtInstalled = true;
  289. // We need to give a moment to the endpoint to become
  290. // available.
  291. waitForExtensionAfterInstall(this.options, 200, 10)
  292. .then(() => {
  293. doGetStreamFromExtension(
  294. doGetStreamFromExtensionOptions,
  295. streamCallback,
  296. failCallback);
  297. })
  298. .catch(() => {
  299. this.handleExtensionInstallationError(options,
  300. streamCallback, failCallback);
  301. });
  302. },
  303. this.handleExtensionInstallationError.bind(this,
  304. options, streamCallback, failCallback)
  305. );
  306. } catch (e) {
  307. this.handleExtensionInstallationError(options, streamCallback,
  308. failCallback, e);
  309. }
  310. }
  311. },
  312. /* eslint-disable max-params */
  313. handleExternalInstall(options, streamCallback, failCallback, e) {
  314. const webStoreInstallUrl = getWebStoreInstallUrl(this.options);
  315. options.listener('waitingForExtension', webStoreInstallUrl);
  316. this.checkForChromeExtensionOnInterval(options, streamCallback,
  317. failCallback, e);
  318. },
  319. handleExtensionInstallationError(options, streamCallback, failCallback, e) {
  320. const webStoreInstallUrl = getWebStoreInstallUrl(this.options);
  321. if ((CHROME_EXTENSION_POPUP_ERROR === e
  322. || CHROME_EXTENSION_IFRAME_ERROR === e
  323. || CHROME_EXTENSION_INLINE_ERROR === e
  324. || CHROME_EXTENSION_INLINE_NOT_SUPPORTED_ERROR === e)
  325. && options.interval > 0
  326. && typeof options.checkAgain === 'function'
  327. && typeof options.listener === 'function') {
  328. this.handleExternalInstall(options, streamCallback,
  329. failCallback, e);
  330. return;
  331. }
  332. const msg
  333. = `Failed to install the extension from ${webStoreInstallUrl}`;
  334. logger.log(msg, e);
  335. let error;
  336. if (e === CHROME_USER_CANCEL_EXTENSION_INSTALL) {
  337. error = JitsiTrackErrors.CHROME_EXTENSION_USER_CANCELED;
  338. } else if (e === CHROME_USER_GESTURE_REQ_ERROR) {
  339. error = JitsiTrackErrors.CHROME_EXTENSION_USER_GESTURE_REQUIRED;
  340. } else {
  341. error = JitsiTrackErrors.CHROME_EXTENSION_INSTALLATION_ERROR;
  342. }
  343. failCallback(new JitsiTrackError(error, msg));
  344. },
  345. /* eslint-enable max-params */
  346. checkForChromeExtensionOnInterval(options, streamCallback, failCallback) {
  347. if (options.checkAgain() === false) {
  348. failCallback(new JitsiTrackError(
  349. JitsiTrackErrors.CHROME_EXTENSION_INSTALLATION_ERROR));
  350. return;
  351. }
  352. waitForExtensionAfterInstall(this.options, options.interval, 1)
  353. .then(() => {
  354. chromeExtInstalled = true;
  355. options.listener('extensionFound');
  356. this.obtainScreenFromExtension(options,
  357. streamCallback, failCallback);
  358. })
  359. .catch(() => {
  360. this.checkForChromeExtensionOnInterval(options,
  361. streamCallback, failCallback);
  362. });
  363. },
  364. /**
  365. * Obtains a screen capture stream using getDisplayMedia.
  366. *
  367. * @param callback - The success callback.
  368. * @param errorCallback - The error callback.
  369. */
  370. obtainScreenFromGetDisplayMedia(options, callback, errorCallback) {
  371. navigator.getDisplayMedia({ video: true })
  372. .then(stream => {
  373. let applyConstraintsPromise;
  374. if (stream
  375. && stream.getTracks()
  376. && stream.getTracks().length > 0) {
  377. applyConstraintsPromise = stream.getTracks()[0]
  378. .applyConstraints(options.trackOptions);
  379. } else {
  380. applyConstraintsPromise = Promise.resolve();
  381. }
  382. applyConstraintsPromise.then(() =>
  383. callback({
  384. stream,
  385. sourceId: stream.id
  386. }));
  387. })
  388. .catch(() =>
  389. errorCallback(new JitsiTrackError(JitsiTrackErrors
  390. .CHROME_EXTENSION_USER_CANCELED)));
  391. }
  392. };
  393. /**
  394. * Obtains a desktop stream using getUserMedia.
  395. * For this to work on Chrome, the
  396. * 'chrome://flags/#enable-usermedia-screen-capture' flag must be enabled.
  397. *
  398. * On firefox, the document's domain must be white-listed in the
  399. * 'media.getusermedia.screensharing.allowed_domains' preference in
  400. * 'about:config'.
  401. */
  402. function obtainWebRTCScreen(options, streamCallback, failCallback) {
  403. gumFunction([ 'screen' ], options)
  404. .then(stream => streamCallback({ stream }), failCallback);
  405. }
  406. /**
  407. * Constructs inline install URL for Chrome desktop streaming extension.
  408. * The 'chromeExtensionId' must be defined in options parameter.
  409. * @param options supports "desktopSharingChromeExtId"
  410. * @returns {string}
  411. */
  412. function getWebStoreInstallUrl(options) {
  413. return (
  414. `https://chrome.google.com/webstore/detail/${
  415. options.desktopSharingChromeExtId}`);
  416. }
  417. /**
  418. * Checks whether an update of the Chrome extension is required.
  419. * @param minVersion minimal required version
  420. * @param extVersion current extension version
  421. * @returns {boolean}
  422. */
  423. function isUpdateRequired(minVersion, extVersion) {
  424. try {
  425. const s1 = minVersion.split('.');
  426. const s2 = extVersion.split('.');
  427. const len = Math.max(s1.length, s2.length);
  428. for (let i = 0; i < len; i++) {
  429. let n1 = 0,
  430. n2 = 0;
  431. if (i < s1.length) {
  432. n1 = parseInt(s1[i], 10);
  433. }
  434. if (i < s2.length) {
  435. n2 = parseInt(s2[i], 10);
  436. }
  437. if (isNaN(n1) || isNaN(n2)) {
  438. return true;
  439. } else if (n1 !== n2) {
  440. return n1 > n2;
  441. }
  442. }
  443. // will happen if both versions have identical numbers in
  444. // their components (even if one of them is longer, has more components)
  445. return false;
  446. } catch (e) {
  447. GlobalOnErrorHandler.callErrorHandler(e);
  448. logger.error('Failed to parse extension version', e);
  449. return true;
  450. }
  451. }
  452. /**
  453. *
  454. * @param callback
  455. * @param options
  456. */
  457. function checkChromeExtInstalled(callback, options) {
  458. if (typeof chrome === 'undefined' || !chrome || !chrome.runtime) {
  459. // No API, so no extension for sure
  460. callback(false, false);
  461. return;
  462. }
  463. chrome.runtime.sendMessage(
  464. options.desktopSharingChromeExtId,
  465. { getVersion: true },
  466. response => {
  467. if (!response || !response.version) {
  468. // Communication failure - assume that no endpoint exists
  469. logger.warn(
  470. 'Extension not installed?: ', chrome.runtime.lastError);
  471. callback(false, false);
  472. return;
  473. }
  474. // Check installed extension version
  475. const extVersion = response.version;
  476. logger.log(`Extension version is: ${extVersion}`);
  477. const updateRequired
  478. = isUpdateRequired(
  479. options.desktopSharingChromeMinExtVersion,
  480. extVersion);
  481. callback(!updateRequired, updateRequired);
  482. }
  483. );
  484. }
  485. /**
  486. *
  487. * @param options
  488. * @param streamCallback
  489. * @param failCallback
  490. */
  491. function doGetStreamFromExtension(options, streamCallback, failCallback) {
  492. const {
  493. desktopSharingChromeSources,
  494. desktopSharingChromeExtId,
  495. gumOptions
  496. } = options;
  497. // Sends 'getStream' msg to the extension.
  498. // Extension id must be defined in the config.
  499. chrome.runtime.sendMessage(
  500. desktopSharingChromeExtId,
  501. {
  502. getStream: true,
  503. sources: desktopSharingChromeSources
  504. },
  505. response => {
  506. if (!response) {
  507. // possibly re-wraping error message to make code consistent
  508. const lastError = chrome.runtime.lastError;
  509. failCallback(lastError instanceof Error
  510. ? lastError
  511. : new JitsiTrackError(
  512. JitsiTrackErrors.CHROME_EXTENSION_GENERIC_ERROR,
  513. lastError));
  514. return;
  515. }
  516. logger.log('Response from extension: ', response);
  517. onGetStreamResponse(
  518. {
  519. response,
  520. gumOptions
  521. },
  522. streamCallback,
  523. failCallback
  524. );
  525. }
  526. );
  527. }
  528. /**
  529. * Initializes <link rel=chrome-webstore-item /> with extension id set in
  530. * config.js to support inline installs. Host site must be selected as main
  531. * website of published extension.
  532. * @param options supports "desktopSharingChromeExtId"
  533. */
  534. function initInlineInstalls(options) {
  535. if ($('link[rel=chrome-webstore-item]').length === 0) {
  536. $('head').append('<link rel="chrome-webstore-item">');
  537. }
  538. $('link[rel=chrome-webstore-item]').attr('href',
  539. getWebStoreInstallUrl(options));
  540. }
  541. /**
  542. *
  543. * @param options
  544. *
  545. * @return {Promise} - a Promise resolved once the initialization process is
  546. * finished.
  547. */
  548. function initChromeExtension(options) {
  549. // Initialize Chrome extension inline installs
  550. initInlineInstalls(options);
  551. return new Promise(resolve => {
  552. // Check if extension is installed
  553. checkChromeExtInstalled((installed, updateRequired) => {
  554. chromeExtInstalled = installed;
  555. chromeExtUpdateRequired = updateRequired;
  556. logger.info(
  557. `Chrome extension installed: ${
  558. chromeExtInstalled} updateRequired: ${
  559. chromeExtUpdateRequired}`);
  560. resolve();
  561. }, options);
  562. });
  563. }
  564. /**
  565. * Checks "retries" times on every "waitInterval"ms whether the ext is alive.
  566. * @param {Object} options the options passed to ScreanObtainer.obtainStream
  567. * @param {int} waitInterval the number of ms between retries
  568. * @param {int} retries the number of retries
  569. * @returns {Promise} returns promise that will be resolved when the extension
  570. * is alive and rejected if the extension is not alive even after "retries"
  571. * checks
  572. */
  573. function waitForExtensionAfterInstall(options, waitInterval, retries) {
  574. if (retries === 0) {
  575. return Promise.reject();
  576. }
  577. return new Promise((resolve, reject) => {
  578. let currentRetries = retries;
  579. const interval = window.setInterval(() => {
  580. checkChromeExtInstalled(installed => {
  581. if (installed) {
  582. window.clearInterval(interval);
  583. resolve();
  584. } else {
  585. currentRetries--;
  586. if (currentRetries === 0) {
  587. reject();
  588. window.clearInterval(interval);
  589. }
  590. }
  591. }, options);
  592. }, waitInterval);
  593. });
  594. }
  595. /**
  596. * Handles response from external application / extension and calls GUM to
  597. * receive the desktop streams or reports error.
  598. * @param {object} options
  599. * @param {object} options.response
  600. * @param {string} options.response.streamId - the streamId for the desktop
  601. * stream.
  602. * @param {string} options.response.error - error to be reported.
  603. * @param {object} options.gumOptions - options passed to GUM.
  604. * @param {Function} onSuccess - callback for success.
  605. * @param {Function} onFailure - callback for failure.
  606. * @param {object} gumOptions - options passed to GUM.
  607. */
  608. function onGetStreamResponse(
  609. options = {
  610. response: {},
  611. gumOptions: {}
  612. },
  613. onSuccess,
  614. onFailure) {
  615. const { streamId, streamType, error } = options.response || {};
  616. if (streamId) {
  617. const gumOptions = {
  618. desktopStream: streamId,
  619. ...options.gumOptions
  620. };
  621. gumFunction([ 'desktop' ], gumOptions)
  622. .then(stream => onSuccess({
  623. stream,
  624. sourceId: streamId,
  625. sourceType: streamType
  626. }), onFailure);
  627. } else {
  628. // As noted in Chrome Desktop Capture API:
  629. // If user didn't select any source (i.e. canceled the prompt)
  630. // then the callback is called with an empty streamId.
  631. if (streamId === '') {
  632. onFailure(new JitsiTrackError(
  633. JitsiTrackErrors.CHROME_EXTENSION_USER_CANCELED));
  634. return;
  635. }
  636. onFailure(new JitsiTrackError(
  637. JitsiTrackErrors.CHROME_EXTENSION_GENERIC_ERROR,
  638. error));
  639. }
  640. }
  641. export default ScreenObtainer;