diff --git a/modules/desktopsharing/ScreenObtainer.js b/modules/desktopsharing/ScreenObtainer.js new file mode 100644 index 000000000..9b3e1ae3d --- /dev/null +++ b/modules/desktopsharing/ScreenObtainer.js @@ -0,0 +1,407 @@ +/* global config, APP, chrome, $, alert */ +/* jshint -W003 */ +var RTCBrowserType = require("../RTC/RTCBrowserType"); +var AdapterJS = require("../RTC/adapter.screenshare"); +var DesktopSharingEventTypes + = require("../../service/desktopsharing/DesktopSharingEventTypes"); + +/** + * Indicates whether the Chrome desktop sharing extension is installed. + * @type {boolean} + */ +var chromeExtInstalled = false; + +/** + * Indicates whether an update of the Chrome desktop sharing extension is + * required. + * @type {boolean} + */ +var chromeExtUpdateRequired = false; + +/** + * Whether the jidesha extension for firefox is installed for the domain on + * which we are running. Null designates an unknown value. + * @type {null} + */ +var firefoxExtInstalled = null; + +/** + * If set to true, detection of an installed firefox extension will be started + * again the next time obtainScreenOnFirefox is called (e.g. next time the + * user tries to enable screen sharing). + */ +var reDetectFirefoxExtension = false; + +/** + * Handles obtaining a stream from a screen capture on different browsers. + */ +function ScreenObtainer(){ +} + +/** + * The EventEmitter to use to emit events. + * @type {null} + */ +ScreenObtainer.prototype.eventEmitter = null; + +/** + * Initializes the function used to obtain a screen capture (this.obtainStream). + * + * If the browser is Chrome, it uses the value of + * 'config.desktopSharingChromeMethod' (or 'config.desktopSharing') to * decide + * whether to use the a Chrome extension (if the value is 'ext'), use the + * "screen" media source (if the value is 'webrtc'), or disable screen capture + * (if the value is other). + * Note that for the "screen" media source to work the + * 'chrome://flags/#enable-usermedia-screen-capture' flag must be set. + */ +ScreenObtainer.prototype.init = function(eventEmitter) { + this.eventEmitter = eventEmitter; + var obtainDesktopStream = null; + + if (RTCBrowserType.isFirefox()) + initFirefoxExtensionDetection(); + + // TODO remove this, config.desktopSharing is deprecated. + var chromeMethod = + (config.desktopSharingChromeMethod || config.desktopSharing); + + if (RTCBrowserType.isTemasysPluginUsed()) { + if (!AdapterJS.WebRTCPlugin.plugin.HasScreensharingFeature) { + console.info("Screensharing not supported by this plugin version"); + } else if (!AdapterJS.WebRTCPlugin.plugin.isScreensharingAvailable) { + console.info( + "Screensharing not available with Temasys plugin on this site"); + } else { + obtainDesktopStream = obtainWebRTCScreen; + console.info("Using Temasys plugin for desktop sharing"); + } + } else if (RTCBrowserType.isChrome()) { + if (chromeMethod == "ext") { + if (RTCBrowserType.getChromeVersion() >= 34) { + obtainDesktopStream = obtainScreenFromExtension; + console.info("Using Chrome extension for desktop sharing"); + initChromeExtension(); + } else { + console.info("Chrome extension not supported until ver 34"); + } + } else if (chromeMethod == "webrtc") { + obtainDesktopStream = obtainWebRTCScreen; + console.info("Using Chrome WebRTC for desktop sharing"); + } + } else if (RTCBrowserType.isFirefox()) { + if (config.desktopSharingFirefoxDisabled) { + obtainDesktopStream = null; + } else if (window.location.protocol === "http:"){ + console.log("Screen sharing is not supported over HTTP. Use of " + + "HTTPS is required."); + obtainDesktopStream = null; + } else { + obtainDesktopStream = this.obtainScreenOnFirefox; + } + + } + + if (!obtainDesktopStream) { + console.info("Desktop sharing disabled"); + } + + ScreenObtainer.prototype.obtainStream = obtainDesktopStream; +}; + +ScreenObtainer.prototype.obtainStream = null; + +/** + * Checks whether obtaining a screen capture is supported in the current + * environment. + * @returns {boolean} + */ +ScreenObtainer.prototype.isSupported = function() { + return !!this.obtainStream; +}; + +/** + * Obtains a desktop stream using getUserMedia. + * For this to work on Chrome, the + * 'chrome://flags/#enable-usermedia-screen-capture' flag must be enabled. + * + * On firefox, the document's domain must be white-listed in the + * 'media.getusermedia.screensharing.allowed_domains' preference in + * 'about:config'. + */ +function obtainWebRTCScreen(streamCallback, failCallback) { + APP.RTC.getUserMediaWithConstraints( + ['screen'], + streamCallback, + failCallback + ); +} + +/** + * Constructs inline install URL for Chrome desktop streaming extension. + * The 'chromeExtensionId' must be defined in config.js. + * @returns {string} + */ +function getWebStoreInstallUrl() +{ + //TODO remove chromeExtensionId (deprecated) + return "https://chrome.google.com/webstore/detail/" + + (config.desktopSharingChromeExtId || config.chromeExtensionId); +} + +/** + * Checks whether an update of the Chrome extension is required. + * @param minVersion minimal required version + * @param extVersion current extension version + * @returns {boolean} + */ +function isUpdateRequired(minVersion, extVersion) { + try { + var s1 = minVersion.split('.'); + var s2 = extVersion.split('.'); + + var len = Math.max(s1.length, s2.length); + for (var i = 0; i < len; i++) { + var n1 = 0, + n2 = 0; + + if (i < s1.length) + n1 = parseInt(s1[i]); + if (i < s2.length) + n2 = parseInt(s2[i]); + + if (isNaN(n1) || isNaN(n2)) { + return true; + } else if (n1 !== n2) { + return n1 > n2; + } + } + + // will happen if both versions have identical numbers in + // their components (even if one of them is longer, has more components) + return false; + } + catch (e) { + console.error("Failed to parse extension version", e); + APP.UI.messageHandler.showError("dialog.error", + "dialog.detectext"); + return true; + } +} + +function checkChromeExtInstalled(callback) { + if (!chrome || !chrome.runtime) { + // No API, so no extension for sure + callback(false, false); + return; + } + chrome.runtime.sendMessage( + //TODO: remove chromeExtensionId (deprecated) + (config.desktopSharingChromeExtId || config.chromeExtensionId), + { getVersion: true }, + function (response) { + if (!response || !response.version) { + // Communication failure - assume that no endpoint exists + console.warn( + "Extension not installed?: ", chrome.runtime.lastError); + callback(false, false); + return; + } + // Check installed extension version + var extVersion = response.version; + console.log('Extension version is: ' + extVersion); + //TODO: remove minChromeExtVersion (deprecated) + var updateRequired + = isUpdateRequired( + (config.desktopSharingChromeMinExtVersion || + config.minChromeExtVersion), + extVersion); + callback(!updateRequired, updateRequired); + } + ); +} + +function doGetStreamFromExtension(streamCallback, failCallback) { + // Sends 'getStream' msg to the extension. + // Extension id must be defined in the config. + chrome.runtime.sendMessage( + //TODO: remove chromeExtensionId (deprecated) + (config.desktopSharingChromeExtId || config.chromeExtensionId), + { + getStream: true, + //TODO: remove desktopSharingSources (deprecated). + sources: (config.desktopSharingChromeSources || + config.desktopSharingSources) + }, + function (response) { + if (!response) { + failCallback(chrome.runtime.lastError); + return; + } + console.log("Response from extension: " + response); + if (response.streamId) { + APP.RTC.getUserMediaWithConstraints( + ['desktop'], + function (stream) { + streamCallback(stream); + }, + failCallback, + null, null, null, + response.streamId); + } else { + failCallback("Extension failed to get the stream"); + } + } + ); +} + +/** + * Asks Chrome extension to call chooseDesktopMedia and gets chrome 'desktop' + * stream for returned stream token. + */ +function obtainScreenFromExtension(streamCallback, failCallback) { + if (chromeExtInstalled) { + doGetStreamFromExtension(streamCallback, failCallback); + } else { + if (chromeExtUpdateRequired) { + alert( + 'Jitsi Desktop Streamer requires update. ' + + 'Changes will take effect after next Chrome restart.'); + } + + chrome.webstore.install( + getWebStoreInstallUrl(), + function (arg) { + console.log("Extension installed successfully", arg); + chromeExtInstalled = true; + // We need to give a moment for the endpoint to become available + window.setTimeout(function () { + doGetStreamFromExtension(streamCallback, failCallback); + }, 500); + }, + function (arg) { + console.log("Failed to install the extension", arg); + failCallback(arg); + APP.UI.messageHandler.showError("dialog.error", + "dialog.failtoinstall"); + } + ); + } +} + +/** + * Initializes with extension id set in + * config.js to support inline installs. Host site must be selected as main + * website of published extension. + */ +function initInlineInstalls() +{ + $("link[rel=chrome-webstore-item]").attr("href", getWebStoreInstallUrl()); +} + +function initChromeExtension() { + // Initialize Chrome extension inline installs + initInlineInstalls(); + // Check if extension is installed + checkChromeExtInstalled(function (installed, updateRequired) { + chromeExtInstalled = installed; + chromeExtUpdateRequired = updateRequired; + console.info( + "Chrome extension installed: " + chromeExtInstalled + + " updateRequired: " + chromeExtUpdateRequired); + }); +} + +/** + * Obtains a screen capture stream on Firefox. + * @param callback + * @param errorCallback + */ +ScreenObtainer.prototype.obtainScreenOnFirefox = + function (callback, errorCallback) { + var self = this; + var extensionRequired = false; + if (config.desktopSharingFirefoxMaxVersionExtRequired === -1 || + (config.desktopSharingFirefoxMaxVersionExtRequired >= 0 && + RTCBrowserType.getFirefoxVersion() <= + config.desktopSharingFirefoxMaxVersionExtRequired)) { + extensionRequired = true; + console.log("Jidesha extension required on firefox version " + + RTCBrowserType.getFirefoxVersion()); + } + + if (!extensionRequired || firefoxExtInstalled === true) { + obtainWebRTCScreen(callback, errorCallback); + return; + } + + if (reDetectFirefoxExtension) { + reDetectFirefoxExtension = false; + initFirefoxExtensionDetection(); + } + + // Give it some (more) time to initialize, and assume lack of extension if + // it hasn't. + if (firefoxExtInstalled === null) { + window.setTimeout( + function() { + if (firefoxExtInstalled === null) + firefoxExtInstalled = false; + self.obtainScreenOnFirefox(callback, errorCallback); + }, + 300 + ); + console.log("Waiting for detection of jidesha on firefox to finish."); + return; + } + + // We need an extension and it isn't installed. + + // Make sure we check for the extension when the user clicks again. + firefoxExtInstalled = null; + reDetectFirefoxExtension = true; + + // Prompt the user to install the extension + this.eventEmitter.emit(DesktopSharingEventTypes.FIREFOX_EXTENSION_NEEDED, + config.desktopSharingFirefoxExtensionURL); + + // Make sure desktopsharing knows that we failed, so that it doesn't get + // stuck in 'switching' mode. + errorCallback('Firefox extension required.'); +}; + +/** + * Starts the detection of an installed jidesha extension for firefox. + */ +function initFirefoxExtensionDetection() { + if (config.desktopSharingFirefoxDisabled) { + return; + } + if (firefoxExtInstalled === false || firefoxExtInstalled === true) + return; + if (!config.desktopSharingFirefoxExtId) { + firefoxExtInstalled = false; + return; + } + + var img = document.createElement('img'); + img.onload = function(){ + console.log("Detected firefox screen sharing extension."); + firefoxExtInstalled = true; + }; + img.onerror = function(){ + console.log("Detected lack of firefox screen sharing extension."); + firefoxExtInstalled = false; + }; + + // The jidesha extension exposes an empty image file under the url: + // "chrome://EXT_ID/content/DOMAIN.png" + // Where EXT_ID is the ID of the extension with "@" replaced by ".", and + // DOMAIN is a domain whitelisted by the extension. + var src = "chrome://" + + (config.desktopSharingFirefoxExtId.replace('@', '.')) + + "/content/" + document.location.hostname + ".png"; + img.setAttribute('src', src); +} + +module.exports = ScreenObtainer; diff --git a/modules/desktopsharing/desktopsharing.js b/modules/desktopsharing/desktopsharing.js index 1c4cecf7b..4c91180cd 100644 --- a/modules/desktopsharing/desktopsharing.js +++ b/modules/desktopsharing/desktopsharing.js @@ -1,10 +1,17 @@ -/* global $, alert, APP, changeLocalVideo, chrome, config, getConferenceHandler, - getUserMediaWithConstraints */ +/* global APP, config */ +var EventEmitter = require("events"); +var DesktopSharingEventTypes + = require("../../service/desktopsharing/DesktopSharingEventTypes"); +var RTCBrowserType = require("../RTC/RTCBrowserType"); +var RTCEvents = require("../../service/RTC/RTCEvents"); +var ScreenObtainer = require("./ScreenObtainer"); + /** - * Indicates that desktop stream is currently in use(for toggle purpose). + * Indicates that desktop stream is currently in use (for toggle purpose). * @type {boolean} */ var isUsingScreenStream = false; + /** * Indicates that switch stream operation is in progress and prevent from * triggering new events. @@ -13,276 +20,12 @@ var isUsingScreenStream = false; var switchInProgress = false; /** - * Method used to get screen sharing stream. - * - * @type {function (stream_callback, failure_callback} + * Used to obtain the screen sharing stream from the browser. */ -var obtainDesktopStream = null; - -/** - * Indicates whether desktop sharing extension is installed. - * @type {boolean} - */ -var extInstalled = false; - -/** - * Indicates whether update of desktop sharing extension is required. - * @type {boolean} - */ -var extUpdateRequired = false; - - -var logger = require("jitsi-meet-logger").getLogger(__filename); -var AdapterJS = require("../RTC/adapter.screenshare"); - -var EventEmitter = require("events"); +var screenObtainer = new ScreenObtainer(); var eventEmitter = new EventEmitter(); -var DesktopSharingEventTypes - = require("../../service/desktopsharing/DesktopSharingEventTypes"); - -var RTCBrowserType = require("../RTC/RTCBrowserType"); - -var RTCEvents = require("../../service/RTC/RTCEvents"); - -/** - * Method obtains desktop stream from WebRTC 'screen' source. - * Flag 'chrome://flags/#enable-usermedia-screen-capture' must be enabled. - */ -function obtainWebRTCScreen(streamCallback, failCallback) { - APP.RTC.getUserMediaWithConstraints( - ['screen'], - streamCallback, - failCallback - ); -} - -/** - * Constructs inline install URL for Chrome desktop streaming extension. - * The 'chromeExtensionId' must be defined in config.js. - * @returns {string} - */ -function getWebStoreInstallUrl() -{ - return "https://chrome.google.com/webstore/detail/" + - config.chromeExtensionId; -} - -/** - * Checks whether extension update is required. - * @param minVersion minimal required version - * @param extVersion current extension version - * @returns {boolean} - */ -function isUpdateRequired(minVersion, extVersion) -{ - try - { - var s1 = minVersion.split('.'); - var s2 = extVersion.split('.'); - - var len = Math.max(s1.length, s2.length); - for (var i = 0; i < len; i++) - { - var n1 = 0, - n2 = 0; - - if (i < s1.length) - n1 = parseInt(s1[i]); - if (i < s2.length) - n2 = parseInt(s2[i]); - - if (isNaN(n1) || isNaN(n2)) - { - return true; - } - else if (n1 !== n2) - { - return n1 > n2; - } - } - - // will happen if boths version has identical numbers in - // their components (even if one of them is longer, has more components) - return false; - } - catch (e) - { - logger.error("Failed to parse extension version", e); - APP.UI.messageHandler.showError("dialog.error", - "dialog.detectext"); - return true; - } -} - -function checkChromeExtInstalled(callback) { - if (!chrome.runtime) { - // No API, so no extension for sure - callback(false, false); - return; - } - chrome.runtime.sendMessage( - config.chromeExtensionId, - { getVersion: true }, - function (response) { - if (!response || !response.version) { - // Communication failure - assume that no endpoint exists - logger.warn( - "Extension not installed?: ", chrome.runtime.lastError); - callback(false, false); - return; - } - // Check installed extension version - var extVersion = response.version; - logger.log('Extension version is: ' + extVersion); - var updateRequired - = isUpdateRequired(config.minChromeExtVersion, extVersion); - callback(!updateRequired, updateRequired); - } - ); -} - -function doGetStreamFromExtension(streamCallback, failCallback) { - // Sends 'getStream' msg to the extension. - // Extension id must be defined in the config. - chrome.runtime.sendMessage( - config.chromeExtensionId, - { getStream: true, sources: config.desktopSharingSources }, - function (response) { - if (!response) { - failCallback(chrome.runtime.lastError); - return; - } - logger.log("Response from extension: " + response); - if (response.streamId) { - APP.RTC.getUserMediaWithConstraints( - ['desktop'], - function (stream) { - streamCallback(stream); - }, - failCallback, - null, null, null, - response.streamId); - } else { - failCallback("Extension failed to get the stream"); - } - } - ); -} -/** - * Asks Chrome extension to call chooseDesktopMedia and gets chrome 'desktop' - * stream for returned stream token. - */ -function obtainScreenFromExtension(streamCallback, failCallback) { - if (extInstalled) { - doGetStreamFromExtension(streamCallback, failCallback); - } else { - if (extUpdateRequired) { - alert( - 'Jitsi Desktop Streamer requires update. ' + - 'Changes will take effect after next Chrome restart.'); - } - - chrome.webstore.install( - getWebStoreInstallUrl(), - function (arg) { - logger.log("Extension installed successfully", arg); - extInstalled = true; - // We need to give a moment for the endpoint to become available - window.setTimeout(function () { - doGetStreamFromExtension(streamCallback, failCallback); - }, 500); - }, - function (arg) { - logger.log("Failed to install the extension", arg); - failCallback(arg); - APP.UI.messageHandler.showError("dialog.error", - "dialog.failtoinstall"); - } - ); - } -} - -/** - * Call this method to toggle desktop sharing feature. - * @param method pass "ext" to use chrome extension for desktop capture(chrome - * extension required), pass "webrtc" to use WebRTC "screen" desktop - * source('chrome://flags/#enable-usermedia-screen-capture' must be - * enabled), pass any other string or nothing in order to disable this - * feature completely. - */ -function setDesktopSharing(method) { - - obtainDesktopStream = null; - - // When TemasysWebRTC plugin is used we always use getUserMedia, so we don't - // care about 'method' parameter - if (RTCBrowserType.isTemasysPluginUsed()) { - if (!AdapterJS.WebRTCPlugin.plugin.HasScreensharingFeature) { - logger.info("Screensharing not supported by this plugin version"); - } else if (!AdapterJS.WebRTCPlugin.plugin.isScreensharingAvailable) { - logger.info( - "Screensharing not available with Temasys plugin on this site"); - } else { - obtainDesktopStream = obtainWebRTCScreen; - logger.info("Using Temasys plugin for desktop sharing"); - } - } else if (RTCBrowserType.isChrome()) { - if (method == "ext") { - if (RTCBrowserType.getChromeVersion() >= 34) { - obtainDesktopStream = obtainScreenFromExtension; - logger.info("Using Chrome extension for desktop sharing"); - initChromeExtension(); - } else { - logger.info("Chrome extension not supported until ver 34"); - } - } else if (method == "webrtc") { - obtainDesktopStream = obtainWebRTCScreen; - logger.info("Using Chrome WebRTC for desktop sharing"); - } - } - - if (!obtainDesktopStream) { - logger.info("Desktop sharing disabled"); - } -} - -/** - * Initializes with extension id set in - * config.js to support inline installs. Host site must be selected as main - * website of published extension. - */ -function initInlineInstalls() -{ - $("link[rel=chrome-webstore-item]").attr("href", getWebStoreInstallUrl()); -} - -function initChromeExtension() { - // Initialize Chrome extension inline installs - initInlineInstalls(); - // Check if extension is installed - checkChromeExtInstalled(function (installed, updateRequired) { - extInstalled = installed; - extUpdateRequired = updateRequired; - logger.info( - "Chrome extension installed: " + extInstalled + - " updateRequired: " + extUpdateRequired); - }); -} - -function getVideoStreamFailed(error) { - logger.error("Failed to obtain the stream to switch to", error); - switchInProgress = false; - isUsingScreenStream = false; - newStreamCreated(null); -} - -function getDesktopStreamFailed(error) { - logger.error("Failed to obtain the stream to switch to", error); - switchInProgress = false; -} - function streamSwitchDone() { switchInProgress = false; eventEmitter.emit( @@ -290,30 +33,29 @@ function streamSwitchDone() { isUsingScreenStream); } -function newStreamCreated(stream) -{ +function newStreamCreated(stream) { eventEmitter.emit(DesktopSharingEventTypes.NEW_STREAM_CREATED, stream, isUsingScreenStream, streamSwitchDone); } +function getVideoStreamFailed(error) { + console.error("Failed to obtain the stream to switch to", error); + switchInProgress = false; + isUsingScreenStream = false; + newStreamCreated(null); +} + +function getDesktopStreamFailed(error) { + console.error("Failed to obtain the stream to switch to", error); + switchInProgress = false; +} + function onEndedHandler(stream) { if (!switchInProgress && isUsingScreenStream) { APP.desktopsharing.toggleScreenSharing(); } - //FIXME: to be verified - if (stream.removeEventListener) { - stream.removeEventListener('ended', onEndedHandler); - } else { - stream.detachEvent('ended', onEndedHandler); - } -} -// Called when RTC finishes initialization -function onWebRtcReady() { - - setDesktopSharing(config.desktopSharing); - - eventEmitter.emit(DesktopSharingEventTypes.INIT); + APP.RTC.removeMediaStreamInactiveHandler(stream, onEndedHandler); } module.exports = { @@ -325,18 +67,24 @@ module.exports = { * @returns {boolean} true if desktop sharing feature is available * and enabled. */ - isDesktopSharingEnabled: function () { return !!obtainDesktopStream; }, + isDesktopSharingEnabled: function () { + return screenObtainer.isSupported(); + }, init: function () { - APP.RTC.addListener(RTCEvents.RTC_READY, onWebRtcReady); + // Called when RTC finishes initialization + APP.RTC.addListener(RTCEvents.RTC_READY, + function() { + screenObtainer.init(eventEmitter); + eventEmitter.emit(DesktopSharingEventTypes.INIT); + }); }, - addListener: function (listener, type) - { + addListener: function (type, listener) { eventEmitter.on(type, listener); }, - removeListener: function (listener, type) { + removeListener: function (type, listener) { eventEmitter.removeListener(type, listener); }, @@ -344,31 +92,25 @@ module.exports = { * Toggles screen sharing. */ toggleScreenSharing: function () { - if (switchInProgress || !obtainDesktopStream) { - logger.warn("Switch in progress or no method defined"); + if (switchInProgress) { + console.warn("Switch in progress."); + return; + } else if (!screenObtainer.isSupported()) { + console.warn("Cannot toggle screen sharing: not supported."); return; } switchInProgress = true; - if (!isUsingScreenStream) - { + if (!isUsingScreenStream) { // Switch to desktop stream - obtainDesktopStream( + screenObtainer.obtainStream( function (stream) { // We now use screen stream isUsingScreenStream = true; // Hook 'ended' event to restore camera // when screen stream stops - //FIXME: to be verified - if (stream.addEventListener) { - stream.addEventListener('ended', function () { - onEndedHandler(stream); - }); - } else { - stream.attachEvent('ended', function () { - onEndedHandler(stream); - }); - } + APP.RTC.addMediaStreamInactiveHandler( + stream, onEndedHandler); newStreamCreated(stream); }, getDesktopStreamFailed); @@ -381,9 +123,15 @@ module.exports = { isUsingScreenStream = false; newStreamCreated(stream); }, - getVideoStreamFailed, config.resolution || '360' + getVideoStreamFailed, + config.resolution || '360' ); } - } + }, + /* + * Exports the event emitter to allow use by ScreenObtainer. Not for outside + * use. + */ + eventEmitter: eventEmitter };