Merge branch 'u22-screenshare18102020' of https://github.com/prlanzarin/bigbluebutton-1 into u23-ilgiardino

This commit is contained in:
prlanzarin 2021-03-11 18:49:14 +00:00
commit 7eb91f9273
28 changed files with 1210 additions and 1574 deletions

View File

@ -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
}

View File

@ -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)
}

View File

@ -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")

View File

@ -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(),

View File

@ -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,

View File

@ -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;
}
}

View File

@ -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)

View File

@ -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.

View File

@ -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">

View File

@ -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,
}

View File

@ -1,236 +1,290 @@
import Auth from '/imports/ui/services/auth';
import BridgeService from './service';
import { fetchWebRTCMappedStunTurnServers, getMappedFallbackStun } from '/imports/utils/fetchStunTurnServers';
import playAndRetry from '/imports/utils/mediaElementPlayRetry';
import logger from '/imports/startup/client/logger';
import BridgeService from './service';
import ScreenshareBroker from '/imports/ui/services/bbb-webrtc-sfu/screenshare-broker';
import { setSharingScreen } from '/imports/ui/components/screenshare/service';
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 BRIDGE_NAME = 'kurento'
const SCREENSHARE_VIDEO_TAG = 'screenshareVideo';
const SEND_ROLE = 'send';
const RECV_ROLE = 'recv';
const CHROME_EXTENSION_KEY = CHROME_CUSTOM_EXTENSION_KEY === 'KEY' ? CHROME_DEFAULT_EXTENSION_KEY : CHROME_CUSTOM_EXTENSION_KEY;
// 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 getUserId = () => Auth.userID;
const mapErrorCode = (error) => {
const { errorCode } = error;
const mappedError = ERROR_MAP[errorCode];
const getMeetingId = () => Auth.meetingID;
if (errorCode == null || mappedError == null) return error;
error.errorCode = mappedError.errorCode;
error.errorMessage = mappedError.errorMessage;
error.message = mappedError.errorMessage;
const getUsername = () => Auth.fullname;
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';
return { errorMessage, errorCode, errorReason };
constructor() {
this.role;
this.broker;
this._gdmStream;
this.hasAudio = false;
this.connectionAttempts = 0;
this.reconnecting = false;
this.reconnectionTimeout;
this.restartIntervalMs = BridgeService.BASE_MEDIA_TIMEOUT;
}
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}`);
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`);
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');
});
}
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 (this.reconnectionTimeout) {
clearTimeout(this.reconnectionTimeout);
this.reconnectionTimeout = null;
}
}
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();
}
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');
// 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 view(hasAudio = false) {
this.hasAudio = hasAudio;
this.role = RECV_ROLE;
const iceServers = await BridgeService.getIceServers(Auth.sessionToken);
const options = {
iceServers,
userName: Auth.fullname,
hasAudio,
};
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();
}
this.broker = new ScreenshareBroker(
Auth.authenticateURL(SFU_URL),
BridgeService.getConferenceBridge(),
Auth.userID,
Auth.meetingID,
this.role,
options,
);
this.broker.onstart = this.handleViewerStart.bind(this);
this.broker.onerror = this.handleBrokerFailure.bind(this);
return this.broker.view().finally(this.scheduleReconnect.bind(this));
}
static screenshareElementLoadAndPlay(stream, element, muted) {
element.muted = muted;
element.pause();
element.srcObject = stream;
KurentoScreenshareBridge.playElement(element);
handlePresenterStart() {
logger.info({
logCode: 'screenshare_presenter_start_success',
}, 'Screenshare presenter started succesfully');
this.clearReconnectionTimeout();
this.reconnecting = false;
this.connectionAttempts = 0;
}
kurentoViewLocalPreview() {
const screenshareMediaElement = document.getElementById(SCREENSHARE_VIDEO_TAG);
const { webRtcPeer } = window.kurentoManager.kurentoScreenshare;
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;
if (webRtcPeer) {
const stream = webRtcPeer.getLocalStream();
KurentoScreenshareBridge.screenshareElementLoadAndPlay(stream, screenshareMediaElement, true);
}
}
const onerror = (error) => {
const normalizedError = this.handleBrokerFailure(error);
if (this.maxConnectionAttemptsReached()) {
this.clearReconnectionTimeout();
this.connectionAttempts = 0;
onFailure(SCREENSHARING_ERRORS.MEDIA_TIMEOUT);
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(),
};
const onFail = (error) => {
KurentoScreenshareBridge.handleViewerFailure(error, started);
};
// Callback for the kurento-extension.js script. It's called when the whole
// negotiation with SFU is successful. This will load the stream into the
// screenshare media element and play it manually.
const onSuccess = () => {
started = true;
const { webRtcPeer } = window.kurentoManager.kurentoVideo;
if (webRtcPeer) {
const stream = webRtcPeer.getRemoteStream();
KurentoScreenshareBridge.screenshareElementLoadAndPlay(
stream,
screenshareMediaElement,
true,
);
return reject(SCREENSHARING_ERRORS.MEDIA_TIMEOUT);
}
};
window.kurentoWatchVideo(
SCREENSHARE_VIDEO_TAG,
BridgeService.getConferenceBridge(),
getUserId(),
getMeetingId(),
onFail,
onSuccess,
options,
);
}
}
kurentoExitVideo() {
window.kurentoExitVideo();
}
async kurentoShareScreen(onFail, stream) {
let iceServers = [];
try {
iceServers = await fetchWebRTCMappedStunTurnServers(getSessionToken());
} catch (error) {
logger.error({ logCode: 'screenshare_presenter_fetchstunturninfo_error' },
'Screenshare bridge failed to fetch STUN/TURN info, using default');
iceServers = getMappedFallbackStun();
} finally {
const iceServers = await BridgeService.getIceServers(Auth.sessionToken);
const options = {
wsUrl: Auth.authenticateURL(SFU_URL),
chromeExtension: CHROME_EXTENSION_KEY,
chromeScreenshareSources: CHROME_SCREENSHARE_SOURCES,
firefoxScreenshareSource: FIREFOX_SCREENSHARE_SOURCE,
iceServers,
logger,
userName: getUsername(),
userName: Auth.fullname,
stream,
hasAudio: this.hasAudio,
};
let started = false;
const failureCallback = (error) => {
const normalizedError = KurentoScreenshareBridge.handlePresenterFailure(error, started);
onFail(normalizedError);
};
const successCallback = () => {
started = true;
logger.info({
logCode: 'screenshare_presenter_start_success',
}, 'Screenshare presenter started succesfully');
};
options.stream = stream || undefined;
window.kurentoShareScreen(
SCREENSHARE_VIDEO_TAG,
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.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();
}
}

View File

@ -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,
};

