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;