diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/ScreenshareModel.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/ScreenshareModel.scala index cae06abb02763bff02e2584313f7484d8f9c4e6b..dcf6934b677f3c22ac96fbe208eaa5f39a023a95 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/ScreenshareModel.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/ScreenshareModel.scala @@ -10,6 +10,7 @@ object ScreenshareModel { status.voiceConf = "" status.screenshareConf = "" status.timestamp = "" + status.hasAudio = false } def getScreenshareStarted(status: ScreenshareModel): Boolean = { @@ -79,6 +80,14 @@ object ScreenshareModel { def getTimestamp(status: ScreenshareModel): String = { status.timestamp } + + def setHasAudio(status: ScreenshareModel, hasAudio: Boolean): Unit = { + status.hasAudio = hasAudio + } + + def getHasAudio(status: ScreenshareModel): Boolean = { + status.hasAudio + } } class ScreenshareModel { @@ -90,4 +99,5 @@ class ScreenshareModel { private var voiceConf: String = "" private var screenshareConf: String = "" private var timestamp: String = "" + private var hasAudio = false } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/screenshare/GetScreenshareStatusReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/screenshare/GetScreenshareStatusReqMsgHdlr.scala index fbea8767508712f11c5940b2646540f0d8fd8fcc..1edea1b84c0f7e7d6129ac628328526ce26b3a13 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/screenshare/GetScreenshareStatusReqMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/screenshare/GetScreenshareStatusReqMsgHdlr.scala @@ -25,9 +25,10 @@ trait GetScreenshareStatusReqMsgHdlr { val vidWidth = ScreenshareModel.getScreenshareVideoWidth(liveMeeting.screenshareModel) val vidHeight = ScreenshareModel.getScreenshareVideoHeight(liveMeeting.screenshareModel) val timestamp = ScreenshareModel.getTimestamp(liveMeeting.screenshareModel) + val hasAudio = ScreenshareModel.getHasAudio(liveMeeting.screenshareModel) val body = ScreenshareRtmpBroadcastStartedEvtMsgBody(voiceConf, screenshareConf, - stream, vidWidth, vidHeight, timestamp) + stream, vidWidth, vidHeight, timestamp, hasAudio) val event = ScreenshareRtmpBroadcastStartedEvtMsg(header, body) BbbCommonEnvCoreMsg(envelope, event) } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/screenshare/ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/screenshare/ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgHdlr.scala index e72788eb6b68e87c3c9cc9ab2158612579df5a48..60ee455a0f115fcd71e264a969d1b9069884d6bd 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/screenshare/ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/screenshare/ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgHdlr.scala @@ -10,7 +10,7 @@ trait ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgHdlr { def handle(msg: ScreenshareRtmpBroadcastStartedVoiceConfEvtMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = { def broadcastEvent(voiceConf: String, screenshareConf: String, stream: String, vidWidth: Int, vidHeight: Int, - timestamp: String): BbbCommonEnvCoreMsg = { + timestamp: String, hasAudio: Boolean): BbbCommonEnvCoreMsg = { val routing = Routing.addMsgToClientRouting( MessageTypes.BROADCAST_TO_MEETING, @@ -23,7 +23,7 @@ trait ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgHdlr { ) val body = ScreenshareRtmpBroadcastStartedEvtMsgBody(voiceConf, screenshareConf, - stream, vidWidth, vidHeight, timestamp) + stream, vidWidth, vidHeight, timestamp, hasAudio) val event = ScreenshareRtmpBroadcastStartedEvtMsg(header, body) BbbCommonEnvCoreMsg(envelope, event) } @@ -41,12 +41,13 @@ trait ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgHdlr { ScreenshareModel.setVoiceConf(liveMeeting.screenshareModel, msg.body.voiceConf) ScreenshareModel.setScreenshareConf(liveMeeting.screenshareModel, msg.body.screenshareConf) ScreenshareModel.setTimestamp(liveMeeting.screenshareModel, msg.body.timestamp) + ScreenshareModel.setHasAudio(liveMeeting.screenshareModel, msg.body.hasAudio) log.info("START broadcast ALLOWED when isBroadcastingRTMP=false") // Notify viewers in the meeting that there's an rtmp stream to view val msgEvent = broadcastEvent(msg.body.voiceConf, msg.body.screenshareConf, msg.body.stream, - msg.body.vidWidth, msg.body.vidHeight, msg.body.timestamp) + msg.body.vidWidth, msg.body.vidHeight, msg.body.timestamp, msg.body.hasAudio) bus.outGW.send(msgEvent) } else { log.info("START broadcast NOT ALLOWED when isBroadcastingRTMP=true") diff --git a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/FreeswitchConferenceEventListener.java b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/FreeswitchConferenceEventListener.java index f9327ec6adb7523344d8000cca9348fa63675284..0242b524e0501582e6007c0b506dfea901d46a4c 100755 --- a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/FreeswitchConferenceEventListener.java +++ b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/FreeswitchConferenceEventListener.java @@ -83,7 +83,7 @@ public class FreeswitchConferenceEventListener implements ConferenceEventListene if (((ScreenshareRTMPBroadcastEvent) event).getBroadcast()) { ScreenshareRTMPBroadcastEvent evt = (ScreenshareRTMPBroadcastEvent) event; vcs.deskShareRTMPBroadcastStarted(evt.getRoom(), evt.getBroadcastingStreamUrl(), - evt.getVideoWidth(), evt.getVideoHeight(), evt.getTimestamp()); + evt.getVideoWidth(), evt.getVideoHeight(), evt.getTimestamp(), evt.getHasAudio()); } else { ScreenshareRTMPBroadcastEvent evt = (ScreenshareRTMPBroadcastEvent) event; vcs.deskShareRTMPBroadcastStopped(evt.getRoom(), evt.getBroadcastingStreamUrl(), diff --git a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/IVoiceConferenceService.java b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/IVoiceConferenceService.java index 77012629d24f1fdd011d28eca0a1b40adde74b18..c60c26984314f2e73e84728a76116adccd6bda1a 100755 --- a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/IVoiceConferenceService.java +++ b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/IVoiceConferenceService.java @@ -55,7 +55,8 @@ public interface IVoiceConferenceService { String streamname, Integer videoWidth, Integer videoHeight, - String timestamp); + String timestamp, + boolean hasAudio); void deskShareRTMPBroadcastStopped(String room, String streamname, diff --git a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/events/ScreenshareRTMPBroadcastEvent.java b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/events/ScreenshareRTMPBroadcastEvent.java index ea8c826888d50921df2be86b7ed860de6c660b84..814a64ff1dce62f5b1910690a6ded0b2047bc557 100755 --- a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/events/ScreenshareRTMPBroadcastEvent.java +++ b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/events/ScreenshareRTMPBroadcastEvent.java @@ -25,6 +25,7 @@ public class ScreenshareRTMPBroadcastEvent extends VoiceConferenceEvent { private String streamUrl; private Integer vw; private Integer vh; + private boolean hasAudio; private final String SCREENSHARE_SUFFIX = "-SCREENSHARE"; @@ -46,6 +47,10 @@ public class ScreenshareRTMPBroadcastEvent extends VoiceConferenceEvent { public void setVideoHeight(Integer vh) {this.vh = vh;} + public void setHasAudio(boolean hasAudio) { + this.hasAudio = hasAudio; + } + public Integer getVideoHeight() {return vh;} public Integer getVideoWidth() {return vw;} @@ -65,4 +70,8 @@ public class ScreenshareRTMPBroadcastEvent extends VoiceConferenceEvent { public boolean getBroadcast() { return broadcast; } + + public boolean getHasAudio() { + return hasAudio; + } } diff --git a/akka-bbb-fsesl/src/main/scala/org/bigbluebutton/freeswitch/VoiceConferenceService.scala b/akka-bbb-fsesl/src/main/scala/org/bigbluebutton/freeswitch/VoiceConferenceService.scala index 7d63c4f1167e9a93bd83fb5330519507c9c3c8bf..c6da363f8c92b878ec8fa31ca50536ac18f5a703 100755 --- a/akka-bbb-fsesl/src/main/scala/org/bigbluebutton/freeswitch/VoiceConferenceService.scala +++ b/akka-bbb-fsesl/src/main/scala/org/bigbluebutton/freeswitch/VoiceConferenceService.scala @@ -237,13 +237,14 @@ class VoiceConferenceService(healthz: HealthzService, streamname: String, vw: java.lang.Integer, vh: java.lang.Integer, - timestamp: String + timestamp: String, + hasAudio: Boolean ) { val header = BbbCoreVoiceConfHeader(ScreenshareRtmpBroadcastStartedVoiceConfEvtMsg.NAME, voiceConfId) val body = ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgBody(voiceConf = voiceConfId, screenshareConf = voiceConfId, stream = streamname, vidWidth = vw.intValue(), vidHeight = vh.intValue(), - timestamp) + timestamp, hasAudio) val envelope = BbbCoreEnvelope(ScreenshareRtmpBroadcastStartedVoiceConfEvtMsg.NAME, Map("voiceConf" -> voiceConfId)) val msg = new ScreenshareRtmpBroadcastStartedVoiceConfEvtMsg(header, body) diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/VoiceConfMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/VoiceConfMsgs.scala index 9916ffddebc43d0b5e9dfe2b2e309e15d9942bed..a6d15a7ffa19d4d5a6b6ca3af0d3bcb8a71e3f77 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/VoiceConfMsgs.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/VoiceConfMsgs.scala @@ -24,7 +24,7 @@ case class ScreenshareRtmpBroadcastStartedVoiceConfEvtMsg( extends VoiceStandardMsg case class ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgBody(voiceConf: String, screenshareConf: String, stream: String, vidWidth: Int, vidHeight: Int, - timestamp: String) + timestamp: String, hasAudio: Boolean) /** * Sent to clients to notify them of an RTMP stream starting. @@ -37,7 +37,7 @@ case class ScreenshareRtmpBroadcastStartedEvtMsg( extends BbbCoreMsg case class ScreenshareRtmpBroadcastStartedEvtMsgBody(voiceConf: String, screenshareConf: String, stream: String, vidWidth: Int, vidHeight: Int, - timestamp: String) + timestamp: String, hasAudio: Boolean) /** * Send by FS that RTMP stream has stopped. diff --git a/bigbluebutton-html5/client/main.html b/bigbluebutton-html5/client/main.html index 00c5bc98cf2565ab43440cb1b4b7cc36ed50cda1..09a8a2af0533d7c69ca501fa0239f3d642bf720e 100755 --- a/bigbluebutton-html5/client/main.html +++ b/bigbluebutton-html5/client/main.html @@ -82,7 +82,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/errors.js b/bigbluebutton-html5/imports/api/screenshare/client/bridge/errors.js new file mode 100644 index 0000000000000000000000000000000000000000..0adbe86643ab70df1118d1daa744ba048faf2a66 --- /dev/null +++ b/bigbluebutton-html5/imports/api/screenshare/client/bridge/errors.js @@ -0,0 +1,61 @@ +import { + SFU_CLIENT_SIDE_ERRORS, + SFU_SERVER_SIDE_ERRORS +} from '/imports/ui/services/bbb-webrtc-sfu/broker-base-errors'; + +// Mapped getDisplayMedia errors. These are bridge agnostic +// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia +const GDM_ERRORS = { + // Fallback error: 1130 + 1130: 'GetDisplayMediaGenericError', + 1131: 'AbortError', + 1132: 'InvalidStateError', + 1133: 'OverconstrainedError', + 1134: 'TypeError', + 1135: 'NotFoundError', + 1136: 'NotAllowedError', + 1137: 'NotSupportedError', + 1138: 'NotReadableError', +}; + +// Import as many bridge specific errors you want in this utilitary and shove +// them into the error class slots down below. +const CLIENT_SIDE_ERRORS = { + 1101: "SIGNALLING_TRANSPORT_DISCONNECTED", + 1102: "SIGNALLING_TRANSPORT_CONNECTION_FAILED", + 1104: "SCREENSHARE_PLAY_FAILED", + 1105: "PEER_NEGOTIATION_FAILED", + 1107: "ICE_STATE_FAILED", + 1120: "MEDIA_TIMEOUT", + 1121: "UNKNOWN_ERROR", +}; + +const SERVER_SIDE_ERRORS = { + ...SFU_SERVER_SIDE_ERRORS, +} + +const AGGREGATED_ERRORS = { + ...CLIENT_SIDE_ERRORS, + ...SERVER_SIDE_ERRORS, + ...GDM_ERRORS, +} + +const expandErrors = () => { + const expandedErrors = Object.keys(AGGREGATED_ERRORS).reduce((map, key) => { + map[AGGREGATED_ERRORS[key]] = { errorCode: key, errorMessage: AGGREGATED_ERRORS[key] }; + return map; + }, {}); + + return { ...AGGREGATED_ERRORS, ...expandedErrors }; +} + +const SCREENSHARING_ERRORS = expandErrors(); + +export { + GDM_ERRORS, + BRIDGE_SERVER_SIDE_ERRORS, + BRIDGE_CLIENT_SIDE_ERRORS, + // All errors, [code]: [message] + // Expanded errors. It's AGGREGATED + message: { errorCode, errorMessage } + SCREENSHARING_ERRORS, +} diff --git a/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js b/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js index d3ffdff3b8e00d297ec3c56c6a65360f9b843482..d16adb4c2c6bf202a5d6b23a4e40d498ee76440d 100755 --- a/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js +++ b/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js @@ -1,236 +1,297 @@ 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, screenShareEndAlert } from '/imports/ui/components/screenshare/service'; +import { SCREENSHARING_ERRORS } from './errors'; 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 SCREENSHARE_VIDEO_TAG = 'screenshareVideo'; -const CHROME_EXTENSION_KEY = CHROME_CUSTOM_EXTENSION_KEY === 'KEY' ? CHROME_DEFAULT_EXTENSION_KEY : CHROME_CUSTOM_EXTENSION_KEY; +const BRIDGE_NAME = 'kurento' +const SCREENSHARE_VIDEO_TAG = 'screenshareVideo'; +const SEND_ROLE = 'send'; +const RECV_ROLE = 'recv'; -const getUserId = () => Auth.userID; +// the error-code mapping is bridge specific; that's why it's not in the errors util +const ERROR_MAP = { + 1301: SCREENSHARING_ERRORS.SIGNALLING_TRANSPORT_DISCONNECTED, + 1302: SCREENSHARING_ERRORS.SIGNALLING_TRANSPORT_CONNECTION_FAILED, + 1305: SCREENSHARING_ERRORS.PEER_NEGOTIATION_FAILED, + 1307: SCREENSHARING_ERRORS.ICE_STATE_FAILED, +} -const getMeetingId = () => Auth.meetingID; +const mapErrorCode = (error) => { + const { errorCode } = error; + const mappedError = ERROR_MAP[errorCode]; -const getUsername = () => Auth.fullname; + if (errorCode == null || mappedError == null) return error; + error.errorCode = mappedError.errorCode; + error.errorMessage = mappedError.errorMessage; + error.message = mappedError.errorMessage; -const getSessionToken = () => Auth.sessionToken; + return error; +} 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'; + constructor() { + this.role; + this.broker; + this._gdmStream; + this.hasAudio = false; + this.connectionAttempts = 0; + this.reconnecting = false; + this.reconnectionTimeout; + this.restartIntervalMs = BridgeService.BASE_MEDIA_TIMEOUT; + } + + get gdmStream() { + return this._gdmStream; + } + + set gdmStream(stream) { + this._gdmStream = stream; + } + + outboundStreamReconnect() { + const currentRestartIntervalMs = this.restartIntervalMs; + const stream = this.gdmStream; + + logger.warn({ + logCode: 'screenshare_presenter_reconnect', + extraInfo: { + reconnecting: this.reconnecting, + role: this.role, + bridge: BRIDGE_NAME + }, + }, `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: { + errorCode: error.errorCode, + 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}`); + inboundStreamReconnect() { + const currentRestartIntervalMs = this.restartIntervalMs; + + logger.warn({ + logCode: 'screenshare_viewer_reconnect', + extraInfo: { + reconnecting: this.reconnecting, + role: this.role, + bridge: BRIDGE_NAME + }, + }, `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(this.hasAudio).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: { + errorCode: error.errorCode, + errorMessage: error.errorMessage, + reconnecting: this.reconnecting, + role: this.role, + bridge: BRIDGE_NAME + }, + }, 'Screensharing reconnect failed'); + }); + } + + 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 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}`); + maxConnectionAttemptsReached () { + return this.connectionAttempts > BridgeService.MAX_CONN_ATTEMPTS; + } + + scheduleReconnect () { + if (this.reconnectionTimeout == null) { + this.reconnectionTimeout = setTimeout( + this.handleConnectionTimeoutExpiry.bind(this), + this.restartIntervalMs + ); } - return normalizedError; } - static playElement(screenshareMediaElement) { - const mediaTagPlayed = () => { - logger.info({ - logCode: 'screenshare_media_play_success', - }, 'Screenshare media played successfully'); - }; + clearReconnectionTimeout () { + this.reconnecting = false; + this.restartIntervalMs = BridgeService.BASE_MEDIA_TIMEOUT; - 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(); + if (this.reconnectionTimeout) { + clearTimeout(this.reconnectionTimeout); + this.reconnectionTimeout = null; } } - static screenshareElementLoadAndPlay(stream, element, muted) { - element.muted = muted; - element.pause(); - element.srcObject = stream; - KurentoScreenshareBridge.playElement(element); + handleViewerStart() { + const mediaElement = document.getElementById(SCREENSHARE_VIDEO_TAG); + + if (mediaElement && this.broker && this.broker.webRtcPeer) { + const stream = this.broker.webRtcPeer.getRemoteStream(); + BridgeService.screenshareLoadAndPlayMediaStream(stream, mediaElement, !this.broker.hasAudio); + } + + this.clearReconnectionTimeout(); } - kurentoViewLocalPreview() { - const screenshareMediaElement = document.getElementById(SCREENSHARE_VIDEO_TAG); - const { webRtcPeer } = window.kurentoManager.kurentoScreenshare; + handleBrokerFailure(error) { + mapErrorCode(error); + const { errorMessage, errorCode } = error; + + logger.error({ + logCode: 'screenshare_broker_failure', + extraInfo: { + errorCode, errorMessage, + role: this.broker.role, + started: this.broker.started, + reconnecting: this.reconnecting, + bridge: BRIDGE_NAME + }, + }, 'Screenshare broker failure'); - if (webRtcPeer) { - const stream = webRtcPeer.getLocalStream(); - KurentoScreenshareBridge.screenshareElementLoadAndPlay(stream, screenshareMediaElement, true); + // 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(); } + + return error; } - async kurentoViewScreen() { - 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(), - }; + async view(hasAudio = false) { + this.hasAudio = hasAudio; + this.role = RECV_ROLE; + const iceServers = await BridgeService.getIceServers(Auth.sessionToken); + const options = { + iceServers, + userName: Auth.fullname, + hasAudio, + }; - const onFail = (error) => { - KurentoScreenshareBridge.handleViewerFailure(error, started); - }; + this.broker = new ScreenshareBroker( + Auth.authenticateURL(SFU_URL), + BridgeService.getConferenceBridge(), + Auth.userID, + Auth.meetingID, + this.role, + options, + ); - // 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, - true, - ); - } - }; + this.broker.onstart = this.handleViewerStart.bind(this); + this.broker.onerror = this.handleBrokerFailure.bind(this); + this.broker.onended = this.handleEnded.bind(this); - window.kurentoWatchVideo( - SCREENSHARE_VIDEO_TAG, - BridgeService.getConferenceBridge(), - getUserId(), - getMeetingId(), - onFail, - onSuccess, - options, - ); - } + return this.broker.view().finally(this.scheduleReconnect.bind(this)); } - kurentoExitVideo() { - window.kurentoExitVideo(); + handlePresenterStart() { + logger.info({ + logCode: 'screenshare_presenter_start_success', + }, 'Screenshare presenter started succesfully'); + this.clearReconnectionTimeout(); + this.reconnecting = false; + this.connectionAttempts = 0; } - async kurentoShareScreen(onFail, stream) { - let iceServers = []; - try { - iceServers = await fetchWebRTCMappedStunTurnServers(getSessionToken()); - } catch (error) { - logger.error({ logCode: 'screenshare_presenter_fetchstunturninfo_error' }, + handleEnded() { + screenShareEndAlert(); + } - 'Screenshare bridge failed to fetch STUN/TURN info, using default'); - iceServers = getMappedFallbackStun(); - } finally { - const options = { - wsUrl: Auth.authenticateURL(SFU_URL), - chromeExtension: CHROME_EXTENSION_KEY, - chromeScreenshareSources: CHROME_SCREENSHARE_SOURCES, - firefoxScreenshareSource: FIREFOX_SCREENSHARE_SOURCE, - iceServers, - logger, - userName: getUsername(), - }; + share(stream, onFailure) { + return new Promise(async (resolve, reject) => { + this.onerror = onFailure; + this.connectionAttempts += 1; + this.role = SEND_ROLE; + this.hasAudio = BridgeService.streamHasAudioTrack(stream); + this.gdmStream = stream; - let started = false; + const onerror = (error) => { + const normalizedError = this.handleBrokerFailure(error); + if (this.maxConnectionAttemptsReached()) { + this.clearReconnectionTimeout(); + this.connectionAttempts = 0; + onFailure(SCREENSHARING_ERRORS.MEDIA_TIMEOUT); - const failureCallback = (error) => { - const normalizedError = KurentoScreenshareBridge.handlePresenterFailure(error, started); - onFail(normalizedError); + return reject(SCREENSHARING_ERRORS.MEDIA_TIMEOUT); + } }; - const successCallback = () => { - started = true; - logger.info({ - logCode: 'screenshare_presenter_start_success', - }, 'Screenshare presenter started succesfully'); + const iceServers = await BridgeService.getIceServers(Auth.sessionToken); + const options = { + iceServers, + userName: Auth.fullname, + stream, + hasAudio: this.hasAudio, }; - options.stream = stream || undefined; - - window.kurentoShareScreen( - SCREENSHARE_VIDEO_TAG, + this.broker = new ScreenshareBroker( + Auth.authenticateURL(SFU_URL), BridgeService.getConferenceBridge(), - getUserId(), - getMeetingId(), - failureCallback, - successCallback, + Auth.userID, + Auth.meetingID, + this.role, options, ); - } - } - kurentoExitScreenShare() { - window.kurentoExitScreenShare(); + this.broker.onerror = onerror.bind(this); + this.broker.onstreamended = this.stop.bind(this); + this.broker.onstart = this.handlePresenterStart.bind(this); + this.broker.onended = this.handleEnded.bind(this); + + this.broker.share().then(() => { + this.scheduleReconnect(); + return resolve(); + }).catch(reject); + }); + }; + + 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 a8c0c901752b450d98d222cd285c6b4401d441af..202fc80a05f3d3a0d3665793f427540c29bf663d 100644 --- a/bigbluebutton-html5/imports/api/screenshare/client/bridge/service.js +++ b/bigbluebutton-html5/imports/api/screenshare/client/bridge/service.js @@ -1,37 +1,66 @@ 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'; +import { SCREENSHARING_ERRORS } from './errors'; 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' +const HAS_DISPLAY_MEDIA = (typeof navigator.getDisplayMedia === 'function' || (navigator.mediaDevices && typeof navigator.mediaDevices.getDisplayMedia === 'function')); const getConferenceBridge = () => Meetings.findOne().voiceProp.voiceConf; +const normalizeGetDisplayMediaError = (error) => { + return SCREENSHARING_ERRORS[error.name] || SCREENSHARING_ERRORS.GetDisplayMediaGenericError; +}; + +const getBoundGDM = () => { + if (typeof navigator.getDisplayMedia === 'function') { + return navigator.getDisplayMedia.bind(navigator); + } else if (navigator.mediaDevices && typeof navigator.mediaDevices.getDisplayMedia === 'function') { + return navigator.mediaDevices.getDisplayMedia.bind(navigator.mediaDevices); + } +} + 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(SCREENSHARING_ERRORS.NotSupportedError); + } + if (typeof stream.getVideoTracks === 'function' - && typeof constraints.video === 'object') { - stream.getVideoTracks().forEach((track) => { - if (typeof track.applyConstraints === 'function') { - track.applyConstraints(constraints.video).catch((error) => { + && typeof GDM_CONSTRAINTS.video === 'object') { + stream.getVideoTracks().forEach(track => { + if (typeof track.applyConstraints === 'function') { + track.applyConstraints(GDM_CONSTRAINTS.video).catch(error => { logger.warn({ 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') { - stream.getAudioTracks().forEach((track) => { - if (typeof track.applyConstraints === 'function') { - track.applyConstraints(constraints.audio).catch((error) => { + && typeof GDM_CONSTRAINTS.audio === 'object') { + stream.getAudioTracks().forEach(track => { + if (typeof track.applyConstraints === 'function') { + track.applyConstraints(GDM_CONSTRAINTS.audio).catch(error => { logger.warn({ logCode: 'screenshare_audioconstraint_failed', extraInfo: { errorName: error.name, errorCode: error.code }, @@ -44,39 +73,81 @@ const getScreenStream = async () => { return Promise.resolve(stream); }; - const constraints = hasDisplayMedia ? GDM_CONSTRAINTS : null; + const getDisplayMedia = getBoundGDM(); - // 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(); - } - if (typeof navigator.getDisplayMedia === 'function') { - return navigator.getDisplayMedia(constraints) + if (typeof getDisplayMedia === 'function') { + return getDisplayMedia(GDM_CONSTRAINTS) .then(gDMCallback) - .catch((error) => { + .catch(error => { + const normalizedError = normalizeGetDisplayMediaError(error); logger.error({ logCode: 'screenshare_getdisplaymedia_failed', - extraInfo: { errorName: error.name, errorCode: error.code }, + extraInfo: { errorCode: normalizedError.errorCode, errorMessage: normalizedError.errorMessage }, }, 'getDisplayMedia call failed'); - return Promise.resolve(); - }); - } if (navigator.mediaDevices && typeof navigator.mediaDevices.getDisplayMedia === 'function') { - return navigator.mediaDevices.getDisplayMedia(constraints) - .then(gDMCallback) - .catch((error) => { - logger.error({ - logCode: 'screenshare_getdisplaymedia_failed', - extraInfo: { errorName: error.name, errorCode: error.code }, - }, 'getDisplayMedia call failed'); - return Promise.resolve(); + return Promise.reject(normalizedError); }); + } else { + // getDisplayMedia isn't supported, error its way out + return Promise.reject(SCREENSHARING_ERRORS.NotSupportedError); } }; +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 { + throw { + errorCode: SCREENSHARING_ERRORS.SCREENSHARE_PLAY_FAILED.errorCode, + errorMessage: error.message || SCREENSHARING_ERRORS.SCREENSHARE_PLAY_FAILED.errorMessage, + }; + } + }); +} export default { - hasDisplayMedia, + HAS_DISPLAY_MEDIA, getConferenceBridge, getScreenStream, + 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 843e615f84c6e3b7199ac4fec54f0bda48bda949..68331d5dba1ed931663eba8e8878128343a21cc9 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/container'; +import ScreenshareButtonContainer from '/imports/ui/components/actions-bar/screenshare/container'; import AudioControlsContainer from '../audio/audio-controls/container'; import JoinVideoOptionsContainer from '../video-provider/video-button/container'; import CaptionsButtonContainer from '/imports/ui/components/actions-bar/captions/container'; @@ -13,20 +13,14 @@ class ActionsBar extends PureComponent { render() { const { amIPresenter, - handleShareScreen, - handleUnshareScreen, - isVideoBroadcasting, amIModerator, - screenSharingCheck, enableVideo, isLayoutSwapped, toggleSwapLayout, handleTakePresenter, intl, isSharingVideo, - screenShareEndAlert, stopExternalVideoShare, - screenshareDataSavingSetting, isCaptionsAvailable, isMeteorConnected, isPollingEnabled, @@ -75,15 +69,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 b5dad9d6c3318cc9fe50191075e71375af7d0273..5893a0aeb4a9b9ed4fd211b6a27593248b0de255 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, @@ -31,10 +27,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, @@ -42,8 +34,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/desktop-share/component.jsx deleted file mode 100755 index 498f0d50e31dfd6260df0404aac21881db6bc3dd..0000000000000000000000000000000000000000 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/desktop-share/component.jsx +++ /dev/null @@ -1,209 +0,0 @@ -import React, { memo } from 'react'; -import PropTypes from 'prop-types'; -import { defineMessages, injectIntl } from 'react-intl'; -import browser from 'browser-detect'; -import Button from '/imports/ui/components/button/component'; -import logger from '/imports/startup/client/logger'; -import { notify } from '/imports/ui/services/notification'; -import cx from 'classnames'; -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'; - -const propTypes = { - intl: PropTypes.object.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, -}; - -const intlMessages = defineMessages({ - desktopShareLabel: { - id: 'app.actionsBar.actionsDropdown.desktopShareLabel', - description: 'Desktop Share option label', - }, - lockedDesktopShareLabel: { - id: 'app.actionsBar.actionsDropdown.lockedDesktopShareLabel', - description: 'Desktop locked Share option label', - }, - stopDesktopShareLabel: { - id: 'app.actionsBar.actionsDropdown.stopDesktopShareLabel', - description: 'Stop Desktop Share option label', - }, - desktopShareDesc: { - id: 'app.actionsBar.actionsDropdown.desktopShareDesc', - description: 'adds context to desktop share option', - }, - stopDesktopShareDesc: { - id: 'app.actionsBar.actionsDropdown.stopDesktopShareDesc', - description: 'adds context to stop desktop share option', - }, - genericError: { - id: 'app.screenshare.genericError', - description: 'error message for when screensharing fails with unknown error', - }, - NotAllowedError: { - id: 'app.screenshare.notAllowed', - description: 'error message when screen access was not granted', - }, - NotSupportedError: { - id: 'app.screenshare.notSupportedError', - description: 'error message when trying to share screen in unsafe environments', - }, - screenShareNotSupported: { - id: 'app.media.screenshare.notSupported', - descriptions: 'error message when trying share screen on unsupported browsers', - }, - screenShareUnavailable: { - id: 'app.media.screenshare.unavailable', - descriptions: 'title for unavailable screen share modal', - }, - NotReadableError: { - id: 'app.screenshare.notReadableError', - description: 'error message when the browser failed to capture the screen', - }, - 1108: { - id: 'app.deskshare.iceConnectionStateError', - description: 'Error message for ice connection state failure', - }, - 2000: { - id: 'app.sfu.mediaServerConnectionError2000', - description: 'Error message fired when the SFU cannot connect to the media server', - }, - 2001: { - id: 'app.sfu.mediaServerOffline2001', - description: 'error message when SFU is offline', - }, - 2002: { - id: 'app.sfu.mediaServerNoResources2002', - description: 'Error message fired when the media server lacks disk, CPU or FDs', - }, - 2003: { - id: 'app.sfu.mediaServerRequestTimeout2003', - description: 'Error message fired when requests are timing out due to lack of resources', - }, - 2021: { - id: 'app.sfu.serverIceGatheringFailed2021', - description: 'Error message fired when the server cannot enact ICE gathering', - }, - 2022: { - id: 'app.sfu.serverIceStateFailed2022', - description: 'Error message fired when the server endpoint transitioned to a FAILED ICE state', - }, - 2200: { - id: 'app.sfu.mediaGenericError2200', - description: 'Error message fired when the SFU component generated a generic error', - }, - 2202: { - id: 'app.sfu.invalidSdp2202', - description: 'Error message fired when the clients provides an invalid SDP', - }, - 2203: { - id: 'app.sfu.noAvailableCodec2203', - description: 'Error message fired when the server has no available codec for the client', - }, -}); - -const BROWSER_RESULTS = browser(); -const isMobileBrowser = (BROWSER_RESULTS ? BROWSER_RESULTS.mobile : false) - || (BROWSER_RESULTS && BROWSER_RESULTS.os - ? BROWSER_RESULTS.os.includes('Android') // mobile flag doesn't always work - : false); -const IS_SAFARI = BROWSER_RESULTS.name === 'safari'; - -const DesktopShare = ({ - intl, - handleShareScreen, - handleUnshareScreen, - isVideoBroadcasting, - amIPresenter, - screenSharingCheck, - screenShareEndAlert, - isMeteorConnected, - screenshareDataSavingSetting, - mountModal, -}) => { - // This is the failure callback that will be passed to the /api/screenshare/kurento.js - // script on the presenter's call - const onFail = (normalizedError) => { - const { errorCode, errorMessage, errorReason } = normalizedError; - 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(); - notify(intl.formatMessage(intlMessages[error]), 'error', 'desktop'); - } else { - // Unmapped error. Log it (so we can infer what's going on), close screenSharing - // session and display generic error message - logger.error({ - logCode: 'screenshare_default_error', - extraInfo: { - errorCode, errorMessage, errorReason, - }, - }, 'Default error handler for screenshare'); - window.kurentoExitScreenShare(); - notify(intl.formatMessage(intlMessages.genericError), 'error', 'desktop'); - } - // Don't trigger the screen share end alert if presenter click to cancel on screen share dialog - if (error !== 'NotAllowedError') { - screenShareEndAlert(); - } - }; - - const screenshareLocked = screenshareDataSavingSetting - ? intlMessages.desktopShareLabel : intlMessages.lockedDesktopShareLabel; - - const vLabel = isVideoBroadcasting - ? intlMessages.stopDesktopShareLabel : screenshareLocked; - - const vDescr = isVideoBroadcasting - ? intlMessages.stopDesktopShareDesc : intlMessages.desktopShareDesc; - - const shouldAllowScreensharing = screenSharingCheck - && !isMobileBrowser - && amIPresenter; - - return shouldAllowScreensharing - ? ( - <Button - className={cx(isVideoBroadcasting || styles.btn)} - disabled={(!isMeteorConnected && !isVideoBroadcasting) || !screenshareDataSavingSetting} - icon={isVideoBroadcasting ? 'desktop' : 'desktop_off'} - label={intl.formatMessage(vLabel)} - description={intl.formatMessage(vDescr)} - color={isVideoBroadcasting ? 'primary' : 'default'} - ghost={!isVideoBroadcasting} - 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>); - } - handleShareScreen(onFail); - } - } - id={isVideoBroadcasting ? 'unshare-screen-button' : 'share-screen-button'} - /> - ) : null; -}; - -DesktopShare.propTypes = propTypes; -export default withModalMounter(injectIntl(memo(DesktopShare))); diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/screenshare/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/screenshare/component.jsx new file mode 100755 index 0000000000000000000000000000000000000000..64dc749da3acd1d22649ee19fe1e38035063c73f --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/screenshare/component.jsx @@ -0,0 +1,209 @@ +import React, { memo } from 'react'; +import PropTypes from 'prop-types'; +import { defineMessages, injectIntl } from 'react-intl'; +import browser from 'browser-detect'; +import Button from '/imports/ui/components/button/component'; +import logger from '/imports/startup/client/logger'; +import { notify } from '/imports/ui/services/notification'; +import cx from 'classnames'; +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'; +import { SCREENSHARING_ERRORS } from '/imports/api/screenshare/client/bridge/errors'; + +const BROWSER_RESULTS = browser(); +const isMobileBrowser = (BROWSER_RESULTS ? BROWSER_RESULTS.mobile : false) + || (BROWSER_RESULTS && BROWSER_RESULTS.os + ? BROWSER_RESULTS.os.includes('Android') // mobile flag doesn't always work + : false); +const IS_SAFARI = BROWSER_RESULTS.name === 'safari'; + +const propTypes = { + intl: PropTypes.objectOf(Object).isRequired, + enabled: PropTypes.bool.isRequired, + amIPresenter: PropTypes.bool.isRequired, + isVideoBroadcasting: PropTypes.bool.isRequired, + isMeteorConnected: PropTypes.bool.isRequired, + screenshareDataSavingSetting: PropTypes.bool.isRequired, +}; + +const intlMessages = defineMessages({ + desktopShareLabel: { + id: 'app.actionsBar.actionsDropdown.desktopShareLabel', + description: 'Desktop Share option label', + }, + lockedDesktopShareLabel: { + id: 'app.actionsBar.actionsDropdown.lockedDesktopShareLabel', + description: 'Desktop locked Share option label', + }, + stopDesktopShareLabel: { + id: 'app.actionsBar.actionsDropdown.stopDesktopShareLabel', + description: 'Stop Desktop Share option label', + }, + desktopShareDesc: { + id: 'app.actionsBar.actionsDropdown.desktopShareDesc', + description: 'adds context to desktop share option', + }, + stopDesktopShareDesc: { + id: 'app.actionsBar.actionsDropdown.stopDesktopShareDesc', + description: 'adds context to stop desktop share option', + }, + screenShareNotSupported: { + id: 'app.media.screenshare.notSupported', + descriptions: 'error message when trying share screen on unsupported browsers', + }, + screenShareUnavailable: { + id: 'app.media.screenshare.unavailable', + descriptions: 'title for unavailable screen share modal', + }, + finalError: { + id: 'app.screenshare.screenshareFinalError', + description: 'Screen sharing failures with no recovery procedure', + }, + retryError: { + id: 'app.screenshare.screenshareRetryError', + description: 'Screen sharing failures where a retry is recommended', + }, + retryOtherEnvError: { + id: 'app.screenshare.screenshareRetryOtherEnvError', + description: 'Screen sharing failures where a retry in another environment is recommended', + }, + unsupportedEnvError: { + id: 'app.screenshare.screenshareUnsupportedEnv', + description: 'Screen sharing is not supported, changing browser or device is recommended', + }, + permissionError: { + id: 'app.screenshare.screensharePermissionError', + description: 'Screen sharing failure due to lack of permission', + }, +}); + +const getErrorLocale = (errorCode) => { + switch (errorCode) { + // Denied getDisplayMedia permission error + case SCREENSHARING_ERRORS.NotAllowedError.errorCode: + return intlMessages.permissionError; + // Browser is supposed to be supported, but a browser-related error happening. + // Suggest retrying in another device/browser/env + case SCREENSHARING_ERRORS.AbortError.errorCode: + case SCREENSHARING_ERRORS.InvalidStateError.errorCode: + case SCREENSHARING_ERRORS.OverconstrainedError.errorCode: + case SCREENSHARING_ERRORS.TypeError.errorCode: + case SCREENSHARING_ERRORS.NotFoundError.errorCode: + case SCREENSHARING_ERRORS.NotReadableError.errorCode: + case SCREENSHARING_ERRORS.PEER_NEGOTIATION_FAILED.errorCode: + case SCREENSHARING_ERRORS.SCREENSHARE_PLAY_FAILED.errorCode: + case SCREENSHARING_ERRORS.MEDIA_NO_AVAILABLE_CODEC.errorCode: + case SCREENSHARING_ERRORS.MEDIA_INVALID_SDP.errorCode: + return intlMessages.retryOtherEnvError; + // Fatal errors where a retry isn't warranted. This probably means the server + // is misconfigured somehow or the provider is utterly botched, so nothing + // the end user can do besides requesting support + case SCREENSHARING_ERRORS.SIGNALLING_TRANSPORT_CONNECTION_FAILED.errorCode: + case SCREENSHARING_ERRORS.MEDIA_SERVER_CONNECTION_ERROR.errorCode: + case SCREENSHARING_ERRORS.SFU_INVALID_REQUEST.errorCode: + return intlMessages.finalError; + // Unsupported errors + case SCREENSHARING_ERRORS.NotSupportedError.errorCode: + return intlMessages.unsupportedEnvError; + // Fall through: everything else is an error which might be solved with a retry + default: + return intlMessages.retryError; + } +} + +const ScreenshareButton = ({ + intl, + enabled, + isVideoBroadcasting, + amIPresenter, + isMeteorConnected, + screenshareDataSavingSetting, + mountModal, +}) => { + // This is the failure callback that will be passed to the /api/screenshare/kurento.js + // script on the presenter's call + const handleFailure = (error) => { + const { + errorCode = SCREENSHARING_ERRORS.UNKNOWN_ERROR.errorCode, + errorMessage + } = error; + + logger.error({ + logCode: 'screenshare_failed', + extraInfo: { errorCode, errorMessage }, + }, 'Screenshare failed'); + + const localizedError = getErrorLocale(errorCode); + notify(intl.formatMessage(localizedError, { 0: errorCode }), 'error', 'desktop'); + screenshareHasEnded(); + }; + + 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; + + const vLabel = isVideoBroadcasting + ? intlMessages.stopDesktopShareLabel : screenshareLocked; + + const vDescr = isVideoBroadcasting + ? intlMessages.stopDesktopShareDesc : intlMessages.desktopShareDesc; + + const shouldAllowScreensharing = enabled + && !isMobileBrowser + && amIPresenter; + + return shouldAllowScreensharing + ? ( + <Button + className={cx(isVideoBroadcasting || styles.btn)} + disabled={(!isMeteorConnected && !isVideoBroadcasting) || !screenshareDataSavingSetting} + icon={isVideoBroadcasting ? 'desktop' : 'desktop_off'} + label={intl.formatMessage(vLabel)} + description={intl.formatMessage(vDescr)} + color={isVideoBroadcasting ? 'primary' : 'default'} + ghost={!isVideoBroadcasting} + hideLabel + circle + size="lg" + onClick={isVideoBroadcasting + ? screenshareHasEnded + : () => { + if (IS_SAFARI && !ScreenshareBridgeService.HAS_DISPLAY_MEDIA) { + renderScreenshareUnavailableModal(); + } else { + shareScreen(handleFailure); + } + } + } + id={isVideoBroadcasting ? 'unshare-screen-button' : 'share-screen-button'} + /> + ) : null; +}; + +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/breakout-join-confirmation/component.jsx b/bigbluebutton-html5/imports/ui/components/breakout-join-confirmation/component.jsx index fa3c77ed970a8e681c85a5a1d9faf224e064d227..0a080fa811238ae4417729b00e69409b2ea27aa6 100755 --- a/bigbluebutton-html5/imports/ui/components/breakout-join-confirmation/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/breakout-join-confirmation/component.jsx @@ -6,6 +6,8 @@ import logger from '/imports/startup/client/logger'; import PropTypes from 'prop-types'; import AudioService from '../audio/service'; import VideoService from '../video-provider/service'; +import { screenshareHasEnded } from '/imports/ui/components/screenshare/service'; +import UserListService from '/imports/ui/components/user-list/service'; import { styles } from './styles'; const intlMessages = defineMessages({ @@ -108,7 +110,7 @@ class BreakoutJoinConfirmation extends Component { } VideoService.exitVideo(); - window.kurentoExitScreenShare(); + if (UserListService.amIPresenter()) screenshareHasEnded(); if (url === '') { logger.error({ logCode: 'breakoutjoinconfirmation_redirecting_to_url', diff --git a/bigbluebutton-html5/imports/ui/components/breakout-room/component.jsx b/bigbluebutton-html5/imports/ui/components/breakout-room/component.jsx index 25a0ffae7f8883dfbf56730a7c1d4411750356f5..364c1b45354155fe8106c9bcd0684e98c5d88b3e 100644 --- a/bigbluebutton-html5/imports/ui/components/breakout-room/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/breakout-room/component.jsx @@ -7,6 +7,8 @@ import logger from '/imports/startup/client/logger'; import { styles } from './styles'; import BreakoutRoomContainer from './breakout-remaining-time/container'; import VideoService from '/imports/ui/components/video-provider/service'; +import { screenshareHasEnded } from '/imports/ui/components/screenshare/service'; +import UserListService from '/imports/ui/components/user-list/service'; const intlMessages = defineMessages({ breakoutTitle: { @@ -224,7 +226,7 @@ class BreakoutRoom extends PureComponent { extraInfo: { logType: 'user_action' }, }, 'joining breakout room closed audio in the main room'); VideoService.exitVideo(); - window.kurentoExitScreenShare(); + if (UserListService.amIPresenter()) screenshareHasEnded(); } } disabled={disable} diff --git a/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx b/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx index 49900e02cda1bd65f38708ee062e842321109e3a..5e960f8d65be867d52dd31f4763a3d40af351a8a 100755 --- a/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx @@ -7,9 +7,22 @@ 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 PollingContainer from '/imports/ui/components/polling/container'; import { withLayoutConsumer } from '/imports/ui/components/layout/context'; +import { + SCREENSHARE_MEDIA_ELEMENT_NAME, + screenshareHasEnded, + screenshareHasStarted, + getMediaElement, + attachLocalPreviewStream, +} from '/imports/ui/components/screenshare/service'; +import { + isStreamStateUnhealthy, + subscribeToStreamStateChange, + unsubscribeFromStreamStateChange, +} from '/imports/ui/services/bbb-webrtc-sfu/stream-state-service'; const intlMessages = defineMessages({ screenShareLabel: { @@ -33,49 +46,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()); } componentDidUpdate(prevProps) { const { - isPresenter, unshareScreen, + isPresenter, } = this.props; if (isPresenter && !prevProps.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 = !isStreamStateUnhealthy(streamState); + event.stopPropagation(); + if (newHealthState !== isStreamHealthy) { + this.setState({ isStreamHealthy: newHealthState }); + } } - onVideoLoad() { + onLoadedData() { this.setState({ loaded: true }); } @@ -147,12 +174,35 @@ class ScreenshareComponent extends React.Component { ); } - render() { - const { loaded, autoplayBlocked, isFullscreen } = this.state; + renderAutoplayOverlay() { const { intl } = this.props; return ( - [!loaded + <AutoplayOverlay + key={_.uniqueId('screenshareAutoplayOverlay')} + autoplayBlockedDesc={intl.formatMessage(intlMessages.autoplayBlockedDesc)} + autoplayAllowLabel={intl.formatMessage(intlMessages.autoplayAllowLabel)} + handleAllowAutoplay={this.handleAllowAutoplay} + /> + ); + } + + render() { + const { loaded, autoplayBlocked, isFullscreen, isStreamHealthy } = this.state; + const { intl, isPresenter, isGloballyBroadcasting } = this.props; + + // Conditions to render the (re)connecting spinner and the unhealthy stream + // grayscale: + // 1 - The local media tag has not received any stream data yet + // 2 - The user is a presenter and the stream wasn't globally broadcasted yet + // 3 - The media was loaded, the stream was globally broadcasted BUT the stream + // state transitioned to an unhealthy stream. tl;dr: screen sharing reconnection + const shouldRenderConnectingState = !loaded + || (isPresenter && !isGloballyBroadcasting) + || !isStreamHealthy && loaded && isGloballyBroadcasting; + + return ( + [(shouldRenderConnectingState) ? ( <div key={_.uniqueId('screenshareArea-')} @@ -163,29 +213,28 @@ 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; }} > + {isFullscreen && <PollingContainer />} + {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]: shouldRenderConnectingState, + })} muted /> </div> @@ -199,7 +248,4 @@ export default injectIntl(withLayoutConsumer(ScreenshareComponent)); ScreenshareComponent.propTypes = { intl: PropTypes.object.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..58031bff86741a38b6a5b1a78c8570219686a4b8 100755 --- a/bigbluebutton-html5/imports/ui/components/screenshare/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/screenshare/container.jsx @@ -4,14 +4,13 @@ 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, + isGloballyBroadcasting, } from './service'; import ScreenshareComponent from './component'; const ScreenshareContainer = (props) => { - const { isVideoBroadcasting: isVB } = props; - if (isVB()) { + if (isVideoBroadcasting()) { return <ScreenshareComponent {...props} />; } return null; @@ -20,11 +19,8 @@ const ScreenshareContainer = (props) => { export default withTracker(() => { const user = Users.findOne({ userId: Auth.userID }, { fields: { presenter: 1 } }); return { + isGloballyBroadcasting: isGloballyBroadcasting(), 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 ca9e566d8ceb3b1951b4810e1c789d1d1cf3679d..16c4552d0c3d91571b2b6c2e0ca3cc98a7a01588 100644 --- a/bigbluebutton-html5/imports/ui/components/screenshare/service.js +++ b/bigbluebutton-html5/imports/ui/components/screenshare/service.js @@ -11,65 +11,119 @@ import UserListService from '/imports/ui/components/user-list/service'; import AudioService from '/imports/ui/components/audio/service'; import {Meteor} from "meteor/meteor"; +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(); + } +}; + +// A simplified, trackable version of isVideoBroadcasting that DOES NOT +// account for the presenter's local sharing state. +// It reflects the GLOBAL screen sharing state (akka-apps) +const isGloballyBroadcasting = () => { + const screenshareEntry = Screenshare.findOne({ meetingId: Auth.meetingID }, + { fields: { 'screenshare.stream': 1 } }); + + return (!screenshareEntry ? false : !!screenshareEntry.screenshare.stream); +} + // 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 (screenIsShared && isSharingScreen) { + setSharingScreen(false); + } + + return sharing || screenIsShared; +}; + + +const screenshareHasAudio = () => { + const screenshareEntry = Screenshare.findOne({ meetingId: Auth.meetingID }, + { fields: { 'screenshare.hasAudio': 1 } }); if (!screenshareEntry) { return false; } - return !!screenshareEntry.screenshare.stream; -}; + return !!screenshareEntry.screenshare.hasAudio; +} -// 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 screenshareHasEnded = () => { + if (isSharingScreen()) { + setSharingScreen(false); + } + + KurentoBridge.stop(); }; -const viewScreenshare = () => { - const amIPresenter = UserListService.isUserPresenter(Auth.userID); - if (!amIPresenter) { - KurentoBridge.kurentoViewScreen(); - } else { - KurentoBridge.kurentoViewLocalPreview(); +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); } -}; +} -// if remote screenshare has been started connect and display the video stream -const presenterScreenshareHasStarted = () => { - // WebRTC restrictions may need a capture device permission to release - // useful ICE candidates on recvonly/no-gUM peers - tryGenerateIceCandidates().then(() => { - viewScreenshare(); - }).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. +const screenshareHasStarted = () => { + // Presenter's screen preview is local, so skip + if (!UserListService.amIPresenter()) { viewScreenshare(); - }); + } }; -const shareScreen = (onFail) => { +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 @@ -78,19 +132,19 @@ const screenShareEndAlert = () => AudioService + Meteor.settings.public.app.instanceId}` + '/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, + isGloballyBroadcasting, }; diff --git a/bigbluebutton-html5/imports/ui/components/screenshare/styles.scss b/bigbluebutton-html5/imports/ui/components/screenshare/styles.scss index 6b66e3b16ec28f2f387231d9947e3d6119b03fe8..fa54e17f32c38689d1f5b725120ad617c79f070d 100755 --- a/bigbluebutton-html5/imports/ui/components/screenshare/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/screenshare/styles.scss @@ -2,10 +2,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{ @@ -17,3 +16,7 @@ width: 100%; height: 100%; } + +.unhealthyStream { + filter: grayscale(50%) opacity(50%); +} diff --git a/bigbluebutton-html5/imports/ui/components/user-list/service.js b/bigbluebutton-html5/imports/ui/components/user-list/service.js index 6cfb414c54152e478aa69c209d50e0f18a762c82..43704ec73ab020dd9ff61cea8eb1c520d949cc7c 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/service.js +++ b/bigbluebutton-html5/imports/ui/components/user-list/service.js @@ -578,6 +578,10 @@ const isUserPresenter = (userId) => { return user ? user.presenter : false; }; +const amIPresenter = () => { + return isUserPresenter(Auth.userID); +}; + export const getUserNamesLink = (docTitle, fnSortedLabel, lnSortedLabel) => { const mimeType = 'text/plain'; const userNamesObj = getUsers() @@ -646,5 +650,6 @@ export default { requestUserInformation, focusFirstDropDownItem, isUserPresenter, + amIPresenter, getUsersProp, }; diff --git a/bigbluebutton-html5/imports/ui/services/bbb-webrtc-sfu/broker-base-errors.js b/bigbluebutton-html5/imports/ui/services/bbb-webrtc-sfu/broker-base-errors.js index cf6cc68d47e3dc1a6b3dae8acddb119ddb583f0a..99bd51cce4fb1e9e675221e0e6f375b9b4d8e865 100644 --- a/bigbluebutton-html5/imports/ui/services/bbb-webrtc-sfu/broker-base-errors.js +++ b/bigbluebutton-html5/imports/ui/services/bbb-webrtc-sfu/broker-base-errors.js @@ -1,9 +1,12 @@ -export default SFU_BROKER_ERRORS = { +const SFU_CLIENT_SIDE_ERRORS = { // 13xx errors are client-side bbb-webrtc-sfu's base broker errors 1301: "WEBSOCKET_DISCONNECTED", 1302: "WEBSOCKET_CONNECTION_FAILED", 1305: "PEER_NEGOTIATION_FAILED", 1307: "ICE_STATE_FAILED", +}; + +const SFU_SERVER_SIDE_ERRORS = { // 2xxx codes are server-side bbb-webrtc-sfu errors 2000: "MEDIA_SERVER_CONNECTION_ERROR", 2001: "MEDIA_SERVER_OFFLINE", @@ -22,4 +25,12 @@ export default SFU_BROKER_ERRORS = { 2210: "MEDIA_CONNECT_ERROR", 2211: "MEDIA_NOT_FLOWING", 2300: "SFU_INVALID_REQUEST", -} +}; + +const SFU_BROKER_ERRORS = { ...SFU_SERVER_SIDE_ERRORS, ...SFU_CLIENT_SIDE_ERRORS }; + +export { + SFU_CLIENT_SIDE_ERRORS, + SFU_SERVER_SIDE_ERRORS, + SFU_BROKER_ERRORS, +}; diff --git a/bigbluebutton-html5/imports/ui/services/bbb-webrtc-sfu/screenshare-broker.js b/bigbluebutton-html5/imports/ui/services/bbb-webrtc-sfu/screenshare-broker.js new file mode 100644 index 0000000000000000000000000000000000000000..216ddf780b61be260d86e3e20936181d0596bc60 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/services/bbb-webrtc-sfu/screenshare-broker.js @@ -0,0 +1,230 @@ +import logger from '/imports/startup/client/logger'; +import BaseBroker from '/imports/ui/services/bbb-webrtc-sfu/sfu-base-broker'; + +const ON_ICE_CANDIDATE_MSG = 'iceCandidate'; +const SFU_COMPONENT_NAME = 'screenshare'; + +class ScreenshareBroker extends BaseBroker { + constructor( + wsUrl, + voiceBridge, + userId, + internalMeetingId, + role, + options = {}, + ) { + super(SFU_COMPONENT_NAME, wsUrl); + this.voiceBridge = voiceBridge; + this.userId = userId; + this.internalMeetingId = internalMeetingId; + this.role = role; + this.ws = null; + this.webRtcPeer = null; + this.hasAudio = false; + + // Optional parameters are: userName, caleeName, iceServers, hasAudio + Object.assign(this, options); + } + + onstreamended () { + // To be implemented by instantiators + } + + share () { + return this.openWSConnection() + .then(this.startScreensharing.bind(this)); + } + + view () { + return this.openWSConnection() + .then(this.subscribeToScreenStream.bind(this)); + } + + onWSMessage (message) { + const parsedMessage = JSON.parse(message.data); + + switch (parsedMessage.id) { + case 'startResponse': + this.processAnswer(parsedMessage); + break; + case 'playStart': + this.onstart(); + this.started = true; + break; + case 'stopSharing': + this.stop(); + break; + case 'iceCandidate': + this.handleIceCandidate(parsedMessage.candidate); + break; + case 'error': + this.handleSFUError(parsedMessage); + break; + case 'pong': + break; + default: + logger.debug({ + logCode: `${this.logCodePrefix}_invalid_req`, + extraInfo: { + messageId: parsedMessage.id || 'Unknown', + sfuComponent: this.sfuComponent, + role: this.role, + } + }, `Discarded invalid SFU message`); + } + } + + handleSFUError (sfuResponse) { + const { code, reason } = sfuResponse; + const error = BaseBroker.assembleError(code, reason); + + logger.error({ + logCode: `${this.logCodePrefix}_sfu_error`, + extraInfo: { + errorCode: code, + errorMessage: error.errorMessage, + role: this.role, + sfuComponent: this.sfuComponent, + started: this.started, + }, + }, `Screen sharing failed in SFU`); + this.onerror(error); + } + + onOfferGenerated (error, sdpOffer) { + if (error) { + logger.error({ + logCode: `${this.logCodePrefix}_offer_failure`, + extraInfo: { + errorMessage: error.name || error.message || 'Unknown error', + role: this.role, + sfuComponent: this.sfuComponent + }, + }, `Screenshare offer generation failed`); + // 1305: "PEER_NEGOTIATION_FAILED", + const normalizedError = BaseBroker.assembleError(1305); + return this.onerror(error); + } + + const message = { + id: 'start', + type: this.sfuComponent, + role: this.role, + internalMeetingId: this.internalMeetingId, + voiceBridge: this.voiceBridge, + userName: this.userName, + callerName: this.userId, + sdpOffer, + hasAudio: !!this.hasAudio, + }; + + this.sendMessage(message); + } + + startScreensharing () { + return new Promise((resolve, reject) => { + const options = { + onicecandidate: (candidate) => { + this.onIceCandidate(candidate, this.role); + }, + videoStream: this.stream, + }; + + this.addIceServers(options); + this.webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerSendonly(options, (error) => { + if (error) { + // 1305: "PEER_NEGOTIATION_FAILED", + const normalizedError = BaseBroker.assembleError(1305); + logger.error({ + logCode: `${this.logCodePrefix}_peer_creation_failed`, + extraInfo: { + errorMessage: error.name || error.message || 'Unknown error', + errorCode: normalizedError.errorCode, + role: this.role, + sfuComponent: this.sfuComponent, + started: this.started, + }, + }, `Screenshare peer creation failed`); + this.onerror(normalizedError); + return reject(normalizedError); + } + + this.webRtcPeer.iceQueue = []; + this.webRtcPeer.generateOffer(this.onOfferGenerated.bind(this)); + + const localStream = this.webRtcPeer.peerConnection.getLocalStreams()[0]; + + localStream.getVideoTracks()[0].onended = () => { + this.webRtcPeer.peerConnection.onconnectionstatechange = null; + this.onstreamended(); + }; + + localStream.getVideoTracks()[0].oninactive = () => { + this.onstreamended(); + }; + + return resolve(); + }); + + this.webRtcPeer.peerConnection.onconnectionstatechange = () => { + this.handleConnectionStateChange('screenshare'); + }; + }); + } + + onIceCandidate (candidate, role) { + const message = { + id: ON_ICE_CANDIDATE_MSG, + role, + type: this.sfuComponent, + voiceBridge: this.voiceBridge, + candidate, + callerName: this.userId, + }; + + this.sendMessage(message); + } + + subscribeToScreenStream () { + return new Promise((resolve, reject) => { + const options = { + mediaConstraints: { + audio: !!this.hasAudio, + }, + onicecandidate: (candidate) => { + this.onIceCandidate(candidate, this.role); + }, + }; + + this.addIceServers(options); + + this.webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly(options, (error) => { + if (error) { + // 1305: "PEER_NEGOTIATION_FAILED", + const normalizedError = BaseBroker.assembleError(1305); + logger.error({ + logCode: `${this.logCodePrefix}_peer_creation_failed`, + extraInfo: { + errorMessage: error.name || error.message || 'Unknown error', + errorCode: normalizedError.errorCode, + role: this.role, + sfuComponent: this.sfuComponent, + started: this.started, + }, + }, `Screenshare peer creation failed`); + this.onerror(normalizedError); + return reject(normalizedError); + } + this.webRtcPeer.iceQueue = []; + this.webRtcPeer.generateOffer(this.onOfferGenerated.bind(this)); + }); + + this.webRtcPeer.peerConnection.onconnectionstatechange = () => { + this.handleConnectionStateChange('screenshare'); + }; + return resolve(); + }); + } +} + +export default ScreenshareBroker; diff --git a/bigbluebutton-html5/imports/ui/services/bbb-webrtc-sfu/sfu-base-broker.js b/bigbluebutton-html5/imports/ui/services/bbb-webrtc-sfu/sfu-base-broker.js index dbd903148807eda6e73406c484536294afdfd9b3..e26f36af10e2f1fe12a54f1352a53e0596ec7de3 100644 --- a/bigbluebutton-html5/imports/ui/services/bbb-webrtc-sfu/sfu-base-broker.js +++ b/bigbluebutton-html5/imports/ui/services/bbb-webrtc-sfu/sfu-base-broker.js @@ -1,6 +1,6 @@ import logger from '/imports/startup/client/logger'; import { notifyStreamStateChange } from '/imports/ui/services/bbb-webrtc-sfu/stream-state-service'; -import SFU_BROKER_ERRORS from '/imports/ui/services/bbb-webrtc-sfu/broker-base-errors'; +import { SFU_BROKER_ERRORS } from '/imports/ui/services/bbb-webrtc-sfu/broker-base-errors'; const PING_INTERVAL_MS = 15000; diff --git a/bigbluebutton-html5/imports/ui/services/bbb-webrtc-sfu/stream-state-service.js b/bigbluebutton-html5/imports/ui/services/bbb-webrtc-sfu/stream-state-service.js index b4e929c4bf33b4de68f73435c5c4735d0beae47b..f45fefdfe63b5c341e79692c2c207d1ac3055ffb 100644 --- a/bigbluebutton-html5/imports/ui/services/bbb-webrtc-sfu/stream-state-service.js +++ b/bigbluebutton-html5/imports/ui/services/bbb-webrtc-sfu/stream-state-service.js @@ -37,7 +37,7 @@ export const unsubscribeFromStreamStateChange = (eventTag, callback) => { } export const isStreamStateUnhealthy = (streamState) => { - return streamState === 'disconnected' || streamState === 'failed' || streamState === 'closed'; + return streamState === 'failed' || streamState === 'closed'; } export const isStreamStateHealthy = (streamState) => { diff --git a/bigbluebutton-html5/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml index 57e5099c8cb9c5959d4024f52e8c796b57f51ea3..2e019c7a640e86a8a2357b61e3729d5931fcdd95 100755 --- a/bigbluebutton-html5/private/config/settings.yml +++ b/bigbluebutton-html5/private/config/settings.yml @@ -147,11 +147,15 @@ public: # Max timeout: used as the max camera subscribe reconnection timeout. Each # subscribe reattempt increases the reconnection timer up to this maxTimeout: 60000 - chromeDefaultExtensionKey: akgoaoikmbmhcopjgakkcepdgdgkjfbc - chromeDefaultExtensionLink: https://chrome.google.com/webstore/detail/bigbluebutton-screenshare/akgoaoikmbmhcopjgakkcepdgdgkjfbc - 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: @@ -162,10 +166,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 @@ -589,7 +589,7 @@ private: - browser: chromeMobileIOS version: Infinity - browser: firefox - version: 63 + version: 68 - browser: firefoxMobile version: 68 - browser: edge diff --git a/bigbluebutton-html5/public/compatibility/kurento-extension.js b/bigbluebutton-html5/public/compatibility/kurento-extension.js deleted file mode 100755 index 10395ded953b2947fcfa455fab4f626cb36d87fc..0000000000000000000000000000000000000000 --- a/bigbluebutton-html5/public/compatibility/kurento-extension.js +++ /dev/null @@ -1,923 +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 - - 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, - }; - - 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: false, - }, - 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(); -}; diff --git a/bigbluebutton-html5/public/locales/en.json b/bigbluebutton-html5/public/locales/en.json index 6b6499f4fc6c649a99f175fab6ecea6cb9382d1f..421fd097c7c4044f165e91a616dcb5f4d3490ac7 100755 --- a/bigbluebutton-html5/public/locales/en.json +++ b/bigbluebutton-html5/public/locales/en.json @@ -128,10 +128,11 @@ "app.media.screenshare.notSupported": "Screensharing is not supported in this browser.", "app.media.screenshare.autoplayBlockedDesc": "We need your permission to show you the presenter's screen.", "app.media.screenshare.autoplayAllowLabel": "View shared screen", - "app.screenshare.notAllowed": "Error: Permission to access screen wasn't granted.", - "app.screenshare.notSupportedError": "Error: Screensharing is allowed only on safe (SSL) domains", - "app.screenshare.notReadableError": "Error: There was a failure while trying to capture your screen", - "app.screenshare.genericError": "Error: An error has occurred with screensharing, please try again", + "app.screenshare.screenshareFinalError": "Code {0}. Could not share the screen.", + "app.screenshare.screenshareRetryError": "Code {0}. Try sharing the screen again.", + "app.screenshare.screenshareRetryOtherEnvError": "Code {0}. Could not share the screen. Try again using a different browser or device.", + "app.screenshare.screenshareUnsupportedEnv": "Code {0}. Browser is not supported. Try again using a different browser or device.", + "app.screenshare.screensharePermissionError": "Code {0}. Permission to capture the screen needs to be granted.", "app.meeting.ended": "This session has ended", "app.meeting.meetingTimeRemaining": "Meeting time remaining: {0}", "app.meeting.meetingTimeHasEnded": "Time ended. Meeting will close soon", @@ -664,7 +665,6 @@ "app.video.clientDisconnected": "Webcam cannot be shared due to connection issues", "app.fullscreenButton.label": "Make {0} fullscreen", "app.fullscreenUndoButton.label": "Undo {0} fullscreen", - "app.deskshare.iceConnectionStateError": "Connection failed when sharing screen (ICE error 1108)", "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)",