e902f2ee27
This commit adds a contentType field in the back-end components of the screenshare feature in order to accomodate the new 'camera as content' feature.
297 lines
8.2 KiB
JavaScript
297 lines
8.2 KiB
JavaScript
import logger from '/imports/startup/client/logger';
|
|
import BaseBroker from '/imports/ui/services/bbb-webrtc-sfu/sfu-base-broker';
|
|
import WebRtcPeer from '/imports/ui/services/webrtc-base/peer';
|
|
|
|
const ON_ICE_CANDIDATE_MSG = 'iceCandidate';
|
|
const SUBSCRIBER_ANSWER = 'subscriberAnswer';
|
|
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.contentType = "camera";
|
|
this.offering = true;
|
|
this.signalCandidates = true;
|
|
this.ending = false;
|
|
|
|
// Optional parameters are:
|
|
// userName,
|
|
// caleeName,
|
|
// iceServers,
|
|
// hasAudio,
|
|
// contentType,
|
|
// bitrate,
|
|
// offering,
|
|
// mediaServer,
|
|
// signalCandidates,
|
|
// traceLogs
|
|
// networkPriority
|
|
// gatheringTimeout
|
|
Object.assign(this, options);
|
|
}
|
|
|
|
_onstreamended() {
|
|
// Flag the broker as ending; we want to abort processing start responses
|
|
this.ending = true;
|
|
this.onstreamended();
|
|
}
|
|
|
|
onstreamended() {
|
|
// To be implemented by instantiators
|
|
}
|
|
|
|
async share () {
|
|
return new Promise((resolve, reject) => {
|
|
if (this.stream == null) {
|
|
logger.error({
|
|
logCode: `${this.logCodePrefix}_missing_stream`,
|
|
extraInfo: { role: this.role, sfuComponent: this.sfuComponent },
|
|
}, 'Screenshare broker start failed: missing stream');
|
|
return reject(BaseBroker.assembleError(1305));
|
|
}
|
|
|
|
return this.openWSConnection()
|
|
.then(this.startScreensharing.bind(this))
|
|
.then(resolve)
|
|
.catch(reject);
|
|
});
|
|
}
|
|
|
|
view () {
|
|
return this.openWSConnection()
|
|
.then(this.subscribeToScreenStream.bind(this));
|
|
}
|
|
|
|
onWSMessage (message) {
|
|
const parsedMessage = JSON.parse(message.data);
|
|
|
|
switch (parsedMessage.id) {
|
|
case 'startResponse':
|
|
if (!this.ending && !this.started) {
|
|
this.onRemoteDescriptionReceived(parsedMessage);
|
|
}
|
|
break;
|
|
case 'playStart':
|
|
if (!this.ending && !this.started) {
|
|
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);
|
|
}
|
|
|
|
sendLocalDescription (localDescription) {
|
|
const message = {
|
|
id: SUBSCRIBER_ANSWER,
|
|
type: this.sfuComponent,
|
|
role: this.role,
|
|
voiceBridge: this.voiceBridge,
|
|
callerName: this.userId,
|
|
answer: localDescription,
|
|
};
|
|
|
|
this.sendMessage(message);
|
|
}
|
|
|
|
onRemoteDescriptionReceived (sfuResponse) {
|
|
if (this.offering) {
|
|
return this.processAnswer(sfuResponse);
|
|
}
|
|
|
|
return this.processOffer(sfuResponse);
|
|
}
|
|
|
|
sendStartReq(offer) {
|
|
const message = {
|
|
id: 'start',
|
|
type: this.sfuComponent,
|
|
role: this.role,
|
|
internalMeetingId: this.internalMeetingId,
|
|
voiceBridge: this.voiceBridge,
|
|
userName: this.userName,
|
|
callerName: this.userId,
|
|
sdpOffer: offer,
|
|
hasAudio: !!this.hasAudio,
|
|
contentType: this.contentType,
|
|
bitrate: this.bitrate,
|
|
mediaServer: this.mediaServer,
|
|
};
|
|
|
|
this.sendMessage(message);
|
|
}
|
|
|
|
_handleOfferGenerationFailure(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",
|
|
return this.onerror(error);
|
|
}
|
|
|
|
startScreensharing() {
|
|
return new Promise((resolve, reject) => {
|
|
try {
|
|
const options = {
|
|
onicecandidate: this.signalCandidates ? this.onIceCandidate.bind(this) : null,
|
|
videoStream: this.stream,
|
|
configuration: this.populatePeerConfiguration(),
|
|
trace: this.traceLogs,
|
|
networkPriorities: this.networkPriority ? { video: this.networkPriority } : undefined,
|
|
gatheringTimeout: this.gatheringTimeout,
|
|
};
|
|
this.webRtcPeer = new WebRtcPeer('sendonly', options);
|
|
this.webRtcPeer.iceQueue = [];
|
|
this.webRtcPeer.start();
|
|
this.webRtcPeer.peerConnection.onconnectionstatechange = () => {
|
|
this.handleConnectionStateChange('screenshare');
|
|
};
|
|
|
|
if (this.offering) {
|
|
this.webRtcPeer.generateOffer()
|
|
.then(this.sendStartReq.bind(this))
|
|
.catch(this._handleOfferGenerationFailure.bind(this));
|
|
} else {
|
|
this.sendStartReq();
|
|
}
|
|
|
|
resolve();
|
|
} catch (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);
|
|
reject(normalizedError);
|
|
}
|
|
});
|
|
}
|
|
|
|
onIceCandidate (candidate) {
|
|
const message = {
|
|
id: ON_ICE_CANDIDATE_MSG,
|
|
role: this.role,
|
|
type: this.sfuComponent,
|
|
voiceBridge: this.voiceBridge,
|
|
candidate,
|
|
callerName: this.userId,
|
|
};
|
|
|
|
this.sendMessage(message);
|
|
}
|
|
|
|
subscribeToScreenStream() {
|
|
return new Promise((resolve, reject) => {
|
|
try {
|
|
const options = {
|
|
mediaConstraints: {
|
|
audio: !!this.hasAudio,
|
|
},
|
|
onicecandidate: this.signalCandidates ? this.onIceCandidate.bind(this) : null,
|
|
configuration: this.populatePeerConfiguration(),
|
|
trace: this.traceLogs,
|
|
gatheringTimeout: this.gatheringTimeout,
|
|
};
|
|
|
|
this.webRtcPeer = new WebRtcPeer('recvonly', options);
|
|
this.webRtcPeer.iceQueue = [];
|
|
this.webRtcPeer.start();
|
|
this.webRtcPeer.peerConnection.onconnectionstatechange = () => {
|
|
this.handleConnectionStateChange('screenshare');
|
|
};
|
|
|
|
if (this.offering) {
|
|
this.webRtcPeer.generateOffer()
|
|
.then(this.sendStartReq.bind(this))
|
|
.catch(this._handleOfferGenerationFailure.bind(this));
|
|
} else {
|
|
this.sendStartReq();
|
|
}
|
|
|
|
resolve();
|
|
} catch (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);
|
|
reject(normalizedError);
|
|
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
export default ScreenshareBroker;
|