View File

@ -1,101 +1,89 @@
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 AudioControlsContainer from '../audio/audio-controls/container';
import JoinVideoOptionsContainer from '../video-provider/video-button/container';
import CaptionsButtonContainer from '/imports/ui/components/actions-bar/captions/container';
import PresentationOptionsContainer from './presentation-options/component';
import { ACTIONSBAR_HEIGHT } from '/imports/ui/components/layout/layout-manager';
import React, { PureComponent } from 'react';
import cx from 'classnames';
import { styles } from './styles.scss';
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';
import PresentationOptionsContainer from './presentation-options/component';
import { ACTIONSBAR_HEIGHT } from '/imports/ui/components/layout/layout-manager';
class ActionsBar extends PureComponent {
render() {
const {
amIPresenter,
handleShareScreen,
handleUnshareScreen,
isVideoBroadcasting,
amIModerator,
screenSharingCheck,
enableVideo,
isLayoutSwapped,
toggleSwapLayout,
handleTakePresenter,
intl,
isSharingVideo,
screenShareEndAlert,
stopExternalVideoShare,
screenshareDataSavingSetting,
isCaptionsAvailable,
isMeteorConnected,
isPollingEnabled,
isPresentationDisabled,
isThereCurrentPresentation,
allowExternalVideo,
} = this.props;
class ActionsBar extends PureComponent {
render() {
const {
amIPresenter,
amIModerator,
enableVideo,
isLayoutSwapped,
toggleSwapLayout,
handleTakePresenter,
intl,
isSharingVideo,
stopExternalVideoShare,
isCaptionsAvailable,
isMeteorConnected,
isPollingEnabled,
isPresentationDisabled,
isThereCurrentPresentation,
allowExternalVideo,
} = this.props;
const actionBarClasses = {};
const actionBarClasses = {};
actionBarClasses[styles.centerWithActions] = amIPresenter;
actionBarClasses[styles.center] = true;
actionBarClasses[styles.mobileLayoutSwapped] = isLayoutSwapped && amIPresenter;
actionBarClasses[styles.centerWithActions] = amIPresenter;
actionBarClasses[styles.center] = true;
actionBarClasses[styles.mobileLayoutSwapped] = isLayoutSwapped && amIPresenter;
return (
<div
className={styles.actionsbar}
style={{
height: ACTIONSBAR_HEIGHT,
}}
>
<div className={styles.left}>
<ActionsDropdown {...{
amIPresenter,
amIModerator,
isPollingEnabled,
allowExternalVideo,
handleTakePresenter,
intl,
isSharingVideo,
stopExternalVideoShare,
isMeteorConnected,
return (
<div
className={styles.actionsbar}
style={{
height: ACTIONSBAR_HEIGHT,
}}
/>
{isCaptionsAvailable
? (
<CaptionsButtonContainer {...{ intl }} />
)
: null
}
</div>
<div className={cx(actionBarClasses)}>
<AudioControlsContainer />
{enableVideo
? (
<JoinVideoOptionsContainer />
)
: null}
<DesktopShare {...{
handleShareScreen,
handleUnshareScreen,
isVideoBroadcasting,
amIPresenter,
screenSharingCheck,
screenShareEndAlert,
isMeteorConnected,
screenshareDataSavingSetting,
}}
/>
</div>
<div className={styles.right}>
{isLayoutSwapped && !isPresentationDisabled
? (
<PresentationOptionsContainer
toggleSwapLayout={toggleSwapLayout}
isThereCurrentPresentation={isThereCurrentPresentation}
/>
)
: null
>
<div className={styles.left}>
<ActionsDropdown {...{
amIPresenter,
amIModerator,
isPollingEnabled,
allowExternalVideo,
handleTakePresenter,
intl,
isSharingVideo,
stopExternalVideoShare,
isMeteorConnected,
}}
/>
{isCaptionsAvailable
? (
<CaptionsButtonContainer {...{ intl }} />
)
: null
}
</div>
<div className={cx(actionBarClasses)}>
<AudioControlsContainer />
{enableVideo
? (
<JoinVideoOptionsContainer />
)
: null}
<ScreenshareButtonContainer {...{
amIPresenter,
isMeteorConnected,
}}
/>
</div>
<div className={styles.right}>
{isLayoutSwapped && !isPresentationDisabled
? (
<PresentationOptionsContainer
toggleSwapLayout={toggleSwapLayout}
isThereCurrentPresentation={isThereCurrentPresentation}
/>
)
: null
}
</div>
</div>

View File

@ -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,

View File

@ -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)));

View File

@ -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)));

View File

@ -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));

View File

@ -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);
}
onVideoLoad() {
onStreamStateChange (event) {
const { streamState } = event.detail;
const { isStreamHealthy } = this.state;
const newHealthState = !isStreamStateUnhealthy(streamState);
event.stopPropagation();
if (newHealthState !== isStreamHealthy) {
this.setState({ isStreamHealthy: newHealthState });
}
}
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,
};

