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

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