您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

ScreenObtainer.js 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  1. /* global chrome, $, alert */
  2. var GlobalOnErrorHandler = require("../util/GlobalOnErrorHandler");
  3. var logger = require("jitsi-meet-logger").getLogger(__filename);
  4. var RTCBrowserType = require("./RTCBrowserType");
  5. import JitsiTrackError from "../../JitsiTrackError";
  6. import * as JitsiTrackErrors from "../../JitsiTrackErrors";
  7. /**
  8. * Indicates whether the Chrome desktop sharing extension is installed.
  9. * @type {boolean}
  10. */
  11. var chromeExtInstalled = false;
  12. /**
  13. * Indicates whether an update of the Chrome desktop sharing extension is
  14. * required.
  15. * @type {boolean}
  16. */
  17. var 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. var 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. var reDetectFirefoxExtension = false;
  30. var 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 message returned by chrome when the extension is installed.
  45. */
  46. const CHROME_NO_EXTENSION_ERROR_MSG // eslint-disable-line no-unused-vars
  47. = "Could not establish connection. Receiving end does not exist.";
  48. /**
  49. * Handles obtaining a stream from a screen capture on different browsers.
  50. */
  51. var ScreenObtainer = {
  52. obtainStream: null,
  53. /**
  54. * Initializes the function used to obtain a screen capture
  55. * (this.obtainStream).
  56. *
  57. * @param options {object}
  58. * @param gum {Function} GUM method
  59. */
  60. init(options, gum) {
  61. var obtainDesktopStream = null;
  62. this.options = options = options || {};
  63. gumFunction = gum;
  64. if (RTCBrowserType.isFirefox())
  65. initFirefoxExtensionDetection(options);
  66. if (RTCBrowserType.isNWJS()) {
  67. obtainDesktopStream = (options, onSuccess, onFailure) => {
  68. window.JitsiMeetNW.obtainDesktopStream (
  69. onSuccess,
  70. (error, constraints) => {
  71. var jitsiError;
  72. // FIXME:
  73. // This is very very durty fix for recognising that the
  74. // user have clicked the cancel button from the Desktop
  75. // sharing pick window. The proper solution would be to
  76. // detect this in the NWJS application by checking the
  77. // streamId === "". Even better solution would be to
  78. // stop calling GUM from the NWJS app and just pass the
  79. // streamId to lib-jitsi-meet. This way the desktop
  80. // sharing implementation for NWJS and chrome extension
  81. // will be the same and lib-jitsi-meet will be able to
  82. // control the constraints, check the streamId, etc.
  83. //
  84. // I cannot find documentation about "InvalidStateError"
  85. // but this is what we are receiving from GUM when the
  86. // streamId for the desktop sharing is "".
  87. if (error && error.name == "InvalidStateError") {
  88. jitsiError = new JitsiTrackError(
  89. JitsiTrackErrors.CHROME_EXTENSION_USER_CANCELED
  90. );
  91. } else {
  92. jitsiError = new JitsiTrackError(
  93. error, constraints, ["desktop"]);
  94. }
  95. (typeof(onFailure) === "function") &&
  96. onFailure(jitsiError);
  97. });
  98. };
  99. } else if(RTCBrowserType.isElectron()) {
  100. obtainDesktopStream = (options, onSuccess, onFailure) =>
  101. window.JitsiMeetElectron.obtainDesktopStream (
  102. streamId =>
  103. onGetStreamResponse({streamId}, onSuccess, onFailure),
  104. err => onFailure(new JitsiTrackError(
  105. JitsiTrackErrors.CHROME_EXTENSION_GENERIC_ERROR, err))
  106. );
  107. } else if (RTCBrowserType.isTemasysPluginUsed()) {
  108. // XXX Don't require Temasys unless it's to be used because it
  109. // doesn't run on React Native, for example.
  110. const AdapterJS = require("./adapter.screenshare");
  111. if (!AdapterJS.WebRTCPlugin.plugin.HasScreensharingFeature) {
  112. logger.info("Screensharing not supported by this plugin " +
  113. "version");
  114. } else if(!AdapterJS.WebRTCPlugin.plugin.isScreensharingAvailable) {
  115. logger.info(
  116. "Screensharing not available with Temasys plugin on" +
  117. " this site");
  118. } else {
  119. obtainDesktopStream = obtainWebRTCScreen;
  120. logger.info("Using Temasys plugin for desktop sharing");
  121. }
  122. } else if (RTCBrowserType.isChrome()) {
  123. if (options.desktopSharingChromeDisabled ||
  124. options.desktopSharingChromeMethod === false ||
  125. !options.desktopSharingChromeExtId) {
  126. // TODO: desktopSharingChromeMethod is deprecated, remove.
  127. obtainDesktopStream = null;
  128. } else if (RTCBrowserType.getChromeVersion() >= 34) {
  129. obtainDesktopStream =
  130. this.obtainScreenFromExtension;
  131. logger.info("Using Chrome extension for desktop sharing");
  132. initChromeExtension(options);
  133. } else {
  134. logger.info("Chrome extension not supported until ver 34");
  135. }
  136. } else if (RTCBrowserType.isFirefox()) {
  137. if (options.desktopSharingFirefoxDisabled) {
  138. obtainDesktopStream = null;
  139. } else if (window.location.protocol === "http:"){
  140. logger.log("Screen sharing is not supported over HTTP. " +
  141. "Use of HTTPS is required.");
  142. obtainDesktopStream = null;
  143. } else {
  144. obtainDesktopStream = this.obtainScreenOnFirefox;
  145. }
  146. }
  147. if (!obtainDesktopStream) {
  148. logger.info("Desktop sharing disabled");
  149. }
  150. this.obtainStream = obtainDesktopStream;
  151. },
  152. /**
  153. * Checks whether obtaining a screen capture is supported in the current
  154. * environment.
  155. * @returns {boolean}
  156. */
  157. isSupported() {
  158. return !!this.obtainStream;
  159. },
  160. /**
  161. * Obtains a screen capture stream on Firefox.
  162. * @param callback
  163. * @param errorCallback
  164. */
  165. obtainScreenOnFirefox(options, callback, errorCallback) {
  166. var extensionRequired = false;
  167. if (this.options.desktopSharingFirefoxMaxVersionExtRequired === -1 ||
  168. (this.options.desktopSharingFirefoxMaxVersionExtRequired >= 0 &&
  169. RTCBrowserType.getFirefoxVersion() <=
  170. this.options.desktopSharingFirefoxMaxVersionExtRequired)) {
  171. extensionRequired = true;
  172. logger.log("Jidesha extension required on firefox version " +
  173. RTCBrowserType.getFirefoxVersion());
  174. }
  175. if (!extensionRequired || firefoxExtInstalled === true) {
  176. obtainWebRTCScreen(options, callback, errorCallback);
  177. return;
  178. }
  179. if (reDetectFirefoxExtension) {
  180. reDetectFirefoxExtension = false;
  181. initFirefoxExtensionDetection(this.options);
  182. }
  183. // Give it some (more) time to initialize, and assume lack of
  184. // extension if it hasn't.
  185. if (firefoxExtInstalled === null) {
  186. window.setTimeout(
  187. () => {
  188. if (firefoxExtInstalled === null)
  189. firefoxExtInstalled = false;
  190. this.obtainScreenOnFirefox(callback, errorCallback);
  191. },
  192. 300);
  193. logger.log("Waiting for detection of jidesha on firefox to " +
  194. "finish.");
  195. return;
  196. }
  197. // We need an extension and it isn't installed.
  198. // Make sure we check for the extension when the user clicks again.
  199. firefoxExtInstalled = null;
  200. reDetectFirefoxExtension = true;
  201. // Make sure desktopsharing knows that we failed, so that it doesn't get
  202. // stuck in 'switching' mode.
  203. errorCallback(
  204. new JitsiTrackError(JitsiTrackErrors.FIREFOX_EXTENSION_NEEDED));
  205. },
  206. /**
  207. * Asks Chrome extension to call chooseDesktopMedia and gets chrome
  208. * 'desktop' stream for returned stream token.
  209. */
  210. obtainScreenFromExtension(options, streamCallback, failCallback) {
  211. if (chromeExtInstalled) {
  212. doGetStreamFromExtension(this.options, streamCallback,
  213. failCallback);
  214. } else {
  215. if (chromeExtUpdateRequired) {
  216. alert(
  217. 'Jitsi Desktop Streamer requires update. ' +
  218. 'Changes will take effect after next Chrome restart.');
  219. }
  220. try {
  221. chrome.webstore.install(
  222. getWebStoreInstallUrl(this.options),
  223. arg => {
  224. logger.log("Extension installed successfully", arg);
  225. chromeExtInstalled = true;
  226. // We need to give a moment to the endpoint to become
  227. // available.
  228. waitForExtensionAfterInstall(this.options, 200, 10)
  229. .then(() => {
  230. doGetStreamFromExtension(this.options,
  231. streamCallback, failCallback);
  232. }).catch(() => {
  233. this.handleExtensionInstallationError(options,
  234. streamCallback, failCallback);
  235. });
  236. },
  237. this.handleExtensionInstallationError.bind(this,
  238. options, streamCallback, failCallback)
  239. );
  240. } catch(e) {
  241. this.handleExtensionInstallationError(options, streamCallback,
  242. failCallback, e);
  243. }
  244. }
  245. },
  246. handleExtensionInstallationError(options, streamCallback, failCallback, e) {
  247. const webStoreInstallUrl = getWebStoreInstallUrl(this.options);
  248. if ((CHROME_EXTENSION_POPUP_ERROR === e
  249. || CHROME_EXTENSION_IFRAME_ERROR === e)
  250. && options.interval > 0
  251. && typeof(options.checkAgain) === "function"
  252. && typeof(options.listener) === "function") {
  253. options.listener("waitingForExtension", webStoreInstallUrl);
  254. this.checkForChromeExtensionOnInterval(options, streamCallback,
  255. failCallback, e);
  256. return;
  257. }
  258. const msg
  259. = "Failed to install the extension from " + webStoreInstallUrl;
  260. logger.log(msg, e);
  261. failCallback(new JitsiTrackError(
  262. JitsiTrackErrors.CHROME_EXTENSION_INSTALLATION_ERROR,
  263. msg));
  264. },
  265. checkForChromeExtensionOnInterval(options, streamCallback, failCallback) {
  266. if (options.checkAgain() === false) {
  267. failCallback(new JitsiTrackError(
  268. JitsiTrackErrors.CHROME_EXTENSION_INSTALLATION_ERROR));
  269. return;
  270. }
  271. waitForExtensionAfterInstall(this.options, options.interval, 1)
  272. .then(() => {
  273. chromeExtInstalled = true;
  274. options.listener("extensionFound");
  275. this.obtainScreenFromExtension(options,
  276. streamCallback, failCallback);
  277. }).catch(() => {
  278. this.checkForChromeExtensionOnInterval(options,
  279. streamCallback, failCallback);
  280. });
  281. }
  282. };
  283. /**
  284. * Obtains a desktop stream using getUserMedia.
  285. * For this to work on Chrome, the
  286. * 'chrome://flags/#enable-usermedia-screen-capture' flag must be enabled.
  287. *
  288. * On firefox, the document's domain must be white-listed in the
  289. * 'media.getusermedia.screensharing.allowed_domains' preference in
  290. * 'about:config'.
  291. */
  292. function obtainWebRTCScreen(options, streamCallback, failCallback) {
  293. gumFunction(['screen'], streamCallback, failCallback);
  294. }
  295. /**
  296. * Constructs inline install URL for Chrome desktop streaming extension.
  297. * The 'chromeExtensionId' must be defined in options parameter.
  298. * @param options supports "desktopSharingChromeExtId"
  299. * @returns {string}
  300. */
  301. function getWebStoreInstallUrl(options)
  302. {
  303. return "https://chrome.google.com/webstore/detail/" +
  304. options.desktopSharingChromeExtId;
  305. }
  306. /**
  307. * Checks whether an update of the Chrome extension is required.
  308. * @param minVersion minimal required version
  309. * @param extVersion current extension version
  310. * @returns {boolean}
  311. */
  312. function isUpdateRequired(minVersion, extVersion) {
  313. try {
  314. var s1 = minVersion.split('.');
  315. var s2 = extVersion.split('.');
  316. var len = Math.max(s1.length, s2.length);
  317. for (var i = 0; i < len; i++) {
  318. var n1 = 0,
  319. n2 = 0;
  320. if (i < s1.length)
  321. n1 = parseInt(s1[i]);
  322. if (i < s2.length)
  323. n2 = parseInt(s2[i]);
  324. if (isNaN(n1) || isNaN(n2)) {
  325. return true;
  326. } else if (n1 !== n2) {
  327. return n1 > n2;
  328. }
  329. }
  330. // will happen if both versions have identical numbers in
  331. // their components (even if one of them is longer, has more components)
  332. return false;
  333. }
  334. catch (e) {
  335. GlobalOnErrorHandler.callErrorHandler(e);
  336. logger.error("Failed to parse extension version", e);
  337. return true;
  338. }
  339. }
  340. function checkChromeExtInstalled(callback, options) {
  341. if (typeof chrome === "undefined" || !chrome || !chrome.runtime) {
  342. // No API, so no extension for sure
  343. callback(false, false);
  344. return;
  345. }
  346. chrome.runtime.sendMessage(
  347. options.desktopSharingChromeExtId,
  348. { getVersion: true },
  349. response => {
  350. if (!response || !response.version) {
  351. // Communication failure - assume that no endpoint exists
  352. logger.warn(
  353. "Extension not installed?: ", chrome.runtime.lastError);
  354. callback(false, false);
  355. return;
  356. }
  357. // Check installed extension version
  358. var extVersion = response.version;
  359. logger.log('Extension version is: ' + extVersion);
  360. var updateRequired
  361. = isUpdateRequired(
  362. options.desktopSharingChromeMinExtVersion,
  363. extVersion);
  364. callback(!updateRequired, updateRequired);
  365. }
  366. );
  367. }
  368. function doGetStreamFromExtension(options, streamCallback, failCallback) {
  369. // Sends 'getStream' msg to the extension.
  370. // Extension id must be defined in the config.
  371. chrome.runtime.sendMessage(
  372. options.desktopSharingChromeExtId,
  373. {
  374. getStream: true,
  375. sources: options.desktopSharingChromeSources
  376. },
  377. response => {
  378. if (!response) {
  379. // possibly re-wraping error message to make code consistent
  380. var lastError = chrome.runtime.lastError;
  381. failCallback(lastError instanceof Error
  382. ? lastError
  383. : new JitsiTrackError(
  384. JitsiTrackErrors.CHROME_EXTENSION_GENERIC_ERROR,
  385. lastError));
  386. return;
  387. }
  388. logger.log("Response from extension: ", response);
  389. onGetStreamResponse(response, streamCallback, failCallback);
  390. }
  391. );
  392. }
  393. /**
  394. * Initializes <link rel=chrome-webstore-item /> with extension id set in
  395. * config.js to support inline installs. Host site must be selected as main
  396. * website of published extension.
  397. * @param options supports "desktopSharingChromeExtId"
  398. */
  399. function initInlineInstalls(options)
  400. {
  401. if($("link[rel=chrome-webstore-item]").length === 0) {
  402. $("head").append("<link rel=\"chrome-webstore-item\">");
  403. }
  404. $("link[rel=chrome-webstore-item]").attr("href",
  405. getWebStoreInstallUrl(options));
  406. }
  407. function initChromeExtension(options) {
  408. // Initialize Chrome extension inline installs
  409. initInlineInstalls(options);
  410. // Check if extension is installed
  411. checkChromeExtInstalled((installed, updateRequired) => {
  412. chromeExtInstalled = installed;
  413. chromeExtUpdateRequired = updateRequired;
  414. logger.info(
  415. "Chrome extension installed: " + chromeExtInstalled +
  416. " updateRequired: " + chromeExtUpdateRequired);
  417. }, options);
  418. }
  419. /**
  420. * Checks "retries" times on every "waitInterval"ms whether the ext is alive.
  421. * @param {Object} options the options passed to ScreanObtainer.obtainStream
  422. * @param {int} waitInterval the number of ms between retries
  423. * @param {int} retries the number of retries
  424. * @returns {Promise} returns promise that will be resolved when the extension
  425. * is alive and rejected if the extension is not alive even after "retries"
  426. * checks
  427. */
  428. function waitForExtensionAfterInstall(options, waitInterval, retries) {
  429. if(retries === 0) {
  430. return Promise.reject();
  431. }
  432. return new Promise((resolve, reject) => {
  433. let currentRetries = retries;
  434. let interval = window.setInterval(() => {
  435. checkChromeExtInstalled( (installed) => {
  436. if(installed) {
  437. window.clearInterval(interval);
  438. resolve();
  439. } else {
  440. currentRetries--;
  441. if(currentRetries === 0) {
  442. reject();
  443. window.clearInterval(interval);
  444. }
  445. }
  446. }, options);
  447. }, waitInterval);
  448. });
  449. }
  450. /**
  451. * Handles response from external application / extension and calls GUM to
  452. * receive the desktop streams or reports error.
  453. * @param {object} response
  454. * @param {string} response.streamId - the streamId for the desktop stream
  455. * @param {string} response.error - error to be reported.
  456. * @param {Function} onSuccess - callback for success.
  457. * @param {Function} onFailure - callback for failure.
  458. */
  459. function onGetStreamResponse(response, onSuccess, onFailure) {
  460. if (response.streamId) {
  461. gumFunction(
  462. ['desktop'],
  463. stream => onSuccess(stream),
  464. onFailure,
  465. { desktopStream: response.streamId });
  466. } else {
  467. // As noted in Chrome Desktop Capture API:
  468. // If user didn't select any source (i.e. canceled the prompt)
  469. // then the callback is called with an empty streamId.
  470. if(response.streamId === "")
  471. {
  472. onFailure(new JitsiTrackError(
  473. JitsiTrackErrors.CHROME_EXTENSION_USER_CANCELED));
  474. return;
  475. }
  476. onFailure(new JitsiTrackError(
  477. JitsiTrackErrors.CHROME_EXTENSION_GENERIC_ERROR,
  478. response.error));
  479. }
  480. }
  481. /**
  482. * Starts the detection of an installed jidesha extension for firefox.
  483. * @param options supports "desktopSharingFirefoxDisabled",
  484. * "desktopSharingFirefoxExtId"
  485. */
  486. function initFirefoxExtensionDetection(options) {
  487. if (options.desktopSharingFirefoxDisabled) {
  488. return;
  489. }
  490. if (firefoxExtInstalled === false || firefoxExtInstalled === true)
  491. return;
  492. if (!options.desktopSharingFirefoxExtId) {
  493. firefoxExtInstalled = false;
  494. return;
  495. }
  496. var img = document.createElement('img');
  497. img.onload = () => {
  498. logger.log("Detected firefox screen sharing extension.");
  499. firefoxExtInstalled = true;
  500. };
  501. img.onerror = () => {
  502. logger.log("Detected lack of firefox screen sharing extension.");
  503. firefoxExtInstalled = false;
  504. };
  505. // The jidesha extension exposes an empty image file under the url:
  506. // "chrome://EXT_ID/content/DOMAIN.png"
  507. // Where EXT_ID is the ID of the extension with "@" replaced by ".", and
  508. // DOMAIN is a domain whitelisted by the extension.
  509. var src = "chrome://" +
  510. (options.desktopSharingFirefoxExtId.replace('@', '.')) +
  511. "/content/" + document.location.hostname + ".png";
  512. img.setAttribute('src', src);
  513. }
  514. module.exports = ScreenObtainer;