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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762
  1. /* global chrome, $, alert */
  2. import JitsiTrackError from '../../JitsiTrackError';
  3. import * as JitsiTrackErrors from '../../JitsiTrackErrors';
  4. import RTCBrowserType from './RTCBrowserType';
  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. /**
  19. * Whether the jidesha extension for firefox is installed for the domain on
  20. * which we are running. Null designates an unknown value.
  21. * @type {null}
  22. */
  23. let firefoxExtInstalled = null;
  24. /**
  25. * If set to true, detection of an installed firefox extension will be started
  26. * again the next time obtainScreenOnFirefox is called (e.g. next time the
  27. * user tries to enable screen sharing).
  28. */
  29. let reDetectFirefoxExtension = false;
  30. let gumFunction = null;
  31. /**
  32. * The error returned by chrome when trying to start inline installation from
  33. * popup.
  34. */
  35. const CHROME_EXTENSION_POPUP_ERROR
  36. = 'Inline installs can not be initiated from pop-up windows.';
  37. /**
  38. * The error returned by chrome when trying to start inline installation from
  39. * iframe.
  40. */
  41. const CHROME_EXTENSION_IFRAME_ERROR
  42. = 'Chrome Web Store installations can only be started by the top frame.';
  43. /**
  44. * The error returned by chrome when trying to start inline installation
  45. * not from the "main" whitelisted site.
  46. * @type {string}
  47. */
  48. const CHROME_EXTENSION_INLINE_ERROR
  49. = 'Installs can only be initiated by one of'
  50. + ' the Chrome Web Store item\'s verified sites.';
  51. /**
  52. * The error message returned by chrome when the extension is installed.
  53. */
  54. const CHROME_NO_EXTENSION_ERROR_MSG // eslint-disable-line no-unused-vars
  55. = 'Could not establish connection. Receiving end does not exist.';
  56. /**
  57. * The error message returned by chrome when the extension install action needs
  58. * to be initiated by a user gesture.
  59. * @type {string}
  60. */
  61. const CHROME_USER_GESTURE_REQ_ERROR
  62. = 'Chrome Web Store installations can only be initated by a user gesture.';
  63. /**
  64. * Handles obtaining a stream from a screen capture on different browsers.
  65. */
  66. const ScreenObtainer = {
  67. /**
  68. * If not <tt>null</tt> it means that the initialization process is still in
  69. * progress. It is used to make desktop stream request wait and continue
  70. * after it's done.
  71. * {@type Promise|null}
  72. */
  73. intChromeExtPromise: null,
  74. obtainStream: null,
  75. /**
  76. * Initializes the function used to obtain a screen capture
  77. * (this.obtainStream).
  78. *
  79. * @param {object} options
  80. * @param {boolean} [options.disableDesktopSharing]
  81. * @param {boolean} [options.desktopSharingChromeDisabled]
  82. * @param {boolean} [options.desktopSharingChromeExtId]
  83. * @param {boolean} [options.desktopSharingFirefoxDisabled]
  84. * @param {boolean} [options.desktopSharingFirefoxExtId] (deprecated)
  85. * @param {Function} gum GUM method
  86. */
  87. init(options = {
  88. disableDesktopSharing: false,
  89. desktopSharingChromeDisabled: false,
  90. desktopSharingChromeExtId: null,
  91. desktopSharingFirefoxDisabled: false,
  92. desktopSharingFirefoxExtId: null
  93. }, gum) {
  94. // eslint-disable-next-line no-param-reassign
  95. this.options = 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 (RTCBrowserType.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 (RTCBrowserType.isElectron()) {
  148. return this.obtainScreenOnElectron;
  149. } else if (RTCBrowserType.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 (RTCBrowserType.isChrome()) {
  167. if (RTCBrowserType.getChromeVersion() < 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 (RTCBrowserType.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. initFirefoxExtensionDetection(options);
  191. return this.obtainScreenOnFirefox;
  192. }
  193. logger.warn(
  194. 'Screen sharing not supported by the current browser: ',
  195. RTCBrowserType.getBrowserType(),
  196. RTCBrowserType.getBrowserName());
  197. return null;
  198. },
  199. /**
  200. * Checks whether obtaining a screen capture is supported in the current
  201. * environment.
  202. * @returns {boolean}
  203. */
  204. isSupported() {
  205. return this.obtainStream !== null;
  206. },
  207. /**
  208. * Obtains a screen capture stream on Firefox.
  209. * @param callback
  210. * @param errorCallback
  211. */
  212. obtainScreenOnFirefox(options, callback, errorCallback) {
  213. let extensionRequired = false;
  214. const { desktopSharingFirefoxMaxVersionExtRequired } = this.options;
  215. if (desktopSharingFirefoxMaxVersionExtRequired === -1
  216. || (desktopSharingFirefoxMaxVersionExtRequired >= 0
  217. && RTCBrowserType.getFirefoxVersion()
  218. <= desktopSharingFirefoxMaxVersionExtRequired)) {
  219. extensionRequired = true;
  220. logger.log(
  221. `Jidesha extension required on firefox version ${
  222. RTCBrowserType.getFirefoxVersion()}`);
  223. }
  224. if (!extensionRequired || firefoxExtInstalled === true) {
  225. obtainWebRTCScreen(options, callback, errorCallback);
  226. return;
  227. }
  228. if (reDetectFirefoxExtension) {
  229. reDetectFirefoxExtension = false;
  230. initFirefoxExtensionDetection(this.options);
  231. }
  232. // Give it some (more) time to initialize, and assume lack of
  233. // extension if it hasn't.
  234. if (firefoxExtInstalled === null) {
  235. window.setTimeout(
  236. () => {
  237. if (firefoxExtInstalled === null) {
  238. firefoxExtInstalled = false;
  239. }
  240. this.obtainScreenOnFirefox(callback, errorCallback);
  241. },
  242. 300);
  243. logger.log(
  244. 'Waiting for detection of jidesha on firefox to finish.');
  245. return;
  246. }
  247. // We need an extension and it isn't installed.
  248. // Make sure we check for the extension when the user clicks again.
  249. firefoxExtInstalled = null;
  250. reDetectFirefoxExtension = true;
  251. // Make sure desktopsharing knows that we failed, so that it doesn't get
  252. // stuck in 'switching' mode.
  253. errorCallback(
  254. new JitsiTrackError(JitsiTrackErrors.FIREFOX_EXTENSION_NEEDED));
  255. },
  256. /**
  257. * Obtains a screen capture stream on Electron.
  258. *
  259. * @param {Object} [options] - Screen sharing options.
  260. * @param {Array<string>} [options.desktopSharingSources] - Array with the
  261. * sources that have to be displayed in the desktop picker window ('screen',
  262. * 'window', etc.).
  263. * @param onSuccess - Success callback.
  264. * @param onFailure - Failure callback.
  265. */
  266. obtainScreenOnElectron(options = {}, onSuccess, onFailure) {
  267. if (window.JitsiMeetScreenObtainer
  268. && window.JitsiMeetScreenObtainer.openDesktopPicker) {
  269. window.JitsiMeetScreenObtainer.openDesktopPicker(
  270. {
  271. desktopSharingSources:
  272. options.desktopSharingSources
  273. || this.options.desktopSharingChromeSources
  274. },
  275. (streamId, streamType) =>
  276. onGetStreamResponse(
  277. {
  278. streamId,
  279. streamType
  280. },
  281. onSuccess,
  282. onFailure
  283. ),
  284. err => onFailure(new JitsiTrackError(
  285. JitsiTrackErrors.ELECTRON_DESKTOP_PICKER_ERROR,
  286. err
  287. ))
  288. );
  289. } else {
  290. onFailure(new JitsiTrackError(
  291. JitsiTrackErrors.ELECTRON_DESKTOP_PICKER_NOT_FOUND));
  292. }
  293. },
  294. /**
  295. * Asks Chrome extension to call chooseDesktopMedia and gets chrome
  296. * 'desktop' stream for returned stream token.
  297. */
  298. obtainScreenFromExtension(options, streamCallback, failCallback) {
  299. if (this.intChromeExtPromise !== null) {
  300. this.intChromeExtPromise.then(() => {
  301. this.obtainScreenFromExtension(
  302. options, streamCallback, failCallback);
  303. });
  304. return;
  305. }
  306. const {
  307. desktopSharingChromeExtId,
  308. desktopSharingChromeSources
  309. } = this.options;
  310. const gumOptions = {
  311. desktopSharingChromeExtId,
  312. desktopSharingChromeSources:
  313. options.desktopSharingSources
  314. || desktopSharingChromeSources
  315. };
  316. if (chromeExtInstalled) {
  317. doGetStreamFromExtension(
  318. gumOptions,
  319. streamCallback,
  320. failCallback);
  321. } else {
  322. if (chromeExtUpdateRequired) {
  323. /* eslint-disable no-alert */
  324. alert(
  325. 'Jitsi Desktop Streamer requires update. '
  326. + 'Changes will take effect after next Chrome restart.');
  327. /* eslint-enable no-alert */
  328. }
  329. try {
  330. chrome.webstore.install(
  331. getWebStoreInstallUrl(this.options),
  332. arg => {
  333. logger.log('Extension installed successfully', arg);
  334. chromeExtInstalled = true;
  335. // We need to give a moment to the endpoint to become
  336. // available.
  337. waitForExtensionAfterInstall(this.options, 200, 10)
  338. .then(() => {
  339. doGetStreamFromExtension(
  340. gumOptions,
  341. streamCallback,
  342. failCallback);
  343. })
  344. .catch(() => {
  345. this.handleExtensionInstallationError(options,
  346. streamCallback, failCallback);
  347. });
  348. },
  349. this.handleExtensionInstallationError.bind(this,
  350. options, streamCallback, failCallback)
  351. );
  352. } catch (e) {
  353. this.handleExtensionInstallationError(options, streamCallback,
  354. failCallback, e);
  355. }
  356. }
  357. },
  358. /* eslint-disable max-params */
  359. handleExtensionInstallationError(options, streamCallback, failCallback, e) {
  360. const webStoreInstallUrl = getWebStoreInstallUrl(this.options);
  361. if ((CHROME_EXTENSION_POPUP_ERROR === e
  362. || CHROME_EXTENSION_IFRAME_ERROR === e
  363. || CHROME_EXTENSION_INLINE_ERROR === e)
  364. && options.interval > 0
  365. && typeof options.checkAgain === 'function'
  366. && typeof options.listener === 'function') {
  367. options.listener('waitingForExtension', webStoreInstallUrl);
  368. this.checkForChromeExtensionOnInterval(options, streamCallback,
  369. failCallback, e);
  370. return;
  371. }
  372. const msg
  373. = `Failed to install the extension from ${webStoreInstallUrl}`;
  374. logger.log(msg, e);
  375. const error
  376. = e === CHROME_USER_GESTURE_REQ_ERROR
  377. ? JitsiTrackErrors.CHROME_EXTENSION_USER_GESTURE_REQUIRED
  378. : JitsiTrackErrors.CHROME_EXTENSION_INSTALLATION_ERROR;
  379. failCallback(new JitsiTrackError(error, msg));
  380. },
  381. /* eslint-enable max-params */
  382. checkForChromeExtensionOnInterval(options, streamCallback, failCallback) {
  383. if (options.checkAgain() === false) {
  384. failCallback(new JitsiTrackError(
  385. JitsiTrackErrors.CHROME_EXTENSION_INSTALLATION_ERROR));
  386. return;
  387. }
  388. waitForExtensionAfterInstall(this.options, options.interval, 1)
  389. .then(() => {
  390. chromeExtInstalled = true;
  391. options.listener('extensionFound');
  392. this.obtainScreenFromExtension(options,
  393. streamCallback, failCallback);
  394. })
  395. .catch(() => {
  396. this.checkForChromeExtensionOnInterval(options,
  397. streamCallback, failCallback);
  398. });
  399. }
  400. };
  401. /**
  402. * Obtains a desktop stream using getUserMedia.
  403. * For this to work on Chrome, the
  404. * 'chrome://flags/#enable-usermedia-screen-capture' flag must be enabled.
  405. *
  406. * On firefox, the document's domain must be white-listed in the
  407. * 'media.getusermedia.screensharing.allowed_domains' preference in
  408. * 'about:config'.
  409. */
  410. function obtainWebRTCScreen(options, streamCallback, failCallback) {
  411. gumFunction(
  412. [ 'screen' ],
  413. stream => streamCallback({ stream }),
  414. failCallback
  415. );
  416. }
  417. /**
  418. * Constructs inline install URL for Chrome desktop streaming extension.
  419. * The 'chromeExtensionId' must be defined in options parameter.
  420. * @param options supports "desktopSharingChromeExtId"
  421. * @returns {string}
  422. */
  423. function getWebStoreInstallUrl(options) {
  424. return (
  425. `https://chrome.google.com/webstore/detail/${
  426. options.desktopSharingChromeExtId}`);
  427. }
  428. /**
  429. * Checks whether an update of the Chrome extension is required.
  430. * @param minVersion minimal required version
  431. * @param extVersion current extension version
  432. * @returns {boolean}
  433. */
  434. function isUpdateRequired(minVersion, extVersion) {
  435. try {
  436. const s1 = minVersion.split('.');
  437. const s2 = extVersion.split('.');
  438. const len = Math.max(s1.length, s2.length);
  439. for (let i = 0; i < len; i++) {
  440. let n1 = 0,
  441. n2 = 0;
  442. if (i < s1.length) {
  443. n1 = parseInt(s1[i], 10);
  444. }
  445. if (i < s2.length) {
  446. n2 = parseInt(s2[i], 10);
  447. }
  448. if (isNaN(n1) || isNaN(n2)) {
  449. return true;
  450. } else if (n1 !== n2) {
  451. return n1 > n2;
  452. }
  453. }
  454. // will happen if both versions have identical numbers in
  455. // their components (even if one of them is longer, has more components)
  456. return false;
  457. } catch (e) {
  458. GlobalOnErrorHandler.callErrorHandler(e);
  459. logger.error('Failed to parse extension version', e);
  460. return true;
  461. }
  462. }
  463. /**
  464. *
  465. * @param callback
  466. * @param options
  467. */
  468. function checkChromeExtInstalled(callback, options) {
  469. if (typeof chrome === 'undefined' || !chrome || !chrome.runtime) {
  470. // No API, so no extension for sure
  471. callback(false, false);
  472. return;
  473. }
  474. chrome.runtime.sendMessage(
  475. options.desktopSharingChromeExtId,
  476. { getVersion: true },
  477. response => {
  478. if (!response || !response.version) {
  479. // Communication failure - assume that no endpoint exists
  480. logger.warn(
  481. 'Extension not installed?: ', chrome.runtime.lastError);
  482. callback(false, false);
  483. return;
  484. }
  485. // Check installed extension version
  486. const extVersion = response.version;
  487. logger.log(`Extension version is: ${extVersion}`);
  488. const updateRequired
  489. = isUpdateRequired(
  490. options.desktopSharingChromeMinExtVersion,
  491. extVersion);
  492. callback(!updateRequired, updateRequired);
  493. }
  494. );
  495. }
  496. /**
  497. *
  498. * @param options
  499. * @param streamCallback
  500. * @param failCallback
  501. */
  502. function doGetStreamFromExtension(options, streamCallback, failCallback) {
  503. // Sends 'getStream' msg to the extension.
  504. // Extension id must be defined in the config.
  505. chrome.runtime.sendMessage(
  506. options.desktopSharingChromeExtId,
  507. {
  508. getStream: true,
  509. sources: options.desktopSharingChromeSources
  510. },
  511. response => {
  512. if (!response) {
  513. // possibly re-wraping error message to make code consistent
  514. const lastError = chrome.runtime.lastError;
  515. failCallback(lastError instanceof Error
  516. ? lastError
  517. : new JitsiTrackError(
  518. JitsiTrackErrors.CHROME_EXTENSION_GENERIC_ERROR,
  519. lastError));
  520. return;
  521. }
  522. logger.log('Response from extension: ', response);
  523. onGetStreamResponse(response, streamCallback, failCallback);
  524. }
  525. );
  526. }
  527. /**
  528. * Initializes <link rel=chrome-webstore-item /> with extension id set in
  529. * config.js to support inline installs. Host site must be selected as main
  530. * website of published extension.
  531. * @param options supports "desktopSharingChromeExtId"
  532. */
  533. function initInlineInstalls(options) {
  534. if ($('link[rel=chrome-webstore-item]').length === 0) {
  535. $('head').append('<link rel="chrome-webstore-item">');
  536. }
  537. $('link[rel=chrome-webstore-item]').attr('href',
  538. getWebStoreInstallUrl(options));
  539. }
  540. /**
  541. *
  542. * @param options
  543. *
  544. * @return {Promise} - a Promise resolved once the initialization process is
  545. * finished.
  546. */
  547. function initChromeExtension(options) {
  548. // Initialize Chrome extension inline installs
  549. initInlineInstalls(options);
  550. return new Promise(resolve => {
  551. // Check if extension is installed
  552. checkChromeExtInstalled((installed, updateRequired) => {
  553. chromeExtInstalled = installed;
  554. chromeExtUpdateRequired = updateRequired;
  555. logger.info(
  556. `Chrome extension installed: ${chromeExtInstalled
  557. } updateRequired: ${chromeExtUpdateRequired}`);
  558. resolve();
  559. }, options);
  560. });
  561. }
  562. /**
  563. * Checks "retries" times on every "waitInterval"ms whether the ext is alive.
  564. * @param {Object} options the options passed to ScreanObtainer.obtainStream
  565. * @param {int} waitInterval the number of ms between retries
  566. * @param {int} retries the number of retries
  567. * @returns {Promise} returns promise that will be resolved when the extension
  568. * is alive and rejected if the extension is not alive even after "retries"
  569. * checks
  570. */
  571. function waitForExtensionAfterInstall(options, waitInterval, retries) {
  572. if (retries === 0) {
  573. return Promise.reject();
  574. }
  575. return new Promise((resolve, reject) => {
  576. let currentRetries = retries;
  577. const interval = window.setInterval(() => {
  578. checkChromeExtInstalled(installed => {
  579. if (installed) {
  580. window.clearInterval(interval);
  581. resolve();
  582. } else {
  583. currentRetries--;
  584. if (currentRetries === 0) {
  585. reject();
  586. window.clearInterval(interval);
  587. }
  588. }
  589. }, options);
  590. }, waitInterval);
  591. });
  592. }
  593. /**
  594. * Handles response from external application / extension and calls GUM to
  595. * receive the desktop streams or reports error.
  596. * @param {object} response
  597. * @param {string} response.streamId - the streamId for the desktop stream
  598. * @param {string} response.error - error to be reported.
  599. * @param {Function} onSuccess - callback for success.
  600. * @param {Function} onFailure - callback for failure.
  601. */
  602. function onGetStreamResponse(
  603. { streamId, streamType, error },
  604. onSuccess,
  605. onFailure) {
  606. if (streamId) {
  607. gumFunction(
  608. [ 'desktop' ],
  609. stream => onSuccess({
  610. stream,
  611. sourceId: streamId,
  612. sourceType: streamType
  613. }),
  614. onFailure,
  615. { desktopStream: streamId });
  616. } else {
  617. // As noted in Chrome Desktop Capture API:
  618. // If user didn't select any source (i.e. canceled the prompt)
  619. // then the callback is called with an empty streamId.
  620. if (streamId === '') {
  621. onFailure(new JitsiTrackError(
  622. JitsiTrackErrors.CHROME_EXTENSION_USER_CANCELED));
  623. return;
  624. }
  625. onFailure(new JitsiTrackError(
  626. JitsiTrackErrors.CHROME_EXTENSION_GENERIC_ERROR,
  627. error));
  628. }
  629. }
  630. /**
  631. * Starts the detection of an installed jidesha extension for firefox.
  632. * @param options supports "desktopSharingFirefoxDisabled",
  633. * "desktopSharingFirefoxExtId"
  634. */
  635. function initFirefoxExtensionDetection(options) {
  636. if (options.desktopSharingFirefoxDisabled) {
  637. return;
  638. }
  639. if (firefoxExtInstalled === false || firefoxExtInstalled === true) {
  640. return;
  641. }
  642. if (!options.desktopSharingFirefoxExtId) {
  643. firefoxExtInstalled = false;
  644. return;
  645. }
  646. const img = document.createElement('img');
  647. img.onload = () => {
  648. logger.log('Detected firefox screen sharing extension.');
  649. firefoxExtInstalled = true;
  650. };
  651. img.onerror = () => {
  652. logger.log('Detected lack of firefox screen sharing extension.');
  653. firefoxExtInstalled = false;
  654. };
  655. // The jidesha extension exposes an empty image file under the url:
  656. // "chrome://EXT_ID/content/DOMAIN.png"
  657. // Where EXT_ID is the ID of the extension with "@" replaced by ".", and
  658. // DOMAIN is a domain whitelisted by the extension.
  659. const src
  660. = `chrome://${options.desktopSharingFirefoxExtId.replace('@', '.')
  661. }/content/${document.location.hostname}.png`;
  662. img.setAttribute('src', src);
  663. }
  664. export default ScreenObtainer;