diff --git a/bigbluebutton-html5/imports/ui/services/bbb-webrtc-sfu/screenshare-broker.js b/bigbluebutton-html5/imports/ui/services/bbb-webrtc-sfu/screenshare-broker.js
new file mode 100644
index 0000000000..b28bd9c79e
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/services/bbb-webrtc-sfu/screenshare-broker.js
@@ -0,0 +1,243 @@
+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;
+ this.userName;
+ this.caleeName;
+ this.iceServers;
+
+ // 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,
+ };
+
+ logger.info({
+ logCode: `${this.logCodePrefix}_offer_generated`,
+ extraInfo: { sfuComponent: this.sfuComponent, role: this.role },
+ }, `SFU screenshare offer generated`);
+
+ 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) {
+ logger.debug({
+ logCode: `${this.logCodePrefix}_client_candidate`,
+ extraInfo: { sfuComponent: this.sfuComponent, candidate, role: this.role }
+ }, `Screenshare candidate generated: ${JSON.stringify(candidate)}`);
+
+ 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;