7697367335
Turns the screenshare component into a generic component, so that it can be used both for screenshare and camera as content fetures. Also changes specific locales and icons for the camera as content feature.
393 lines
12 KiB
JavaScript
Executable File
393 lines
12 KiB
JavaScript
Executable File
import Auth from '/imports/ui/services/auth';
|
|
import logger from '/imports/startup/client/logger';
|
|
import BridgeService from './service';
|
|
import ScreenshareBroker from '/imports/ui/services/bbb-webrtc-sfu/screenshare-broker';
|
|
import { setIsSharing, screenShareEndAlert } from '/imports/ui/components/screenshare/service';
|
|
import { SCREENSHARING_ERRORS } from './errors';
|
|
import { shouldForceRelay } from '/imports/ui/services/bbb-webrtc-sfu/utils';
|
|
import MediaStreamUtils from '/imports/utils/media-stream-utils';
|
|
import { notifyStreamStateChange } from '/imports/ui/services/bbb-webrtc-sfu/stream-state-service';
|
|
|
|
const SFU_CONFIG = Meteor.settings.public.kurento;
|
|
const SFU_URL = SFU_CONFIG.wsUrl;
|
|
const OFFERING = SFU_CONFIG.screenshare.subscriberOffering;
|
|
const SIGNAL_CANDIDATES = Meteor.settings.public.kurento.signalCandidates;
|
|
const TRACE_LOGS = Meteor.settings.public.kurento.traceLogs;
|
|
const { screenshare: NETWORK_PRIORITY } = Meteor.settings.public.media.networkPriorities || {};
|
|
const GATHERING_TIMEOUT = Meteor.settings.public.kurento.gatheringTimeout;
|
|
|
|
const BRIDGE_NAME = 'kurento'
|
|
const SCREENSHARE_VIDEO_TAG = 'screenshareVideo';
|
|
const SEND_ROLE = 'send';
|
|
const RECV_ROLE = 'recv';
|
|
const DEFAULT_VOLUME = 1;
|
|
|
|
// 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,
|
|
1310: SCREENSHARING_ERRORS.ENDED_WHILE_STARTING,
|
|
};
|
|
|
|
const mapErrorCode = (error) => {
|
|
const { errorCode } = error;
|
|
const mappedError = ERROR_MAP[errorCode];
|
|
|
|
if (errorCode == null || mappedError == null) return error;
|
|
error.errorCode = mappedError.errorCode;
|
|
error.errorMessage = mappedError.errorMessage;
|
|
error.message = mappedError.errorMessage;
|
|
|
|
return error;
|
|
}
|
|
|
|
export default class KurentoScreenshareBridge {
|
|
constructor() {
|
|
this.role;
|
|
this.broker;
|
|
this._gdmStream;
|
|
this.hasAudio = false;
|
|
this.connectionAttempts = 0;
|
|
this.reconnecting = false;
|
|
this.reconnectionTimeout;
|
|
this.restartIntervalMs = BridgeService.BASE_MEDIA_TIMEOUT;
|
|
this.startedOnce = false;
|
|
}
|
|
|
|
get gdmStream() {
|
|
return this._gdmStream;
|
|
}
|
|
|
|
set gdmStream(stream) {
|
|
this._gdmStream = stream;
|
|
}
|
|
|
|
_shouldReconnect() {
|
|
// Sender/presenter reconnect is *not* implemented yet
|
|
return this.reconnectionTimeout == null && this.role === RECV_ROLE;
|
|
}
|
|
|
|
/**
|
|
* Get the RTCPeerConnection object related to the screensharing stream.
|
|
* @returns {Object} The RTCPeerConnection object related to the presenter/
|
|
* viewer peer. If there's no stream being shared, returns
|
|
* null.
|
|
*/
|
|
getPeerConnection() {
|
|
try {
|
|
let peerConnection = null;
|
|
|
|
if (this.broker && this.broker.webRtcPeer) {
|
|
peerConnection = this.broker.webRtcPeer.peerConnection;
|
|
}
|
|
|
|
return peerConnection;
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
inboundStreamReconnect() {
|
|
const currentRestartIntervalMs = this.restartIntervalMs;
|
|
|
|
logger.warn({
|
|
logCode: 'screenshare_viewer_reconnect',
|
|
extraInfo: {
|
|
reconnecting: this.reconnecting,
|
|
role: this.role,
|
|
bridge: BRIDGE_NAME,
|
|
},
|
|
}, 'Screenshare viewer 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();
|
|
|
|
// Sender/presenter reconnect is *not* implemented yet
|
|
case SEND_ROLE:
|
|
default:
|
|
this.reconnecting = false;
|
|
logger.error({
|
|
logCode: 'screenshare_wont_reconnect',
|
|
extraInfo: {
|
|
role: this.broker?.role || this.role,
|
|
started: !!(this.broker?.started),
|
|
bridge: BRIDGE_NAME,
|
|
},
|
|
}, 'Screen sharing will not reconnect');
|
|
break;
|
|
}
|
|
}
|
|
|
|
maxConnectionAttemptsReached () {
|
|
return this.connectionAttempts > BridgeService.MAX_CONN_ATTEMPTS;
|
|
}
|
|
|
|
scheduleReconnect({
|
|
overrideTimeout,
|
|
} = { }) {
|
|
if (this.reconnectionTimeout == null) {
|
|
let nextRestartInterval = this.restartIntervalMs;
|
|
if (typeof overrideTimeout === 'number') nextRestartInterval = overrideTimeout;
|
|
|
|
this.reconnectionTimeout = setTimeout(
|
|
this.handleConnectionTimeoutExpiry.bind(this),
|
|
nextRestartInterval,
|
|
);
|
|
}
|
|
}
|
|
|
|
clearReconnectionTimeout () {
|
|
this.reconnecting = false;
|
|
this.restartIntervalMs = BridgeService.BASE_MEDIA_TIMEOUT;
|
|
|
|
if (this.reconnectionTimeout) {
|
|
clearTimeout(this.reconnectionTimeout);
|
|
this.reconnectionTimeout = null;
|
|
}
|
|
}
|
|
|
|
setVolume(volume) {
|
|
const mediaElement = document.getElementById(SCREENSHARE_VIDEO_TAG);
|
|
|
|
if (mediaElement) {
|
|
if (typeof volume === 'number' && volume >= 0 && volume <= 1) {
|
|
mediaElement.volume = volume;
|
|
}
|
|
|
|
return mediaElement.volume;
|
|
}
|
|
|
|
return DEFAULT_VOLUME;
|
|
}
|
|
|
|
getVolume() {
|
|
const mediaElement = document.getElementById(SCREENSHARE_VIDEO_TAG);
|
|
|
|
if (mediaElement) return mediaElement.volume;
|
|
|
|
return DEFAULT_VOLUME;
|
|
}
|
|
|
|
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.startedOnce = true;
|
|
this.clearReconnectionTimeout();
|
|
this.connectionAttempts = 0;
|
|
}
|
|
|
|
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: ${errorMessage}`);
|
|
|
|
notifyStreamStateChange('screenshare', 'failed');
|
|
// Screensharing was already successfully negotiated and error occurred during
|
|
// during call; schedule a reconnect
|
|
if (this._shouldReconnect()) {
|
|
// this.broker.started => whether the reconnect should happen immediately.
|
|
// If this session previously established connection (N-sessions back)
|
|
// and it failed abruptly, then the timeout is overridden to a intermediate value
|
|
// (BASE_RECONNECTION_TIMEOUT)
|
|
let overrideTimeout;
|
|
if (this.broker?.started) {
|
|
overrideTimeout = 0;
|
|
} else if (this.startedOnce) {
|
|
overrideTimeout = BridgeService.BASE_RECONNECTION_TIMEOUT;
|
|
}
|
|
|
|
this.scheduleReconnect({ overrideTimeout });
|
|
}
|
|
|
|
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,
|
|
offering: OFFERING,
|
|
mediaServer: BridgeService.getMediaServerAdapter(),
|
|
signalCandidates: SIGNAL_CANDIDATES,
|
|
forceRelay: shouldForceRelay(),
|
|
traceLogs: TRACE_LOGS,
|
|
gatheringTimeout: GATHERING_TIMEOUT,
|
|
};
|
|
|
|
this.broker = new ScreenshareBroker(
|
|
Auth.authenticateURL(SFU_URL),
|
|
BridgeService.getConferenceBridge(),
|
|
Auth.userID,
|
|
Auth.meetingID,
|
|
this.role,
|
|
options,
|
|
);
|
|
|
|
this.broker.onstart = this.handleViewerStart.bind(this);
|
|
this.broker.onerror = this.handleBrokerFailure.bind(this);
|
|
this.broker.onended = this.handleEnded.bind(this);
|
|
|
|
return this.broker.view().finally(this.scheduleReconnect.bind(this));
|
|
}
|
|
|
|
handlePresenterStart() {
|
|
logger.info({
|
|
logCode: 'screenshare_presenter_start_success',
|
|
}, 'Screenshare presenter started succesfully');
|
|
this.clearReconnectionTimeout();
|
|
this.startedOnce = true;
|
|
this.reconnecting = false;
|
|
this.connectionAttempts = 0;
|
|
}
|
|
|
|
handleEnded() {
|
|
screenShareEndAlert();
|
|
}
|
|
|
|
share(stream, onFailure, contentType) {
|
|
return new Promise(async (resolve, reject) => {
|
|
this.onerror = onFailure;
|
|
this.connectionAttempts += 1;
|
|
this.role = SEND_ROLE;
|
|
this.hasAudio = BridgeService.streamHasAudioTrack(stream);
|
|
this.gdmStream = stream;
|
|
|
|
const onerror = (error) => {
|
|
const normalizedError = this.handleBrokerFailure(error);
|
|
if (!this.broker.started) {
|
|
// Broker hasn't started - if there are retries left, try again.
|
|
if (this.maxConnectionAttemptsReached()) {
|
|
this.clearReconnectionTimeout();
|
|
this.connectionAttempts = 0;
|
|
onFailure(SCREENSHARING_ERRORS.MEDIA_TIMEOUT);
|
|
reject(SCREENSHARING_ERRORS.MEDIA_TIMEOUT);
|
|
}
|
|
} else if (!this._shouldReconnect()) {
|
|
// Broker has started - should reconnect? If it shouldn't, end it.
|
|
onFailure(normalizedError);
|
|
}
|
|
};
|
|
|
|
const iceServers = await BridgeService.getIceServers(Auth.sessionToken);
|
|
const options = {
|
|
iceServers,
|
|
userName: Auth.fullname,
|
|
stream,
|
|
hasAudio: this.hasAudio,
|
|
contentType: contentType,
|
|
bitrate: BridgeService.BASE_BITRATE,
|
|
offering: true,
|
|
mediaServer: BridgeService.getMediaServerAdapter(),
|
|
signalCandidates: SIGNAL_CANDIDATES,
|
|
forceRelay: shouldForceRelay(),
|
|
traceLogs: TRACE_LOGS,
|
|
networkPriority: NETWORK_PRIORITY,
|
|
gatheringTimeout: GATHERING_TIMEOUT,
|
|
};
|
|
|
|
this.broker = new ScreenshareBroker(
|
|
Auth.authenticateURL(SFU_URL),
|
|
BridgeService.getConferenceBridge(),
|
|
Auth.userID,
|
|
Auth.meetingID,
|
|
this.role,
|
|
options,
|
|
);
|
|
|
|
this.broker.onerror = onerror.bind(this);
|
|
this.broker.onstreamended = this.stop.bind(this);
|
|
this.broker.onstart = this.handlePresenterStart.bind(this);
|
|
this.broker.onended = this.handleEnded.bind(this);
|
|
|
|
this.broker.share().then(() => {
|
|
this.scheduleReconnect();
|
|
return resolve();
|
|
}).catch((error) => reject(mapErrorCode(error)));
|
|
});
|
|
}
|
|
|
|
// This is a reconnect-safe internal method. Should be used when one wants
|
|
// to clear the internal components (ie broker, connection timeouts) without
|
|
// affecting externally controlled components (ie gDM stream,
|
|
// media tag, connectionAttempts, ...)
|
|
_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 && this.broker.role === SEND_ROLE && !this.reconnecting) {
|
|
setIsSharing(false);
|
|
}
|
|
this.broker = null;
|
|
}
|
|
|
|
this.clearReconnectionTimeout();
|
|
}
|
|
|
|
stop() {
|
|
const mediaElement = document.getElementById(SCREENSHARE_VIDEO_TAG);
|
|
|
|
this._stop();
|
|
this.connectionAttempts = 0;
|
|
|
|
if (mediaElement && typeof mediaElement.pause === 'function') {
|
|
mediaElement.pause();
|
|
mediaElement.srcObject = null;
|
|
}
|
|
|
|
if (this.gdmStream) {
|
|
MediaStreamUtils.stopMediaStreamTracks(this.gdmStream);
|
|
this.gdmStream = null;
|
|
}
|
|
}
|
|
}
|