bigbluebutton-Github/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js
prlanzarin 1383ab4def screenshare/html5: rewrite most of the client side code
Added new SFU broker for screen sharing

Removed kurento-extension entirely

Added inbound and outbound reconnection procedures

Improve UI responsiveness when sharing

Add reconnection UI states

Redo error handling

Refactor actions-bar screen share components. Make it smarter with less prop drilling and less re-rendering. Also more readable. Still work to do in that I think

Add a connection retry procedure for screen presenters when they are sharing; try a configurable amount of times when failure is triggered, with configurable min and max reconn timeouts and timeout increase factor

Make local preview attachment smarter

ADD PARTIAL SUPPORT FOR AUDIO SHARING VIA SCREEN SHARING WITH GET DISPLAY MEDIA, RECORDING STILL NOT SUPPORTED!!!
2020-12-09 22:00:54 +00:00

260 lines
7.8 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 { setSharingScreen } from '/imports/ui/components/screenshare/service';
const SFU_CONFIG = Meteor.settings.public.kurento;
const SFU_URL = SFU_CONFIG.wsUrl;
const BRIDGE_NAME = 'kurento'
const SCREENSHARE_VIDEO_TAG = 'screenshareVideo';
const SEND_ROLE = 'send';
const RECV_ROLE = 'recv';
const errorCodeMap = {
1301: 1101,
1302: 1102,
1305: 1105,
1307: 1108, // This should be 1107, but I'm preserving the existing locales - prlanzarin
}
const mapErrorCode = (error) => {
const { errorCode } = error;
const mappedErrorCode = errorCodeMap[errorCode];
if (errorCode == null || mappedErrorCode == null) return error;
error.errorCode = mappedErrorCode;
return error;
}
export default class KurentoScreenshareBridge {
constructor() {
this.role;
this.broker;
this._gdmStream;
this.connectionAttempts = 0;
this.reconnecting = false;
this.reconnectionTimeout;
this.restartIntervalMs = BridgeService.BASE_MEDIA_TIMEOUT;
}
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'
}, `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: {
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',
}, `Screenshare viewer session is reconnecting`);
// Cleanly stop everything before triggering a reconnect
this.stop();
// Create new reconnect interval time
this.restartIntervalMs = BridgeService.getNextReconnectionInterval(currentRestartIntervalMs);
this.view(stream, this.onerror).then(() => {
this.clearReconnectionTimeout();
}).catch(error => {
// Error handling is a no-op because it will be "handled" in handleViewerFailure
logger.debug({
logCode: 'screenshare_reconnect_failed',
extraInfo: {
errorMessage: error.errorMessage,
reconnecting: this.reconnecting,
role: this.role,
bridge: BRIDGE_NAME
},
}, 'Screensharing reconnect failed');
});
}
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;
}
}
maxConnectionAttemptsReached () {
return this.connectionAttempts > BridgeService.MAX_CONN_ATTEMPTS;
}
scheduleReconnect () {
if (this.reconnectionTimeout == null) {
this.reconnectionTimeout = setTimeout(
this.handleConnectionTimeoutExpiry.bind(this),
this.restartIntervalMs
);
}
}
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);
BridgeService.handleViewerFailure(error, this.broker.started);
// Screensharing was already successfully negotiated and error occurred during
// during call; schedule a reconnect
// If the session has not yet started, a reconnect should already be scheduled
if (this.broker.started) {
this.scheduleReconnect();
}
}
async view(hasAudio = false) {
this.role = RECV_ROLE;
const iceServers = await BridgeService.getIceServers(Auth.sessionToken);
const options = {
iceServers,
userName: Auth.fullname,
hasAudio,
};
this.broker = new ScreenshareBroker(
Auth.authenticateURL(SFU_URL),
BridgeService.getConferenceBridge(),
Auth.userID,
Auth.meetingID,
this.role,
options,
);
this.broker.onstart = this.handleViewerStart.bind(this);
this.broker.onerror = this.handleBrokerFailure.bind(this);
this.broker.onstreamended = this.stop.bind(this);
return this.broker.view().finally(this.scheduleReconnect.bind(this));
}
handlePresenterStart() {
logger.info({
logCode: 'screenshare_presenter_start_success',
}, 'Screenshare presenter started succesfully');
this.clearReconnectionTimeout();
this.reconnecting = false;
this.connectionAttempts = 0;
}
async share(stream, onFailure) {
this.onerror = onFailure;
this.connectionAttempts += 1;
this.role = SEND_ROLE;
this.gdmStream = stream;
const onerror = (error) => {
mapErrorCode(error);
const normalizedError = BridgeService.handlePresenterFailure(error, this.broker.started);
// Gracious mid call reconnects aren't yet implemented, so stop it.
if (this.broker.started) {
return onFailure(normalizedError);
}
// Otherwise, sharing attempts have a finite amount of attempts for it
// to work (configurable). If expired, error out.
if (this.maxConnectionAttemptsReached()) {
this.clearReconnectionTimeout();
this.connectionAttempts = 0;
return onFailure({
errorCode: 1120,
errorMessage: `MAX_CONNECTION_ATTEMPTS_REACHED`,
});
}
};
const iceServers = await BridgeService.getIceServers(Auth.sessionToken);
const options = {
iceServers,
userName: Auth.fullname,
stream,
hasAudio: BridgeService.streamHasAudioTrack(stream),
};
this.broker = new ScreenshareBroker(
Auth.authenticateURL(SFU_URL),
BridgeService.getConferenceBridge(),
Auth.userID,
Auth.meetingID,
this.role,
options,
);
this.broker.onstart = this.handlePresenterStart.bind(this);
this.broker.onerror = onerror.bind(this);
this.broker.onstreamended = this.stop.bind(this);
return this.broker.share().finally(this.scheduleReconnect.bind(this));
};
stop() {
if (this.broker) {
this.broker.stop();
// Checks if this session is a sharer and if it's not reconnecting
// If that's the case, clear the local sharing state in screen sharing UI
// component tracker to be extra sure we won't have any client-side state
// inconsistency - prlanzarin
if (this.broker.role === SEND_ROLE && !this.reconnecting) setSharingScreen(false);
this.broker = null;
}
this.gdmStream = null;
this.clearReconnectionTimeout();
}
}