diff --git a/bigbluebutton-html5/client/main.html b/bigbluebutton-html5/client/main.html index 477638ae336c7159ba5778f4bd6bf64a4a1e924f..d9628be3ca15bfd8d470bb4250b123594f8df72d 100755 --- a/bigbluebutton-html5/client/main.html +++ b/bigbluebutton-html5/client/main.html @@ -67,7 +67,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. </script> <script src="compatibility/adapter.js?v=VERSION" language="javascript"></script> <script src="compatibility/sip.js?v=VERSION" language="javascript"></script> - <script src="compatibility/kurento-extension.js?v=VERSION" language="javascript"></script> <script src="compatibility/kurento-utils.js?v=VERSION" language="javascript"></script> </head> <body style="background-color: #06172A"> diff --git a/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js b/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js index 2f9ac06072b253b157bb9675bdfa35f6df5fee15..93ed27748b3a932196f575f0cd9c229dc7b8ee95 100755 --- a/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js +++ b/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js @@ -1,240 +1,259 @@ import Auth from '/imports/ui/services/auth'; -import BridgeService from './service'; -import { fetchWebRTCMappedStunTurnServers, getMappedFallbackStun } from '/imports/utils/fetchStunTurnServers'; -import playAndRetry from '/imports/utils/mediaElementPlayRetry'; import logger from '/imports/startup/client/logger'; +import BridgeService from './service'; +import ScreenshareBroker from '/imports/ui/services/bbb-webrtc-sfu/screenshare-broker'; +import { setSharingScreen } from '/imports/ui/components/screenshare/service'; const SFU_CONFIG = Meteor.settings.public.kurento; const SFU_URL = SFU_CONFIG.wsUrl; -const CHROME_DEFAULT_EXTENSION_KEY = SFU_CONFIG.chromeDefaultExtensionKey; -const CHROME_CUSTOM_EXTENSION_KEY = SFU_CONFIG.chromeExtensionKey; -const CHROME_SCREENSHARE_SOURCES = SFU_CONFIG.screenshare.chromeScreenshareSources; -const FIREFOX_SCREENSHARE_SOURCE = SFU_CONFIG.screenshare.firefoxScreenshareSource; + +const BRIDGE_NAME = 'kurento' const SCREENSHARE_VIDEO_TAG = 'screenshareVideo'; +const SEND_ROLE = 'send'; +const RECV_ROLE = 'recv'; -const CHROME_EXTENSION_KEY = CHROME_CUSTOM_EXTENSION_KEY === 'KEY' ? CHROME_DEFAULT_EXTENSION_KEY : CHROME_CUSTOM_EXTENSION_KEY; +const errorCodeMap = { + 1301: 1101, + 1302: 1102, + 1305: 1105, + 1307: 1108, // This should be 1107, but I'm preserving the existing locales - prlanzarin +} -const getUserId = () => Auth.userID; +const mapErrorCode = (error) => { + const { errorCode } = error; + const mappedErrorCode = errorCodeMap[errorCode]; + if (errorCode == null || mappedErrorCode == null) return error; + error.errorCode = mappedErrorCode; + return error; +} -const getMeetingId = () => Auth.meetingID; +export default class KurentoScreenshareBridge { + constructor() { + this.role; + this.broker; + this._gdmStream; + this.connectionAttempts = 0; + this.reconnecting = false; + this.reconnectionTimeout; + this.restartIntervalMs = BridgeService.BASE_MEDIA_TIMEOUT; + } -const getUsername = () => Auth.fullname; + get gdmStream() { + return this._gdmStream; + } -const getSessionToken = () => Auth.sessionToken; + set gdmStream(stream) { + this._gdmStream = stream; + } -export default class KurentoScreenshareBridge { - static normalizeError(error = {}) { - const errorMessage = error.name || error.message || error.reason || 'Unknown error'; - const errorCode = error.code || undefined; - const errorReason = error.reason || error.id || 'Undefined reason'; + outboundStreamReconnect() { + const currentRestartIntervalMs = this.restartIntervalMs; + const stream = this.gdmStream; + + logger.warn({ + logCode: 'screenshare_presenter_reconnect' + }, `Screenshare presenter session is reconnecting`); - return { errorMessage, errorCode, errorReason }; + this.stop(); + this.restartIntervalMs = BridgeService.getNextReconnectionInterval(currentRestartIntervalMs); + this.share(stream, this.onerror).then(() => { + this.clearReconnectionTimeout(); + }).catch(error => { + // Error handling is a no-op because it will be "handled" in handlePresenterFailure + logger.debug({ + logCode: 'screenshare_reconnect_failed', + extraInfo: { + errorMessage: error.errorMessage, + reconnecting: this.reconnecting, + role: this.role, + bridge: BRIDGE_NAME + }, + }, 'Screensharing reconnect failed'); + }); } - static handlePresenterFailure(error, started = false) { - const normalizedError = KurentoScreenshareBridge.normalizeError(error); - if (!started) { - logger.error({ - logCode: 'screenshare_presenter_error_failed_to_connect', - extraInfo: { ...normalizedError }, - }, `Screenshare presenter failed when trying to start due to ${normalizedError.errorMessage}`); - } else { - logger.error({ - logCode: 'screenshare_presenter_error_failed_after_success', - extraInfo: { ...normalizedError }, - }, `Screenshare presenter failed during working session due to ${normalizedError.errorMessage}`); - } - return normalizedError; + inboundStreamReconnect() { + const currentRestartIntervalMs = this.restartIntervalMs; + + logger.warn({ + logCode: 'screenshare_viewer_reconnect', + }, `Screenshare viewer session is reconnecting`); + // Cleanly stop everything before triggering a reconnect + this.stop(); + // Create new reconnect interval time + this.restartIntervalMs = BridgeService.getNextReconnectionInterval(currentRestartIntervalMs); + this.view(stream, this.onerror).then(() => { + this.clearReconnectionTimeout(); + }).catch(error => { + // Error handling is a no-op because it will be "handled" in handleViewerFailure + logger.debug({ + logCode: 'screenshare_reconnect_failed', + extraInfo: { + errorMessage: error.errorMessage, + reconnecting: this.reconnecting, + role: this.role, + bridge: BRIDGE_NAME + }, + }, 'Screensharing reconnect failed'); + }); } - static handleViewerFailure(error, started = false) { - const normalizedError = KurentoScreenshareBridge.normalizeError(error); - if (!started) { - logger.error({ - logCode: 'screenshare_viewer_error_failed_to_connect', - extraInfo: { ...normalizedError }, - }, `Screenshare viewer failed when trying to start due to ${normalizedError.errorMessage}`); - } else { - logger.error({ - logCode: 'screenshare_viewer_error_failed_after_success', - extraInfo: { ...normalizedError }, - }, `Screenshare viewer failed during working session due to ${normalizedError.errorMessage}`); + handleConnectionTimeoutExpiry() { + this.reconnecting = true; + + switch (this.role) { + case RECV_ROLE: + return this.inboundStreamReconnect(); + case SEND_ROLE: + return this.outboundStreamReconnect(); + default: + this.reconnecting = false; + logger.error({ + logCode: 'screenshare_invalid_role' + }, 'Screen sharing with invalid role, wont reconnect'); + break; } - return normalizedError; } - static playElement(screenshareMediaElement) { - const mediaTagPlayed = () => { - logger.info({ - logCode: 'screenshare_media_play_success', - }, 'Screenshare media played successfully'); - }; + maxConnectionAttemptsReached () { + return this.connectionAttempts > BridgeService.MAX_CONN_ATTEMPTS; + } - if (screenshareMediaElement.paused) { - // Tag isn't playing yet. Play it. - screenshareMediaElement.play() - .then(mediaTagPlayed) - .catch((error) => { - // NotAllowedError equals autoplay issues, fire autoplay handling event. - // This will be handled in the screenshare react component. - if (error.name === 'NotAllowedError') { - logger.error({ - logCode: 'screenshare_error_autoplay', - extraInfo: { errorName: error.name }, - }, 'Screenshare play failed due to autoplay error'); - const tagFailedEvent = new CustomEvent('screensharePlayFailed', - { detail: { mediaElement: screenshareMediaElement } }); - window.dispatchEvent(tagFailedEvent); - } else { - // Tag failed for reasons other than autoplay. Log the error and - // try playing again a few times until it works or fails for good - const played = playAndRetry(screenshareMediaElement); - if (!played) { - logger.error({ - logCode: 'screenshare_error_media_play_failed', - extraInfo: { errorName: error.name }, - }, `Screenshare media play failed due to ${error.name}`); - } else { - mediaTagPlayed(); - } - } - }); - } else { - // Media tag is already playing, so log a success. This is really a - // logging fallback for a case that shouldn't happen. But if it does - // (ie someone re-enables the autoPlay prop in the element), then it - // means the stream is playing properly and it'll be logged. - mediaTagPlayed(); + scheduleReconnect () { + if (this.reconnectionTimeout == null) { + this.reconnectionTimeout = setTimeout( + this.handleConnectionTimeoutExpiry.bind(this), + this.restartIntervalMs + ); } - }; + } + + clearReconnectionTimeout () { + this.reconnecting = false; + this.restartIntervalMs = BridgeService.BASE_MEDIA_TIMEOUT; - static screenshareElementLoadAndPlay(stream, element, muted) { - element.muted = muted; - element.pause(); - element.srcObject = stream; - KurentoScreenshareBridge.playElement(element); + if (this.reconnectionTimeout) { + clearTimeout(this.reconnectionTimeout); + this.reconnectionTimeout = null; + } } - kurentoViewLocalPreview() { - const screenshareMediaElement = document.getElementById(SCREENSHARE_VIDEO_TAG); - const { webRtcPeer } = window.kurentoManager.kurentoScreenshare; + handleViewerStart() { + const mediaElement = document.getElementById(SCREENSHARE_VIDEO_TAG); - if (webRtcPeer) { - const stream = webRtcPeer.getLocalStream(); - KurentoScreenshareBridge.screenshareElementLoadAndPlay(stream, screenshareMediaElement, true); + if (mediaElement && this.broker && this.broker.webRtcPeer) { + const stream = this.broker.webRtcPeer.getRemoteStream(); + BridgeService.screenshareLoadAndPlayMediaStream(stream, mediaElement, !this.broker.hasAudio); } + + this.clearReconnectionTimeout(); } - async kurentoViewScreen(hasAudio) { - const screenshareMediaElement = document.getElementById(SCREENSHARE_VIDEO_TAG); - let iceServers = []; - let started = false; - - try { - iceServers = await fetchWebRTCMappedStunTurnServers(getSessionToken()); - } catch (error) { - logger.error({ - logCode: 'screenshare_viewer_fetchstunturninfo_error', - extraInfo: { error } - }, 'Screenshare bridge failed to fetch STUN/TURN info, using default'); - iceServers = getMappedFallbackStun(); - } finally { - const options = { - wsUrl: Auth.authenticateURL(SFU_URL), - iceServers, - logger, - userName: getUsername(), - hasAudio, - }; - - const onFail = (error) => { - KurentoScreenshareBridge.handleViewerFailure(error, started); - }; - - // Callback for the kurento-extension.js script. It's called when the whole - // negotiation with SFU is successful. This will load the stream into the - // screenshare media element and play it manually. - const onSuccess = () => { - started = true; - const { webRtcPeer } = window.kurentoManager.kurentoVideo; - if (webRtcPeer) { - const stream = webRtcPeer.getRemoteStream(); - KurentoScreenshareBridge.screenshareElementLoadAndPlay( - stream, - screenshareMediaElement, - !hasAudio, - ); - } - }; - - window.kurentoWatchVideo( - SCREENSHARE_VIDEO_TAG, - BridgeService.getConferenceBridge(), - getUserId(), - getMeetingId(), - onFail, - onSuccess, - options, - hasAudio, - ); + handleBrokerFailure(error) { + mapErrorCode(error); + BridgeService.handleViewerFailure(error, this.broker.started); + // Screensharing was already successfully negotiated and error occurred during + // during call; schedule a reconnect + // If the session has not yet started, a reconnect should already be scheduled + if (this.broker.started) { + this.scheduleReconnect(); } } - kurentoExitVideo() { - window.kurentoExitVideo(); + async view(hasAudio = false) { + this.role = RECV_ROLE; + const iceServers = await BridgeService.getIceServers(Auth.sessionToken); + const options = { + iceServers, + userName: Auth.fullname, + hasAudio, + }; + + this.broker = new ScreenshareBroker( + Auth.authenticateURL(SFU_URL), + BridgeService.getConferenceBridge(), + Auth.userID, + Auth.meetingID, + this.role, + options, + ); + + this.broker.onstart = this.handleViewerStart.bind(this); + this.broker.onerror = this.handleBrokerFailure.bind(this); + this.broker.onstreamended = this.stop.bind(this); + return this.broker.view().finally(this.scheduleReconnect.bind(this)); } - async kurentoShareScreen(onFail, stream) { - let iceServers = []; - try { - iceServers = await fetchWebRTCMappedStunTurnServers(getSessionToken()); - } catch (error) { - logger.error({ logCode: 'screenshare_presenter_fetchstunturninfo_error' }, - - 'Screenshare bridge failed to fetch STUN/TURN info, using default'); - iceServers = getMappedFallbackStun(); - } finally { - const hasAudioTrack = stream.getAudioTracks().length >= 1; - const options = { - wsUrl: Auth.authenticateURL(SFU_URL), - chromeExtension: CHROME_EXTENSION_KEY, - chromeScreenshareSources: CHROME_SCREENSHARE_SOURCES, - firefoxScreenshareSource: FIREFOX_SCREENSHARE_SOURCE, - iceServers, - logger, - userName: getUsername(), - hasAudio: hasAudioTrack, - }; - - let started = false; - - const failureCallback = (error) => { - const normalizedError = KurentoScreenshareBridge.handlePresenterFailure(error, started); - onFail(normalizedError); - }; - - const successCallback = () => { - started = true; - logger.info({ - logCode: 'screenshare_presenter_start_success', - }, 'Screenshare presenter started succesfully'); - }; - - options.stream = stream || undefined; - - window.kurentoShareScreen( - SCREENSHARE_VIDEO_TAG, - BridgeService.getConferenceBridge(), - getUserId(), - getMeetingId(), - failureCallback, - successCallback, - options, - ); - } + handlePresenterStart() { + logger.info({ + logCode: 'screenshare_presenter_start_success', + }, 'Screenshare presenter started succesfully'); + this.clearReconnectionTimeout(); + this.reconnecting = false; + this.connectionAttempts = 0; } - kurentoExitScreenShare() { - window.kurentoExitScreenShare(); + async share(stream, onFailure) { + this.onerror = onFailure; + this.connectionAttempts += 1; + this.role = SEND_ROLE; + this.gdmStream = stream; + + const onerror = (error) => { + mapErrorCode(error); + const normalizedError = BridgeService.handlePresenterFailure(error, this.broker.started); + + // Gracious mid call reconnects aren't yet implemented, so stop it. + if (this.broker.started) { + return onFailure(normalizedError); + } + + // Otherwise, sharing attempts have a finite amount of attempts for it + // to work (configurable). If expired, error out. + if (this.maxConnectionAttemptsReached()) { + this.clearReconnectionTimeout(); + this.connectionAttempts = 0; + return onFailure({ + errorCode: 1120, + errorMessage: `MAX_CONNECTION_ATTEMPTS_REACHED`, + }); + } + }; + + const iceServers = await BridgeService.getIceServers(Auth.sessionToken); + const options = { + iceServers, + userName: Auth.fullname, + stream, + hasAudio: BridgeService.streamHasAudioTrack(stream), + }; + + this.broker = new ScreenshareBroker( + Auth.authenticateURL(SFU_URL), + BridgeService.getConferenceBridge(), + Auth.userID, + Auth.meetingID, + this.role, + options, + ); + + this.broker.onstart = this.handlePresenterStart.bind(this); + this.broker.onerror = onerror.bind(this); + this.broker.onstreamended = this.stop.bind(this); + return this.broker.share().finally(this.scheduleReconnect.bind(this)); + }; + + stop() { + if (this.broker) { + this.broker.stop(); + // Checks if this session is a sharer and if it's not reconnecting + // If that's the case, clear the local sharing state in screen sharing UI + // component tracker to be extra sure we won't have any client-side state + // inconsistency - prlanzarin + if (this.broker.role === SEND_ROLE && !this.reconnecting) setSharingScreen(false); + this.broker = null; + } + this.gdmStream = null; + this.clearReconnectionTimeout(); } } diff --git a/bigbluebutton-html5/imports/api/screenshare/client/bridge/service.js b/bigbluebutton-html5/imports/api/screenshare/client/bridge/service.js index e2565e434cbffcb6e33bb70e95bbddeec2b08b56..1183fd81047dc9fe273a22ee82d56ccfab4e8442 100644 --- a/bigbluebutton-html5/imports/api/screenshare/client/bridge/service.js +++ b/bigbluebutton-html5/imports/api/screenshare/client/bridge/service.js @@ -1,10 +1,20 @@ import Meetings from '/imports/api/meetings'; import logger from '/imports/startup/client/logger'; +import { fetchWebRTCMappedStunTurnServers, getMappedFallbackStun } from '/imports/utils/fetchStunTurnServers'; +import loadAndPlayMediaStream from '/imports/ui/services/bbb-webrtc-sfu/load-play'; const { constraints: GDM_CONSTRAINTS, + mediaTimeouts: MEDIA_TIMEOUTS, } = Meteor.settings.public.kurento.screenshare; +const { + baseTimeout: BASE_MEDIA_TIMEOUT, + maxTimeout: MAX_MEDIA_TIMEOUT, + maxConnectionAttempts: MAX_CONN_ATTEMPTS, + timeoutIncreaseFactor: TIMEOUT_INCREASE_FACTOR, +} = MEDIA_TIMEOUTS; + const hasDisplayMedia = (typeof navigator.getDisplayMedia === 'function' || (navigator.mediaDevices && typeof navigator.mediaDevices.getDisplayMedia === 'function')); @@ -12,8 +22,19 @@ const getConferenceBridge = () => Meetings.findOne().voiceProp.voiceConf; const getScreenStream = async () => { const gDMCallback = (stream) => { + // Some older Chromium variants choke on gDM when audio: true by NOT generating + // a promise rejection AND not generating a valid input screen stream, need to + // work around that manually for now - prlanzarin + if (stream == null) { + return Promise.reject({ + errorMessage: 'NotSupportedError', + errorName: 'NotSupportedError', + errorCode: 9, + }); + } + if (typeof stream.getVideoTracks === 'function' - && typeof constraints.video === 'object') { + && typeof constraints.video === 'object') { stream.getVideoTracks().forEach(track => { if (typeof track.applyConstraints === 'function') { track.applyConstraints(constraints.video).catch(error => { @@ -21,14 +42,14 @@ const getScreenStream = async () => { logCode: 'screenshare_videoconstraint_failed', extraInfo: { errorName: error.name, errorCode: error.code }, }, - 'Error applying screenshare video constraint'); + 'Error applying screenshare video constraint'); }); } }); } if (typeof stream.getAudioTracks === 'function' - && typeof constraints.audio === 'object') { + && typeof constraints.audio === 'object') { stream.getAudioTracks().forEach(track => { if (typeof track.applyConstraints === 'function') { track.applyConstraints(constraints.audio).catch(error => { @@ -46,11 +67,9 @@ const getScreenStream = async () => { const constraints = hasDisplayMedia ? GDM_CONSTRAINTS : null; - // getDisplayMedia isn't supported, generate no stream and let the legacy - // constraint fetcher work its way on kurento-extension.js - if (constraints == null) { - return Promise.resolve(); - } else { + if (hasDisplayMedia) { + // The double checks here is to detect whether gDM is in navigator or mediaDevices + // because it can be on either of them depending on the browser+version if (typeof navigator.getDisplayMedia === 'function') { return navigator.getDisplayMedia(constraints) .then(gDMCallback) @@ -59,7 +78,7 @@ const getScreenStream = async () => { logCode: 'screenshare_getdisplaymedia_failed', extraInfo: { errorName: error.name, errorCode: error.code }, }, 'getDisplayMedia call failed'); - return Promise.resolve(); + return Promise.reject({ errorCode: error.code, errorMessage: error.name || error.message }); }); } else if (navigator.mediaDevices && typeof navigator.mediaDevices.getDisplayMedia === 'function') { return navigator.mediaDevices.getDisplayMedia(constraints) @@ -69,15 +88,119 @@ const getScreenStream = async () => { logCode: 'screenshare_getdisplaymedia_failed', extraInfo: { errorName: error.name, errorCode: error.code }, }, 'getDisplayMedia call failed'); - return Promise.resolve(); + return Promise.reject({ errorCode: error.code, errorMessage: error.name || error.message }); }); } + } else { + // getDisplayMedia isn't supported, error its way out + return Promise.reject({ + errorMessage: 'NotSupportedError', + errorName: 'NotSupportedError', + errorCode: 9, + }); + } +} + +const normalizeError = (error = {}) => { + const errorMessage = error.errorMessage || error.name || error.message || error.reason || 'Unknown error'; + const errorCode = error.errorCode || error.code || undefined; + const errorReason = error.reason || error.id || 'Undefined reason'; + + return { errorMessage, errorCode, errorReason }; +} + +const handlePresenterFailure = (error, started = false) => { + const normalizedError = normalizeError(error); + if (!started) { + logger.error({ + logCode: 'screenshare_presenter_error_failed_to_connect', + extraInfo: { ...normalizedError }, + }, `Screenshare presenter failed when trying to start due to ${normalizedError.errorMessage}`); + } else { + logger.error({ + logCode: 'screenshare_presenter_error_failed_after_success', + extraInfo: { ...normalizedError }, + }, `Screenshare presenter failed during working session due to ${normalizedError.errorMessage}`); + } + return normalizedError; +} + +const handleViewerFailure = (error, started = false) => { + const normalizedError = normalizeError(error); + if (!started) { + logger.error({ + logCode: 'screenshare_viewer_error_failed_to_connect', + extraInfo: { ...normalizedError }, + }, `Screenshare viewer failed when trying to start due to ${normalizedError.errorMessage}`); + } else { + logger.error({ + logCode: 'screenshare_viewer_error_failed_after_success', + extraInfo: { ...normalizedError }, + }, `Screenshare viewer failed during working session due to ${normalizedError.errorMessage}`); } + return normalizedError; +} + +const getIceServers = (sessionToken) => { + return fetchWebRTCMappedStunTurnServers(sessionToken).catch(error => { + logger.error({ + logCode: 'screenshare_fetchstunturninfo_error', + extraInfo: { error } + }, 'Screenshare bridge failed to fetch STUN/TURN info'); + return getMappedFallbackStun(); + }); +} + +const getNextReconnectionInterval = (oldInterval) => { + return Math.min( + TIMEOUT_INCREASE_FACTOR * oldInterval, + MAX_MEDIA_TIMEOUT, + ); } +const streamHasAudioTrack = (stream) => { + return stream + && typeof stream.getAudioTracks === 'function' + && stream.getAudioTracks().length >= 1; +} + +const dispatchAutoplayHandlingEvent = (mediaElement) => { + const tagFailedEvent = new CustomEvent('screensharePlayFailed', + { detail: { mediaElement } }); + window.dispatchEvent(tagFailedEvent); +} + +const screenshareLoadAndPlayMediaStream = (stream, mediaElement, muted) => { + return loadAndPlayMediaStream(stream, mediaElement, muted).catch(error => { + // NotAllowedError equals autoplay issues, fire autoplay handling event. + // This will be handled in the screenshare react component. + if (error.name === 'NotAllowedError') { + logger.error({ + logCode: 'screenshare_error_autoplay', + extraInfo: { errorName: error.name }, + }, 'Screen share media play failed: autoplay error'); + dispatchAutoplayHandlingEvent(mediaElement); + } else { + const normalizedError = { + errorCode: 1104, + errorMessage: error.message || 'SCREENSHARE_PLAY_FAILED', + }; + throw normalizedError; + } + }); +} export default { hasDisplayMedia, getConferenceBridge, getScreenStream, + normalizeError, + handlePresenterFailure, + handleViewerFailure, + getIceServers, + getNextReconnectionInterval, + streamHasAudioTrack, + screenshareLoadAndPlayMediaStream, + BASE_MEDIA_TIMEOUT, + MAX_CONN_ATTEMPTS, }; diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx index 2708acbea7e346a8746a9c314d3a1244cd2e41f7..f287a25085eafa0c1be800de565dbc67eb438f8f 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx @@ -1,8 +1,8 @@ import React, { PureComponent } from 'react'; import cx from 'classnames'; import { styles } from './styles.scss'; -import DesktopShare from './desktop-share/component'; import ActionsDropdown from './actions-dropdown/component'; +import ScreenshareButtonContainer from '/imports/ui/components/actions-bar/screenshare/container'; import QuickPollDropdown from './quick-poll-dropdown/component'; import AudioControlsContainer from '../audio/audio-controls/container'; import JoinVideoOptionsContainer from '../video-provider/video-button/container'; @@ -13,11 +13,7 @@ class ActionsBar extends PureComponent { render() { const { amIPresenter, - handleShareScreen, - handleUnshareScreen, - isVideoBroadcasting, amIModerator, - screenSharingCheck, enableVideo, isLayoutSwapped, toggleSwapLayout, @@ -26,9 +22,7 @@ class ActionsBar extends PureComponent { currentSlidHasContent, parseCurrentSlideContent, isSharingVideo, - screenShareEndAlert, stopExternalVideoShare, - screenshareDataSavingSetting, isCaptionsAvailable, isMeteorConnected, isPollingEnabled, @@ -83,15 +77,9 @@ class ActionsBar extends PureComponent { <JoinVideoOptionsContainer /> ) : null} - <DesktopShare {...{ - handleShareScreen, - handleUnshareScreen, - isVideoBroadcasting, + <ScreenshareButtonContainer {...{ amIPresenter, - screenSharingCheck, - screenShareEndAlert, isMeteorConnected, - screenshareDataSavingSetting, }} /> </div> diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx index f75cf3d683bbec1cff4d92ddc7d4fc99f8ed7c89..70788aa6c1ed5780e9796978c780b9ed6d6843d4 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx @@ -11,12 +11,8 @@ import Service from './service'; import ExternalVideoService from '/imports/ui/components/external-video-player/service'; import CaptionsService from '/imports/ui/components/captions/service'; import { - shareScreen, - unshareScreen, isVideoBroadcasting, - screenShareEndAlert, - dataSavingSetting, -} from '../screenshare/service'; +} from '/imports/ui/components/screenshare/service'; import MediaService, { getSwapLayout, @@ -30,10 +26,6 @@ export default withTracker(() => ({ amIPresenter: Service.amIPresenter(), amIModerator: Service.amIModerator(), stopExternalVideoShare: ExternalVideoService.stopWatching, - handleShareScreen: onFail => shareScreen(onFail), - handleUnshareScreen: () => unshareScreen(), - isVideoBroadcasting: isVideoBroadcasting(), - screenSharingCheck: getFromUserSettings('bbb_enable_screen_sharing', Meteor.settings.public.kurento.enableScreensharing), enableVideo: getFromUserSettings('bbb_enable_video', Meteor.settings.public.kurento.enableVideo), isLayoutSwapped: getSwapLayout() && shouldEnableSwapLayout(), toggleSwapLayout: MediaService.toggleSwapLayout, @@ -41,8 +33,6 @@ export default withTracker(() => ({ currentSlidHasContent: PresentationService.currentSlidHasContent(), parseCurrentSlideContent: PresentationService.parseCurrentSlideContent, isSharingVideo: Service.isSharingVideo(), - screenShareEndAlert, - screenshareDataSavingSetting: dataSavingSetting(), isCaptionsAvailable: CaptionsService.isCaptionsAvailable(), isMeteorConnected: Meteor.status().connected, isPollingEnabled: POLLING_ENABLED, diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/desktop-share/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/screenshare/component.jsx similarity index 82% rename from bigbluebutton-html5/imports/ui/components/actions-bar/desktop-share/component.jsx rename to bigbluebutton-html5/imports/ui/components/actions-bar/screenshare/component.jsx index 09b26906588339d577d71b950335ed10e403a4a4..c5ef076ecb585a9db27eb6f00fbbfe58c44164a0 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/desktop-share/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/screenshare/component.jsx @@ -10,15 +10,20 @@ import Modal from '/imports/ui/components/modal/simple/component'; import { withModalMounter } from '../../modal/service'; import { styles } from '../styles'; import ScreenshareBridgeService from '/imports/api/screenshare/client/bridge/service'; +import { + shareScreen, + stop, + screenshareHasEnded, + screenShareEndAlert, + isVideoBroadcasting, +} from '/imports/ui/components/screenshare/service'; + const propTypes = { intl: intlShape.isRequired, + enabled: PropTypes.bool.isRequired, amIPresenter: PropTypes.bool.isRequired, - handleShareScreen: PropTypes.func.isRequired, - handleUnshareScreen: PropTypes.func.isRequired, isVideoBroadcasting: PropTypes.bool.isRequired, - screenSharingCheck: PropTypes.bool.isRequired, - screenShareEndAlert: PropTypes.func.isRequired, isMeteorConnected: PropTypes.bool.isRequired, screenshareDataSavingSetting: PropTypes.bool.isRequired, }; @@ -72,6 +77,10 @@ const intlMessages = defineMessages({ id: 'app.deskshare.iceConnectionStateError', description: 'Error message for ice connection state failure', }, + 1120: { + id: 'app.deskshare.mediaFlowTimeout', + description: 'Error message for screenshare media flow timeout', + }, 2000: { id: 'app.sfu.mediaServerConnectionError2000', description: 'Error message fired when the SFU cannot connect to the media server', @@ -117,14 +126,12 @@ const isMobileBrowser = (BROWSER_RESULTS ? BROWSER_RESULTS.mobile : false) : false); const IS_SAFARI = BROWSER_RESULTS.name === 'safari'; -const DesktopShare = ({ + +const ScreenshareButton = ({ intl, - handleShareScreen, - handleUnshareScreen, + enabled, isVideoBroadcasting, amIPresenter, - screenSharingCheck, - screenShareEndAlert, isMeteorConnected, screenshareDataSavingSetting, mountModal, @@ -136,7 +143,7 @@ const DesktopShare = ({ const error = errorCode || errorMessage || errorReason; // We have a properly mapped error for this. Exit screenshare and show a toast notification if (intlMessages[error]) { - window.kurentoExitScreenShare(); + screenshareHasEnded(); notify(intl.formatMessage(intlMessages[error]), 'error', 'desktop'); } else { // Unmapped error. Log it (so we can infer what's going on), close screenSharing @@ -147,7 +154,7 @@ const DesktopShare = ({ errorCode, errorMessage, errorReason, }, }, 'Default error handler for screenshare'); - window.kurentoExitScreenShare(); + screenshareHasEnded(); notify(intl.formatMessage(intlMessages.genericError), 'error', 'desktop'); } // Don't trigger the screen share end alert if presenter click to cancel on screen share dialog @@ -156,6 +163,23 @@ const DesktopShare = ({ } }; + const renderScreenshareUnavailableModal = () => { + return mountModal( + <Modal + overlayClassName={styles.overlay} + className={styles.modal} + onRequestClose={() => mountModal(null)} + hideBorder + contentLabel={intl.formatMessage(intlMessages.screenShareUnavailable)} + > + <h3 className={styles.title}> + {intl.formatMessage(intlMessages.screenShareUnavailable)} + </h3> + <p>{intl.formatMessage(intlMessages.screenShareNotSupported)}</p> + </Modal> + ) + }; + const screenshareLocked = screenshareDataSavingSetting ? intlMessages.desktopShareLabel : intlMessages.lockedDesktopShareLabel; @@ -165,7 +189,7 @@ const DesktopShare = ({ const vDescr = isVideoBroadcasting ? intlMessages.stopDesktopShareDesc : intlMessages.desktopShareDesc; - const shouldAllowScreensharing = screenSharingCheck + const shouldAllowScreensharing = enabled && !isMobileBrowser && amIPresenter; @@ -182,28 +206,20 @@ const DesktopShare = ({ hideLabel circle size="lg" - onClick={isVideoBroadcasting ? handleUnshareScreen : () => { - if (IS_SAFARI && !ScreenshareBridgeService.hasDisplayMedia) { - return mountModal(<Modal - overlayClassName={styles.overlay} - className={styles.modal} - onRequestClose={() => mountModal(null)} - hideBorder - contentLabel={intl.formatMessage(intlMessages.screenShareUnavailable)} - > - <h3 className={styles.title}> - {intl.formatMessage(intlMessages.screenShareUnavailable)} - </h3> - <p>{intl.formatMessage(intlMessages.screenShareNotSupported)}</p> - </Modal>); + onClick={isVideoBroadcasting + ? screenshareHasEnded + : () => { + if (IS_SAFARI && !ScreenshareBridgeService.hasDisplayMedia) { + renderScreenshareUnavailableModal(); + } else { + shareScreen(onFail); + } } - handleShareScreen(onFail); - } } id={isVideoBroadcasting ? 'unshare-screen-button' : 'share-screen-button'} /> ) : null; }; -DesktopShare.propTypes = propTypes; -export default withModalMounter(injectIntl(memo(DesktopShare))); +ScreenshareButton.propTypes = propTypes; +export default withModalMounter(injectIntl(memo(ScreenshareButton))); diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/screenshare/container.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/screenshare/container.jsx new file mode 100644 index 0000000000000000000000000000000000000000..5e100fd6f43753316c9c74142ba0849fac089a88 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/screenshare/container.jsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { withTracker } from 'meteor/react-meteor-data'; +import { withModalMounter } from '/imports/ui/components/modal/service'; +import ScreenshareButton from './component'; +import getFromUserSettings from '/imports/ui/services/users-settings'; +import { + isVideoBroadcasting, + dataSavingSetting, +} from '/imports/ui/components/screenshare/service'; + +const ScreenshareButtonContainer = props => <ScreenshareButton {...props} />; + +/* + * All props, including the ones that are inherited from actions-bar + * isVideoBroadcasting, + * amIPresenter, + * screenSharingCheck, + * isMeteorConnected, + * screenshareDataSavingSetting, + */ +export default withModalMounter(withTracker(({ mountModal }) => ({ + isVideoBroadcasting: isVideoBroadcasting(), + screenshareDataSavingSetting: dataSavingSetting(), + enabled: getFromUserSettings( + 'bbb_enable_screen_sharing', + Meteor.settings.public.kurento.enableScreensharing + ), +}))(ScreenshareButtonContainer)); diff --git a/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx b/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx index 26abe0eae01b0afcf3cefa48f0ddd7d749cfd64a..cdf759790f3c530d569b1b897d2860367a72cd19 100755 --- a/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx @@ -7,7 +7,20 @@ import FullscreenButtonContainer from '../fullscreen-button/container'; import { styles } from './styles'; import AutoplayOverlay from '../media/autoplay-overlay/component'; import logger from '/imports/startup/client/logger'; +import cx from 'classnames'; import playAndRetry from '/imports/utils/mediaElementPlayRetry'; +import { + SCREENSHARE_MEDIA_ELEMENT_NAME, + screenshareHasEnded, + screenshareHasStarted, + getMediaElement, + attachLocalPreviewStream, +} from '/imports/ui/components/screenshare/service'; +import { + isStreamStateHealthy, + subscribeToStreamStateChange, + unsubscribeFromStreamStateChange, +} from '/imports/ui/services/bbb-webrtc-sfu/stream-state-service'; const intlMessages = defineMessages({ screenShareLabel: { @@ -31,49 +44,63 @@ class ScreenshareComponent extends React.Component { loaded: false, isFullscreen: false, autoplayBlocked: false, + isStreamHealthy: false, }; - this.onVideoLoad = this.onVideoLoad.bind(this); + this.onLoadedData = this.onLoadedData.bind(this); this.onFullscreenChange = this.onFullscreenChange.bind(this); this.handleAllowAutoplay = this.handleAllowAutoplay.bind(this); this.handlePlayElementFailed = this.handlePlayElementFailed.bind(this); this.failedMediaElements = []; + this.onStreamStateChange = this.onStreamStateChange.bind(this); } componentDidMount() { - const { presenterScreenshareHasStarted } = this.props; - presenterScreenshareHasStarted(); - + screenshareHasStarted(); this.screenshareContainer.addEventListener('fullscreenchange', this.onFullscreenChange); + // Autoplay failure handling window.addEventListener('screensharePlayFailed', this.handlePlayElementFailed); + // Stream health state tracker to propagate UI changes on reconnections + subscribeToStreamStateChange('screenshare', this.onStreamStateChange); + // Attaches the local stream if it exists to serve as the local presenter preview + attachLocalPreviewStream(getMediaElement()); } componentWillReceiveProps(nextProps) { const { - isPresenter, unshareScreen, + isPresenter, } = this.props; if (isPresenter && !nextProps.isPresenter) { - unshareScreen(); + screenshareHasEnded(); } } componentWillUnmount() { const { - presenterScreenshareHasEnded, - unshareScreen, getSwapLayout, shouldEnableSwapLayout, toggleSwapLayout, } = this.props; const layoutSwapped = getSwapLayout() && shouldEnableSwapLayout(); if (layoutSwapped) toggleSwapLayout(); - presenterScreenshareHasEnded(); - unshareScreen(); + screenshareHasEnded(); this.screenshareContainer.removeEventListener('fullscreenchange', this.onFullscreenChange); window.removeEventListener('screensharePlayFailed', this.handlePlayElementFailed); + unsubscribeFromStreamStateChange('screenshare', this.onStreamStateChange); + } + + onStreamStateChange (event) { + const { streamState } = event.detail; + const { isStreamHealthy } = this.state; + + const newHealthState = isStreamStateHealthy(streamState); + event.stopPropagation(); + if (newHealthState !== isStreamHealthy) { + this.setState({ isStreamHealthy: newHealthState }); + } } - onVideoLoad() { + onLoadedData() { this.setState({ loaded: true }); } @@ -143,12 +170,26 @@ class ScreenshareComponent extends React.Component { ); } + renderAutoplayOverlay() { + const { intl } = this.props; + + return ( + <AutoplayOverlay + key={_.uniqueId('screenshareAutoplayOverlay')} + autoplayBlockedDesc={intl.formatMessage(intlMessages.autoplayBlockedDesc)} + autoplayAllowLabel={intl.formatMessage(intlMessages.autoplayAllowLabel)} + handleAllowAutoplay={this.handleAllowAutoplay} + /> + ); + } + render() { - const { loaded, autoplayBlocked } = this.state; + const { loaded, autoplayBlocked, isStreamHealthy } = this.state; const { intl } = this.props; + const shouldRenderReconnect = !isStreamHealthy && loaded; return ( - [!loaded + [(!loaded || shouldRenderReconnect) ? ( <div key={_.uniqueId('screenshareArea-')} @@ -158,28 +199,26 @@ class ScreenshareComponent extends React.Component { : null, !autoplayBlocked ? null - : ( - <AutoplayOverlay - key={_.uniqueId('screenshareAutoplayOverlay')} - autoplayBlockedDesc={intl.formatMessage(intlMessages.autoplayBlockedDesc)} - autoplayAllowLabel={intl.formatMessage(intlMessages.autoplayAllowLabel)} - handleAllowAutoplay={this.handleAllowAutoplay} - /> - ), + : (this.renderAutoplayOverlay()), ( <div className={styles.screenshareContainer} key="screenshareContainer" ref={(ref) => { this.screenshareContainer = ref; }} > + {loaded && this.renderFullscreenButton()} + <video - id="screenshareVideo" - key="screenshareVideo" + id={SCREENSHARE_MEDIA_ELEMENT_NAME} + key={SCREENSHARE_MEDIA_ELEMENT_NAME} style={{ maxHeight: '100%', width: '100%', height: '100%' }} playsInline - onLoadedData={this.onVideoLoad} + onLoadedData={this.onLoadedData} ref={(ref) => { this.videoTag = ref; }} + className={cx({ + [styles.unhealthyStream]: !isStreamHealthy, + })} muted /> </div> @@ -193,7 +232,4 @@ export default injectIntl(ScreenshareComponent); ScreenshareComponent.propTypes = { intl: intlShape.isRequired, isPresenter: PropTypes.bool.isRequired, - unshareScreen: PropTypes.func.isRequired, - presenterScreenshareHasEnded: PropTypes.func.isRequired, - presenterScreenshareHasStarted: PropTypes.func.isRequired, }; diff --git a/bigbluebutton-html5/imports/ui/components/screenshare/container.jsx b/bigbluebutton-html5/imports/ui/components/screenshare/container.jsx index 699204b1365376479d816218d24c5029af3148be..60502e3981812d593fec1570ce892287b6aceafb 100755 --- a/bigbluebutton-html5/imports/ui/components/screenshare/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/screenshare/container.jsx @@ -4,14 +4,12 @@ import Users from '/imports/api/users/'; import Auth from '/imports/ui/services/auth'; import MediaService, { getSwapLayout, shouldEnableSwapLayout } from '/imports/ui/components/media/service'; import { - isVideoBroadcasting, presenterScreenshareHasEnded, unshareScreen, - presenterScreenshareHasStarted, + isVideoBroadcasting, } from './service'; import ScreenshareComponent from './component'; const ScreenshareContainer = (props) => { - const { isVideoBroadcasting: isVB } = props; - if (isVB()) { + if (isVideoBroadcasting()) { return <ScreenshareComponent {...props} />; } return null; @@ -21,10 +19,6 @@ export default withTracker(() => { const user = Users.findOne({ userId: Auth.userID }, { fields: { presenter: 1 } }); return { isPresenter: user.presenter, - unshareScreen, - isVideoBroadcasting, - presenterScreenshareHasStarted, - presenterScreenshareHasEnded, getSwapLayout, shouldEnableSwapLayout, toggleSwapLayout: MediaService.toggleSwapLayout, diff --git a/bigbluebutton-html5/imports/ui/components/screenshare/service.js b/bigbluebutton-html5/imports/ui/components/screenshare/service.js index 6ec077badc455928ef77b55077c0b279b49828e2..c22da4fdeb302eefbe298582686b68b10a3fa002 100644 --- a/bigbluebutton-html5/imports/ui/components/screenshare/service.js +++ b/bigbluebutton-html5/imports/ui/components/screenshare/service.js @@ -10,35 +10,42 @@ import Auth from '/imports/ui/services/auth'; import UserListService from '/imports/ui/components/user-list/service'; import AudioService from '/imports/ui/components/audio/service'; +const SCREENSHARE_MEDIA_ELEMENT_NAME = 'screenshareVideo'; + +let _isSharingScreen = false; +const _sharingScreenDep = { + value: false, + tracker: new Tracker.Dependency(), +}; + +const isSharingScreen = () => { + _sharingScreenDep.tracker.depend(); + return _sharingScreenDep.value; +}; + +const setSharingScreen = (isSharingScreen) => { + if (_sharingScreenDep.value !== isSharingScreen) { + _sharingScreenDep.value = isSharingScreen; + _sharingScreenDep.tracker.changed(); + } +}; + // when the meeting information has been updated check to see if it was // screensharing. If it has changed either trigger a call to receive video // and display it, or end the call and hide the video const isVideoBroadcasting = () => { + const sharing = isSharingScreen(); const screenshareEntry = Screenshare.findOne({ meetingId: Auth.meetingID }, { fields: { 'screenshare.stream': 1 } }); + const screenIsShared = !screenshareEntry ? false : !!screenshareEntry.screenshare.stream; - if (!screenshareEntry) { - return false; + if (screenIsShared && isSharingScreen) { + setSharingScreen(false); } - return !!screenshareEntry.screenshare.stream; + return sharing || screenIsShared; }; -// if remote screenshare has been ended disconnect and hide the video stream -const presenterScreenshareHasEnded = () => { - // references a function in the global namespace inside kurento-extension.js - // that we load dynamically - KurentoBridge.kurentoExitVideo(); -}; - -const viewScreenshare = (hasAudio) => { - const amIPresenter = UserListService.isUserPresenter(Auth.userID); - if (!amIPresenter) { - KurentoBridge.kurentoViewScreen(hasAudio); - } else { - KurentoBridge.kurentoViewLocalPreview(); - } -}; const screenshareHasAudio = () => { const screenshareEntry = Screenshare.findOne({ meetingId: Auth.meetingID }, @@ -51,37 +58,66 @@ const screenshareHasAudio = () => { return !!screenshareEntry.screenshare.hasAudio; } -// if remote screenshare has been started connect and display the video stream -const presenterScreenshareHasStarted = () => { - const hasAudio = screenshareHasAudio(); +const screenshareHasEnded = () => { + const amIPresenter = UserListService.isUserPresenter(Auth.userID); - // WebRTC restrictions may need a capture device permission to release - // useful ICE candidates on recvonly/no-gUM peers - tryGenerateIceCandidates().then(() => { - viewScreenshare(hasAudio); - }).catch((error) => { - logger.error({ - logCode: 'screenshare_no_valid_candidate_gum_failure', - extraInfo: { - errorName: error.name, - errorMessage: error.message, - }, - }, `Forced gUM to release additional ICE candidates failed due to ${error.name}.`); - // The fallback gUM failed. Try it anyways and hope for the best. - viewScreenshare(hasAudio); - }); + if (amIPresenter) { + setSharingScreen(false); + } + + KurentoBridge.stop(); + screenShareEndAlert(); }; -const shareScreen = (onFail) => { +const getMediaElement = () => { + return document.getElementById(SCREENSHARE_MEDIA_ELEMENT_NAME); +} + +const attachLocalPreviewStream = (mediaElement) => { + const stream = KurentoBridge.gdmStream; + if (stream && mediaElement) { + // Always muted, presenter preview. + BridgeService.screenshareLoadAndPlayMediaStream(stream, mediaElement, true); + } +} + +const screenshareHasStarted = () => { + const amIPresenter = UserListService.isUserPresenter(Auth.userID); + + // Presenter's screen preview is local, so skip + if (!amIPresenter) { + viewScreenshare(); + } +}; + +const shareScreen = async (onFail) => { // stop external video share if running const meeting = Meetings.findOne({ meetingId: Auth.meetingID }); + if (meeting && meeting.externalVideoUrl) { stopWatching(); } - BridgeService.getScreenStream().then(stream => { - KurentoBridge.kurentoShareScreen(onFail, stream); - }).catch(onFail); + try { + const stream = await BridgeService.getScreenStream(); + await KurentoBridge.share(stream, onFail); + setSharingScreen(true); + } catch (error) { + return onFail(error); + } +}; + +const viewScreenshare = () => { + const hasAudio = screenshareHasAudio(); + KurentoBridge.view(hasAudio).catch((error) => { + logger.error({ + logCode: 'screenshare_view_failed', + extraInfo: { + errorName: error.name, + errorMessage: error.message, + }, + }, `Screenshare viewer failure`); + }); }; const screenShareEndAlert = () => AudioService @@ -89,19 +125,18 @@ const screenShareEndAlert = () => AudioService + Meteor.settings.public.app.basename}` + '/resources/sounds/ScreenshareOff.mp3'); -const unshareScreen = () => { - KurentoBridge.kurentoExitScreenShare(); - screenShareEndAlert(); -}; - const dataSavingSetting = () => Settings.dataSaving.viewScreenshare; export { + SCREENSHARE_MEDIA_ELEMENT_NAME, isVideoBroadcasting, - presenterScreenshareHasEnded, - presenterScreenshareHasStarted, + screenshareHasEnded, + screenshareHasStarted, shareScreen, screenShareEndAlert, - unshareScreen, dataSavingSetting, + isSharingScreen, + setSharingScreen, + getMediaElement, + attachLocalPreviewStream, }; diff --git a/bigbluebutton-html5/imports/ui/components/screenshare/styles.scss b/bigbluebutton-html5/imports/ui/components/screenshare/styles.scss index aff8c3d1932b53bfe58544b3a3dc44a37bb7942b..a9ca8486628d78b4e1edd4a3f26c6eab13d80db4 100755 --- a/bigbluebutton-html5/imports/ui/components/screenshare/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/screenshare/styles.scss @@ -3,10 +3,9 @@ .connecting { @extend .connectingSpinner; - z-index: -1; background-color: transparent; color: var(--color-white); - font-size: 2.5rem * 5; + font-size: 2.5rem * 3; } .screenshareContainer{ @@ -18,3 +17,7 @@ width: 100%; height: 100%; } + +.unhealthyStream { + filter: grayscale(50%) opacity(50%); +} diff --git a/bigbluebutton-html5/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml index 62c69a3f6aa3fae15ab8fa27f9339c75ad666bfc..fa5ac6653775838e751ef045851f38fb6155ce3e 100755 --- a/bigbluebutton-html5/private/config/settings.yml +++ b/bigbluebutton-html5/private/config/settings.yml @@ -110,6 +110,14 @@ public: chromeExtensionKey: KEY chromeExtensionLink: LINK screenshare: + mediaTimeouts: + maxConnectionAttempts: 2 + # Base screen media timeout (send|recv) + baseTimeout: 15000 + # Max timeout: used as the max camera subscribe reconnection timeout. Each + # subscribe reattempt increases the reconnection timer up to this + maxTimeout: 35000 + timeoutIncreaseFactor: 1.5 constraints: video: frameRate: @@ -120,10 +128,6 @@ public: height: max: 1600 audio: false - chromeScreenshareSources: - - window - - screen - firefoxScreenshareSource: window # cameraProfiles is an array of: # - id: profile identifier # name: human-readable profile name diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json index 7bac8d4e733f71f1b8f49cde347c5f10e2a32e1d..b460ccea07c8c782c211c4c8a9186165cc20570b 100755 --- a/bigbluebutton-html5/private/locales/en.json +++ b/bigbluebutton-html5/private/locales/en.json @@ -593,6 +593,7 @@ "app.video.clientDisconnected": "Webcam cannot be shared due to connection issues", "app.fullscreenButton.label": "Make {0} fullscreen", "app.deskshare.iceConnectionStateError": "Connection failed when sharing screen (ICE error 1108)", + "app.deskshare.mediaFlowTimeout": "Media could not reach the server (error 1120)", "app.sfu.mediaServerConnectionError2000": "Unable to connect to media server (error 2000)", "app.sfu.mediaServerOffline2001": "Media server is offline. Please try again later (error 2001)", "app.sfu.mediaServerNoResources2002": "Media server has no available resources (error 2002)", diff --git a/bigbluebutton-html5/public/compatibility/kurento-extension.js b/bigbluebutton-html5/public/compatibility/kurento-extension.js deleted file mode 100755 index d5e0f51da465651fc41f72fcef3dfe6714f900be..0000000000000000000000000000000000000000 --- a/bigbluebutton-html5/public/compatibility/kurento-extension.js +++ /dev/null @@ -1,924 +0,0 @@ -const isFirefox = typeof window.InstallTrigger !== 'undefined'; -const isOpera = !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0; -const isChrome = !!window.chrome && !isOpera; -const isSafari = navigator.userAgent.indexOf('Safari') >= 0 && !isChrome; -const isElectron = navigator.userAgent.toLowerCase().indexOf(' electron/') > -1; -const hasDisplayMedia = (typeof navigator.getDisplayMedia === 'function' - || (navigator.mediaDevices && typeof navigator.mediaDevices.getDisplayMedia === 'function')); - -Kurento = function ( - tag, - voiceBridge, - userId, - internalMeetingId, - onFail, - onSuccess, - options = {}, -) { - this.ws = null; - this.video = null; - this.screen = null; - this.webRtcPeer = null; - this.mediaCallback = null; - - this.renderTag = tag; - this.voiceBridge = voiceBridge; - this.userId = userId; - this.internalMeetingId = internalMeetingId; - - // Optional parameters are: userName, caleeName, chromeExtension, wsUrl, iceServers, - // chromeScreenshareSources, firefoxScreenshareSource, logger, stream, hasAudio - - Object.assign(this, options); - - this.SEND_ROLE = 'send'; - this.RECV_ROLE = 'recv'; - this.SFU_APP = 'screenshare'; - this.ON_ICE_CANDIDATE_MSG = 'iceCandidate'; - this.PING_INTERVAL = 15000; - - window.Logger = this.logger || console; - - if (this.wsUrl == null) { - this.defaultPath = 'bbb-webrtc-sfu'; - this.hostName = window.location.hostname; - this.wsUrl = `wss://${this.hostName}/${this.defaultPath}`; - } - - if (this.chromeScreenshareSources == null) { - this.chromeScreenshareSources = ['screen', 'window']; - } - - if (this.firefoxScreenshareSource == null) { - this.firefoxScreenshareSource = 'window'; - } - - // Limiting max resolution to WQXGA - // In FireFox we force full screen share and in the case - // of multiple screens the total area shared becomes too large - this.vid_max_width = 2560; - this.vid_max_height = 1600; - this.width = window.screen.width; - this.height = window.screen.height; - - - this.userId = userId; - - this.pingInterval = null; - - // TODO FIXME we need to implement a handleError method to normalize errors - // generated in this script - if (onFail != null) { - this.onFail = Kurento.normalizeCallback(onFail); - } else { - const _this = this; - this.onFail = function () { - _this.logger.error('Default error handler'); - }; - } - - if (onSuccess != null) { - this.onSuccess = Kurento.normalizeCallback(onSuccess); - } else { - const _this = this; - this.onSuccess = function () { - _this.logger.info('Default success handler'); - }; - } -}; - -this.KurentoManager = function () { - this.kurentoVideo = null; - this.kurentoScreenshare = null; - this.kurentoAudio = null; -}; - -KurentoManager.prototype.exitScreenShare = function () { - if (typeof this.kurentoScreenshare !== 'undefined' && this.kurentoScreenshare) { - if (this.kurentoScreenshare.logger !== null) { - this.kurentoScreenshare.logger.info({ logCode: 'kurentoextension_exit_screenshare_presenter' }, - 'Exiting screensharing as presenter'); - } - - if(this.kurentoScreenshare.webRtcPeer) { - this.kurentoScreenshare.webRtcPeer.peerConnection.oniceconnectionstatechange = null; - } - - if (this.kurentoScreenshare.ws !== null) { - this.kurentoScreenshare.ws.onclose = function () {}; - this.kurentoScreenshare.ws.close(); - } - - if (this.kurentoScreenshare.pingInterval) { - clearInterval(this.kurentoScreenshare.pingInterval); - } - - this.kurentoScreenshare.dispose(); - this.kurentoScreenshare = null; - } -}; - -KurentoManager.prototype.exitVideo = function () { - try { - if (typeof this.kurentoVideo !== 'undefined' && this.kurentoVideo) { - if(this.kurentoVideo.webRtcPeer) { - this.kurentoVideo.webRtcPeer.peerConnection.oniceconnectionstatechange = null; - } - - if (this.kurentoVideo.logger !== null) { - this.kurentoScreenshare.logger.info({ logCode: 'kurentoextension_exit_screenshare_viewer' }, - 'Exiting screensharing as viewer'); - } - - if (this.kurentoVideo.ws !== null) { - this.kurentoVideo.ws.onclose = function () {}; - this.kurentoVideo.ws.close(); - } - - if (this.kurentoVideo.pingInterval) { - clearInterval(this.kurentoVideo.pingInterval); - } - - this.kurentoVideo.dispose(); - this.kurentoVideo = null; - } - } - catch (err) { - if (this.kurentoVideo) { - this.kurentoVideo.dispose(); - this.kurentoVideo = null; - } - } -}; - -KurentoManager.prototype.exitAudio = function () { - if (typeof this.kurentoAudio !== 'undefined' && this.kurentoAudio) { - if (this.kurentoAudio.logger !== null) { - this.kurentoAudio.logger.info({ logCode: 'kurentoextension_exit_listen_only' }, - 'Exiting listen only'); - } - - if (this.kurentoAudio.webRtcPeer) { - this.kurentoAudio.webRtcPeer.peerConnection.oniceconnectionstatechange = null; - } - - if (this.kurentoAudio.ws !== null) { - this.kurentoAudio.ws.onclose = function () {}; - this.kurentoAudio.ws.close(); - } - - if (this.kurentoAudio.pingInterval) { - clearInterval(this.kurentoAudio.pingInterval); - } - - this.kurentoAudio.dispose(); - this.kurentoAudio = null; - } -}; - - -KurentoManager.prototype.shareScreen = function (tag) { - this.exitScreenShare(); - const obj = Object.create(Kurento.prototype); - Kurento.apply(obj, arguments); - this.kurentoScreenshare = obj; - this.kurentoScreenshare.setScreensharing(tag); -}; - -KurentoManager.prototype.joinWatchVideo = function (tag) { - this.exitVideo(); - const obj = Object.create(Kurento.prototype); - Kurento.apply(obj, arguments); - this.kurentoVideo = obj; - this.kurentoVideo.setWatchVideo(tag); -}; - -KurentoManager.prototype.getFirefoxScreenshareSource = function () { - return this.kurentoScreenshare.firefoxScreenshareSource; -}; - -KurentoManager.prototype.getChromeScreenshareSources = function () { - return this.kurentoScreenshare.chromeScreenshareSources; -}; - -KurentoManager.prototype.getChromeExtensionKey = function () { - return this.kurentoScreenshare.chromeExtension; -}; - - -Kurento.prototype.setScreensharing = function (tag) { - this.mediaCallback = this.startScreensharing.bind(this); - this.create(tag); -}; - -Kurento.prototype.create = function (tag) { - this.setRenderTag(tag); - this.init(); -}; - -Kurento.prototype.downscaleResolution = function (oldWidth, oldHeight) { - const factorWidth = this.vid_max_width / oldWidth; - const factorHeight = this.vid_max_height / oldHeight; - let width, - height; - - if (factorWidth < factorHeight) { - width = Math.trunc(oldWidth * factorWidth); - height = Math.trunc(oldHeight * factorWidth); - } else { - width = Math.trunc(oldWidth * factorHeight); - height = Math.trunc(oldHeight * factorHeight); - } - - return { width, height }; -}; - -Kurento.prototype.init = function () { - const self = this; - if ('WebSocket' in window) { - this.ws = new WebSocket(this.wsUrl); - - this.ws.onmessage = this.onWSMessage.bind(this); - this.ws.onclose = () => { - kurentoManager.exitScreenShare(); - this.logger.error({ logCode: 'kurentoextension_websocket_close' }, - 'WebSocket connection to SFU closed unexpectedly, screenshare/listen only will drop'); - self.onFail('Websocket connection closed'); - }; - this.ws.onerror = (error) => { - kurentoManager.exitScreenShare(); - this.logger.error({ - logCode: 'kurentoextension_websocket_error', - extraInfo: { errorMessage: error.name || error.message || 'Unknown error' } - }, 'Error in the WebSocket connection to SFU, screenshare/listen only will drop'); - self.onFail('Websocket connection error'); - }; - this.ws.onopen = function () { - self.pingInterval = setInterval(self.ping.bind(self), self.PING_INTERVAL); - self.mediaCallback(); - }; - } else { - this.logger.info({ logCode: 'kurentoextension_websocket_unsupported'}, - 'Browser does not support websockets'); - } -}; - -Kurento.prototype.onWSMessage = function (message) { - const parsedMessage = JSON.parse(message.data); - switch (parsedMessage.id) { - case 'startResponse': - this.startResponse(parsedMessage); - break; - case 'stopSharing': - kurentoManager.exitScreenShare(); - break; - case 'iceCandidate': - this.handleIceCandidate(parsedMessage.candidate); - break; - case 'webRTCAudioSuccess': - this.onSuccess(parsedMessage.success); - break; - case 'webRTCAudioError': - case 'error': - this.handleSFUError(parsedMessage); - break; - case 'pong': - break; - default: - this.logger.error({ - logCode: 'kurentoextension_unrecognized_sfu_message', - extraInfo: { sfuResponse: parsedMessage } - }, `Unrecognized SFU message: ${parsedMessage.id}`); - } -}; - -Kurento.prototype.setRenderTag = function (tag) { - this.renderTag = tag; -}; - -Kurento.prototype.processIceQueue = function () { - const peer = this.webRtcPeer; - while (peer.iceQueue.length) { - const candidate = peer.iceQueue.shift(); - peer.addIceCandidate(candidate, (error) => { - if (error) { - // Just log the error. We can't be sure if a candidate failure on add is - // fatal or not, so that's why we have a timeout set up for negotiations and - // listeners for ICE state transitioning to failures, so we won't act on it here - this.logger.error({ - logCode: 'kurentoextension_addicecandidate_error', - extraInfo: { errorMessage: error.name || error.message || 'Unknown error' }, - }, `Adding ICE candidate failed due to ${error.message}`); - } - }); - } -} - -Kurento.prototype.handleIceCandidate = function (candidate) { - const peer = this.webRtcPeer; - if (peer.negotiated) { - peer.addIceCandidate(candidate, (error) => { - if (error) { - // Just log the error. We can't be sure if a candidate failure on add is - // fatal or not, so that's why we have a timeout set up for negotiations and - // listeners for ICE state transitioning to failures, so we won't act on it here - this.logger.error({ - logCode: 'kurentoextension_addicecandidate_error', - extraInfo: { errorMessage: error.name || error.message || 'Unknown error' }, - }, `Adding ICE candidate failed due to ${error.message}`); - } - }); - } else { - // ICE candidates are queued until a SDP answer has been processed. - // This was done due to a long term iOS/Safari quirk where it'd - // fail if candidates were added before the offer/answer cycle was completed. - // IT STILL HAPPENS - prlanzarin sept 2019 - peer.iceQueue.push(candidate); - } -} - -Kurento.prototype.startResponse = function (message) { - if (message.response !== 'accepted') { - this.handleSFUError(message); - } else { - this.logger.info({ - logCode: 'kurentoextension_start_success', - extraInfo: { sfuResponse: message } - }, `Start request accepted for ${message.type}`); - - this.webRtcPeer.processAnswer(message.sdpAnswer, (error) => { - if (error) { - this.logger.error({ - logCode: 'kurentoextension_peerconnection_processanswer_error', - extraInfo: { - errorMessage: error.name || error.message || 'Unknown error', - }, - }, `Processing SDP answer from SFU for failed due to ${error.message}`); - - return this.onFail(error); - } - - this.logger.info({ - logCode: 'kurentoextension_process_answer', - }, `Answer processed with success`); - - // Mark the peer as negotiated and flush the ICE queue - this.webRtcPeer.negotiated = true; - this.processIceQueue(); - // audio calls gets their success callback in a subsequent step (@webRTCAudioSuccess) - // due to legacy messaging which I don't intend to break now - prlanzarin - if (message.type === 'screenshare') { - this.onSuccess() - } - }); - } -}; - -Kurento.prototype.handleSFUError = function (sfuResponse) { - const { type, code, reason, role } = sfuResponse; - switch (type) { - case 'screenshare': - this.logger.error({ - logCode: 'kurentoextension_screenshare_start_rejected', - extraInfo: { sfuResponse } - }, `SFU screenshare rejected by SFU with error ${code} = ${reason}`); - - if (role === this.SEND_ROLE) { - kurentoManager.exitScreenShare(); - } else if (role === this.RECV_ROLE) { - kurentoManager.exitVideo(); - } - break; - case 'audio': - this.logger.error({ - logCode: 'kurentoextension_listenonly_start_rejected', - extraInfo: { sfuResponse } - }, `SFU listen only rejected by SFU with error ${code} = ${reason}`); - - kurentoManager.exitAudio(); - break; - } - - this.onFail( { code, reason } ); -}; - -Kurento.prototype.onOfferPresenter = function (error, offerSdp) { - const self = this; - - if (error) { - this.logger.error({ - logCode: 'kurentoextension_screenshare_presenter_offer_failure', - extraInfo: { errorMessage: error.name || error.message || 'Unknown error' }, - }, `Failed to generate peer connection offer for screenshare presenter with error ${error.message}`); - this.onFail(error); - return; - } - - const message = { - id: 'start', - type: this.SFU_APP, - role: this.SEND_ROLE, - internalMeetingId: self.internalMeetingId, - voiceBridge: self.voiceBridge, - callerName: self.userId, - sdpOffer: offerSdp, - vh: this.height, - vw: this.width, - userName: self.userName, - hasAudio: !!this.hasAudio, - }; - - this.logger.info({ - logCode: 'kurentoextension_screenshare_request_start_presenter' , - extraInfo: { sfuRequest: message }, - }, `Screenshare presenter offer generated. Sending start request to SFU`); - - this.sendMessage(message); -}; - - -Kurento.prototype.startScreensharing = function () { - if (window.chrome) { - if (this.chromeExtension == null && !hasDisplayMedia) { - this.logger.error({ logCode: "kurentoextension_screenshare_noextensionkey" }, - 'Screenshare hasnt got a Chrome extension key configured', - ); - // TODO error handling here - this.onFail(); - return; - } - } - - const options = { - localVideo: document.getElementById(this.renderTag), - onicecandidate: (candidate) => { - this.onIceCandidate(candidate, this.SEND_ROLE); - }, - sendSource: 'desktop', - videoStream: this.stream || undefined, - }; - - let resolution; - this.logger.debug({ logCode: 'kurentoextension_screenshare_screen_dimensions'}, - `Screenshare screen dimensions are ${this.width} x ${this.height}`); - if (this.width > this.vid_max_width || this.height > this.vid_max_height) { - resolution = this.downscaleResolution(this.width, this.height); - this.width = resolution.width; - this.height = resolution.height; - this.logger.info({ logCode: 'kurentoextension_screenshare_track_resize' }, - `Screenshare track dimensions have been resized to ${this.width} x ${this.height}`); - } - - this.addIceServers(this.iceServers, options); - - this.webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerSendonly(options, (error) => { - if (error) { - this.logger.error({ - logCode: 'kurentoextension_screenshare_peerconnection_create_error', - extraInfo: { errorMessage: error.name || error.message || 'Unknown error' }, - }, `WebRTC peer constructor for screenshare (presenter) failed due to ${error.message}`); - this.onFail(error); - return kurentoManager.exitScreenShare(); - } - - this.webRtcPeer.iceQueue = []; - this.webRtcPeer.generateOffer(this.onOfferPresenter.bind(this)); - - const localStream = this.webRtcPeer.peerConnection.getLocalStreams()[0]; - const _this = this; - localStream.getVideoTracks()[0].onended = function () { - _this.webRtcPeer.peerConnection.oniceconnectionstatechange = null; - return kurentoManager.exitScreenShare(); - }; - - localStream.getVideoTracks()[0].oninactive = function () { - return kurentoManager.exitScreenShare(); - }; - }); - this.webRtcPeer.peerConnection.oniceconnectionstatechange = () => { - if (this.webRtcPeer) { - const iceConnectionState = this.webRtcPeer.peerConnection.iceConnectionState; - if (iceConnectionState === 'failed' || iceConnectionState === 'closed') { - this.webRtcPeer.peerConnection.oniceconnectionstatechange = null; - this.logger.error({ - logCode: 'kurentoextension_screenshare_presenter_ice_failed', - extraInfo: { iceConnectionState } - }, `WebRTC peer for screenshare presenter failed due to ICE transitioning to ${iceConnectionState}`); - this.onFail({ message: 'iceConnectionStateError', code: 1108 }); - } - } - }; -}; - -Kurento.prototype.onIceCandidate = function (candidate, role) { - const self = this; - this.logger.debug({ - logCode: 'kurentoextension_screenshare_client_candidate', - extraInfo: { candidate } - }, `Screenshare client-side candidate generated: ${JSON.stringify(candidate)}`); - - const message = { - id: this.ON_ICE_CANDIDATE_MSG, - role, - type: this.SFU_APP, - voiceBridge: self.voiceBridge, - candidate, - callerName: self.userId, - }; - - this.sendMessage(message); -}; - -Kurento.prototype.setWatchVideo = function (tag) { - this.useVideo = true; - this.useCamera = 'none'; - this.useMic = 'none'; - this.mediaCallback = this.viewer; - this.create(tag); -}; - -Kurento.prototype.viewer = function () { - const self = this; - if (!this.webRtcPeer) { - const options = { - mediaConstraints: { - audio: !!self.hasAudio, - }, - onicecandidate: (candidate) => { - this.onIceCandidate(candidate, this.RECV_ROLE); - }, - }; - - this.addIceServers(this.iceServers, options); - - self.webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly(options, function (error) { - if (error) { - return self.onFail(error); - } - self.webRtcPeer.iceQueue = []; - this.generateOffer(self.onOfferViewer.bind(self)); - }); - self.webRtcPeer.peerConnection.oniceconnectionstatechange = () => { - if (this.webRtcPeer) { - const iceConnectionState = this.webRtcPeer.peerConnection.iceConnectionState; - if (iceConnectionState === 'failed' || iceConnectionState === 'closed') { - this.webRtcPeer.peerConnection.oniceconnectionstatechange = null; - this.logger.error({ - logCode: 'kurentoextension_screenshare_viewer_ice_failed', - extraInfo: { iceConnectionState } - }, `WebRTC peer for screenshare viewer failed due to ICE transitioning to ${iceConnectionState}`); - this.onFail({ message: 'iceConnectionStateError', code: 1108 }); - } - } - }; - } -}; - -Kurento.prototype.onOfferViewer = function (error, offerSdp) { - const self = this; - if (error) { - this.logger.error({ - logCode: 'kurentoextension_screenshare_viewer_offer_failure', - extraInfo: { errorMessage: error.name || error.message || 'Unknown error' }, - }, `Failed to generate peer connection offer for screenshare viewer with error ${error.message}`); - - return this.onFail(error); - } - - const message = { - id: 'start', - type: this.SFU_APP, - role: this.RECV_ROLE, - internalMeetingId: self.internalMeetingId, - voiceBridge: self.voiceBridge, - callerName: self.userId, - sdpOffer: offerSdp, - userName: self.userName, - }; - - this.logger.info({ - logCode: 'kurentoextension_screenshare_request_start_viewer', - extraInfo: { sfuRequest: message }, - }, `Screenshare viewer offer generated. Sending start request to SFU`); - - this.sendMessage(message); -}; - -KurentoManager.prototype.joinAudio = function (tag) { - this.exitAudio(); - const obj = Object.create(Kurento.prototype); - Kurento.apply(obj, arguments); - this.kurentoAudio = obj; - this.kurentoAudio.setAudio(tag); -}; - -Kurento.prototype.setAudio = function (tag) { - this.mediaCallback = this.listenOnly.bind(this); - this.create(tag); -}; - -Kurento.prototype.listenOnly = function () { - if (!this.webRtcPeer) { - const options = { - onicecandidate : this.onListenOnlyIceCandidate.bind(this), - mediaConstraints: { - audio: true, - video: false, - }, - }; - - this.addIceServers(this.iceServers, options); - - this.webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly(options, (error) => { - if (error) { - return this.onFail(error); - } - - this.webRtcPeer.iceQueue = []; - this.webRtcPeer.peerConnection.oniceconnectionstatechange = () => { - if (this.webRtcPeer) { - const iceConnectionState = this.webRtcPeer.peerConnection.iceConnectionState; - - if (iceConnectionState === 'failed' || iceConnectionState === 'closed') { - this.webRtcPeer.peerConnection.oniceconnectionstatechange = null; - this.logger.error({ - logCode: 'kurentoextension_listenonly_ice_failed', - extraInfo: { iceConnectionState } - }, `WebRTC peer for listen only failed due to ICE transitioning to ${iceConnectionState}`); - this.onFail({ - errorCode: 1007, - errorMessage: `ICE negotiation failed. Current state - ${iceConnectionState}`, - }); - } - } - } - - this.webRtcPeer.generateOffer(this.onOfferListenOnly.bind(this)); - }); - } -}; - -Kurento.prototype.onListenOnlyIceCandidate = function (candidate) { - const self = this; - this.logger.debug({ - logCode: 'kurentoextension_listenonly_client_candidate', - extraInfo: { candidate } - }, `Listen only client-side candidate generated: ${JSON.stringify(candidate)}`); - - const message = { - id: this.ON_ICE_CANDIDATE_MSG, - type: 'audio', - role: 'viewer', - voiceBridge: self.voiceBridge, - candidate, - }; - this.sendMessage(message); -}; - -Kurento.prototype.onOfferListenOnly = function (error, offerSdp) { - const self = this; - if (error) { - this.logger.error({ - logCode: 'kurentoextension_listenonly_offer_failure', - extraInfo: { errorMessage: error.name || error.message || 'Unknown error' }, - }, `Failed to generate peer connection offer for listen only with error ${error.message}`); - - return this.onFail(error); - } - - const message = { - id: 'start', - type: 'audio', - role: 'viewer', - voiceBridge: self.voiceBridge, - caleeName: self.caleeName, - sdpOffer: offerSdp, - userId: self.userId, - userName: self.userName, - internalMeetingId: self.internalMeetingId, - }; - - this.logger.info({ - logCode: 'kurentoextension_listenonly_request_start', - extraInfo: { sfuRequest: message }, - }, "Listen only offer generated. Sending start request to SFU"); - this.sendMessage(message); -}; - -Kurento.prototype.pauseTrack = function (message) { - const localStream = this.webRtcPeer.peerConnection.getLocalStreams()[0]; - const track = localStream.getVideoTracks()[0]; - - if (track) { - track.enabled = false; - } -}; - -Kurento.prototype.resumeTrack = function (message) { - const localStream = this.webRtcPeer.peerConnection.getLocalStreams()[0]; - const track = localStream.getVideoTracks()[0]; - - if (track) { - track.enabled = true; - } -}; - -Kurento.prototype.addIceServers = function (iceServers, options) { - if (iceServers && iceServers.length > 0) { - this.logger.debug({ - logCode: 'kurentoextension_add_iceservers', - extraInfo: { iceServers } - }, `Injecting ICE servers into peer creation`); - - options.configuration = {}; - options.configuration.iceServers = iceServers; - } -}; - -Kurento.prototype.stop = function () { - // if (this.webRtcPeer) { - // var message = { - // id : 'stop', - // type : 'screenshare', - // voiceBridge: kurentoHandler.voiceBridge - // } - // kurentoHandler.sendMessage(message); - // kurentoHandler.disposeScreenShare(); - // } -}; - -Kurento.prototype.dispose = function () { - if (this.webRtcPeer) { - this.webRtcPeer.dispose(); - this.webRtcPeer = null; - } -}; - -Kurento.prototype.ping = function () { - const message = { - id: 'ping', - }; - this.sendMessage(message); -}; - -Kurento.prototype.sendMessage = function (message) { - const jsonMessage = JSON.stringify(message); - this.ws.send(jsonMessage); -}; - -Kurento.normalizeCallback = function (callback) { - if (typeof callback === 'function') { - return callback; - } - return function (args) { - document.getElementById('BigBlueButton')[callback](args); - }; -}; - - -/* Global methods */ - -// this function explains how to use above methods/objects -window.getScreenConstraints = function (sendSource, callback) { - let screenConstraints = { video: {}, audio: false }; - - // Limiting FPS to a range of 5-10 (5 ideal) - screenConstraints.video.frameRate = { ideal: 5, max: 10 }; - - screenConstraints.video.height = { max: kurentoManager.kurentoScreenshare.vid_max_height }; - screenConstraints.video.width = { max: kurentoManager.kurentoScreenshare.vid_max_width }; - - const getChromeScreenConstraints = function (extensionKey) { - return new Promise((resolve, reject) => { - chrome.runtime.sendMessage( - extensionKey, - { - getStream: true, - sources: kurentoManager.getChromeScreenshareSources(), - }, - (response) => { - resolve(response); - } - ); - }); - }; - - const getDisplayMediaConstraints = function () { - // The fine-grained constraints (e.g.: frameRate) are supposed to go into - // the MediaStream because getDisplayMedia does not support them, - // so they're passed differently - kurentoManager.kurentoScreenshare.extensionInstalled = true; - optionalConstraints.width = { max: kurentoManager.kurentoScreenshare.vid_max_width }; - optionalConstraints.height = { max: kurentoManager.kurentoScreenshare.vid_max_height }; - optionalConstraints.frameRate = { ideal: 5, max: 10 }; - - let gDPConstraints = { - video: true, - optional: optionalConstraints - }; - - return gDPConstraints; - }; - - const optionalConstraints = [ - { googCpuOveruseDetection: true }, - { googCpuOveruseEncodeUsage: true }, - { googCpuUnderuseThreshold: 55 }, - { googCpuOveruseThreshold: 100 }, - { googPayloadPadding: true }, - { googScreencastMinBitrate: 600 }, - { googHighStartBitrate: true }, - { googHighBitrate: true }, - { googVeryHighBitrate: true }, - ]; - - if (isElectron) { - const sourceId = ipcRenderer.sendSync('screen-chooseSync'); - kurentoManager.kurentoScreenshare.extensionInstalled = true; - - // this statement sets gets 'sourceId" and sets "chromeMediaSourceId" - screenConstraints.video.chromeMediaSource = { exact: [sendSource] }; - screenConstraints.video.chromeMediaSourceId = sourceId; - screenConstraints.optional = optionalConstraints; - - return callback(null, screenConstraints); - } - - if (isChrome) { - if (!hasDisplayMedia) { - const extensionKey = kurentoManager.getChromeExtensionKey(); - getChromeScreenConstraints(extensionKey).then((constraints) => { - if (!constraints) { - document.dispatchEvent(new Event('installChromeExtension')); - return; - } - - const sourceId = constraints.streamId; - - kurentoManager.kurentoScreenshare.extensionInstalled = true; - - // Re-wrap the video constraints into the mandatory object (latest adapter) - screenConstraints.video = {}; - screenConstraints.video.mandatory = {}; - screenConstraints.video.mandatory.maxFrameRate = 10; - screenConstraints.video.mandatory.maxHeight = kurentoManager.kurentoScreenshare.vid_max_height; - screenConstraints.video.mandatory.maxWidth = kurentoManager.kurentoScreenshare.vid_max_width; - screenConstraints.video.mandatory.chromeMediaSource = sendSource; - screenConstraints.video.mandatory.chromeMediaSourceId = sourceId; - screenConstraints.optional = optionalConstraints; - - return callback(null, screenConstraints); - }); - } else { - return callback(null, getDisplayMediaConstraints()); - } - } - - if (isFirefox) { - const firefoxScreenshareSource = kurentoManager.getFirefoxScreenshareSource(); - screenConstraints.video.mediaSource = firefoxScreenshareSource; - return callback(null, screenConstraints); - } - - // Falls back to getDisplayMedia if the browser supports it - if (hasDisplayMedia) { - return callback(null, getDisplayMediaConstraints()); - } -}; - -window.kurentoInitialize = function () { - if (window.kurentoManager == null || window.KurentoManager === undefined) { - window.kurentoManager = new KurentoManager(); - } -}; - -window.kurentoShareScreen = function () { - window.kurentoInitialize(); - window.kurentoManager.shareScreen.apply(window.kurentoManager, arguments); -}; - - -window.kurentoExitScreenShare = function () { - window.kurentoInitialize(); - window.kurentoManager.exitScreenShare(); -}; - -window.kurentoWatchVideo = function () { - window.kurentoInitialize(); - window.kurentoManager.joinWatchVideo.apply(window.kurentoManager, arguments); -}; - -window.kurentoExitVideo = function () { - window.kurentoInitialize(); - window.kurentoManager.exitVideo(); -}; - -window.kurentoJoinAudio = function () { - window.kurentoInitialize(); - window.kurentoManager.joinAudio.apply(window.kurentoManager, arguments); -}; - -window.kurentoExitAudio = function () { - window.kurentoInitialize(); - window.kurentoManager.exitAudio(); -};