View File

@ -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,

View File

@ -11,65 +11,124 @@ 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 viewScreenshare = () => {
const screenshareHasEnded = () => {
const amIPresenter = UserListService.isUserPresenter(Auth.userID);
if (amIPresenter) {
setSharingScreen(false);
}
KurentoBridge.stop();
screenShareEndAlert();
};
const getMediaElement = () => {
return document.getElementById(SCREENSHARE_MEDIA_ELEMENT_NAME);
}
const attachLocalPreviewStream = (mediaElement) => {
const stream = KurentoBridge.gdmStream;
if (stream && mediaElement) {
// Always muted, presenter preview.
BridgeService.screenshareLoadAndPlayMediaStream(stream, mediaElement, true);
}
}
const screenshareHasStarted = () => {
const amIPresenter = UserListService.isUserPresenter(Auth.userID);
// Presenter's screen preview is local, so skip
if (!amIPresenter) {
KurentoBridge.kurentoViewScreen();
} else {
KurentoBridge.kurentoViewLocalPreview();
viewScreenshare();
}
};
// 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.
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 +137,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,
};

View File

@ -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%);
}

View File

@ -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,
};

View File

@ -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;

View File

@ -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;

View File

@ -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) => {

View File

@ -146,11 +146,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:
@ -161,10 +165,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
@ -583,7 +583,7 @@ private:
- browser: chromeMobileIOS
version: Infinity
- browser: firefox
version: 63
version: 68
- browser: firefoxMobile
version: 68
- browser: edge

View File

@ -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();
};

View File

@ -127,10 +127,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",
@ -662,7 +663,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)",