Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

ScreenObtainer.js 24KB

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