0e162f1cda
RTCRTPSender exposes DSCP marking via `networkPriority` in the encodings configuration dictionaries. That should allow us to control QoS priorities for different media streams, eg audio with higher network priority than video. The only browser that implements that right now is Chromium. To use this, the public.app.media.networkPriorities configuration in settings.yml. Audio, camera and screenshare priorities can be controlled separately. For further info on the possible values, see: - https://www.w3.org/TR/webrtc-priority/ - https://datatracker.ietf.org/doc/html/rfc8837#section-5
291 lines
8.0 KiB
JavaScript
291 lines
8.0 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.offering = true;
|
|
this.signalCandidates = true;
|
|
this.ending = false;
|
|
|
|
// Optional parameters are:
|
|
// userName,
|
|
// caleeName,
|
|
// iceServers,
|
|
// hasAudio,
|
|
// bitrate,
|
|
// offering,
|
|
// mediaServer,
|
|
// signalCandidates,
|
|
// traceLogs
|
|
// networkPriority
|
|
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,
|
|
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,
|
|
};
|
|
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,
|
|
};
|
|
|
|
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;
|