bigbluebutton-Github/bigbluebutton-html5/imports/api/audio/client/bridge/kurento.js

247 lines
8.9 KiB
JavaScript
Raw Normal View History

import BaseAudioBridge from './base';
import Auth from '/imports/ui/services/auth';
import { fetchWebRTCMappedStunTurnServers } from '/imports/utils/fetchStunTurnServers';
import { playAndRetry } from '/imports/utils/mediaElementPlayRetry';
import logger from '/imports/startup/client/logger';
const SFU_URL = Meteor.settings.public.kurento.wsUrl;
const MEDIA = Meteor.settings.public.media;
const MEDIA_TAG = MEDIA.mediaTag.replace(/#/g, '');
const GLOBAL_AUDIO_PREFIX = 'GLOBAL_AUDIO_';
const RECONNECT_TIMEOUT_MS = 15000;
export default class KurentoAudioBridge extends BaseAudioBridge {
constructor(userData) {
super();
const {
userId,
username,
voiceBridge,
meetingId,
sessionToken,
} = userData;
this.user = {
userId,
name: username,
sessionToken,
};
this.media = {
inputDevice: {},
};
this.internalMeetingID = meetingId;
this.voiceBridge = voiceBridge;
this.reconnectOngoing = false;
this.hasSuccessfullyStarted = false;
}
static normalizeError(error = {}) {
const errorMessage = error.message || error.name || error.reason || 'Unknown error';
const errorCode = error.code || 'Undefined code';
const errorReason = error.reason || error.id || 'Undefined reason';
return { errorMessage, errorCode, errorReason };
}
joinAudio({ isListenOnly, inputStream }, callback) {
return new Promise(async (resolve, reject) => {
this.callback = callback;
let iceServers = [];
try {
iceServers = await fetchWebRTCMappedStunTurnServers(this.user.sessionToken);
} catch (error) {
logger.error({ logCode: 'sfuaudiobridge_stunturn_fetch_failed' },
'SFU audio bridge failed to fetch STUN/TURN info, using default servers');
} finally {
logger.debug({ logCode: 'sfuaudiobridge_stunturn_fetch_sucess', extraInfo: { iceServers } },
'SFU audio bridge got STUN/TURN servers');
const options = {
wsUrl: Auth.authenticateURL(SFU_URL),
userName: this.user.name,
caleeName: `${GLOBAL_AUDIO_PREFIX}${this.voiceBridge}`,
iceServers,
logger,
inputStream,
};
const audioTag = document.getElementById(MEDIA_TAG);
const playElement = () => {
const mediaTagPlayed = () => {
logger.info({
logCode: 'listenonly_media_play_success',
}, 'Listen only media played successfully');
resolve(this.callback({ status: this.baseCallStates.started }));
};
if (audioTag.paused) {
// Tag isn't playing yet. Play it.
audioTag.play()
.then(mediaTagPlayed)
.catch((error) => {
// NotAllowedError equals autoplay issues, fire autoplay handling event.
// This will be handled in audio-manager.
if (error.name === 'NotAllowedError') {
logger.error({
logCode: 'listenonly_error_autoplay',
extraInfo: { errorName: error.name },
}, 'Listen only media play failed due to autoplay error');
const tagFailedEvent = new CustomEvent('audioPlayFailed', { detail: { mediaElement: audioTag } });
window.dispatchEvent(tagFailedEvent);
resolve(this.callback({
status: this.baseCallStates.autoplayBlocked,
}));
} else {
// Tag failed for reasons other than autoplay. Log the error and
// try playing again a few times until it works or fails for good
const played = playAndRetry(audioTag);
if (!played) {
logger.error({
logCode: 'listenonly_error_media_play_failed',
extraInfo: { errorName: error.name },
}, `Listen only media play failed due to ${error.name}`);
} else {
mediaTagPlayed();
}
}
});
} else {
// Media tag is already playing, so log a success. This is really a
// logging fallback for a case that shouldn't happen. But if it does
// (ie someone re-enables the autoPlay prop in the element), then it
// means the stream is playing properly and it'll be logged.
mediaTagPlayed();
}
};
const onSuccess = () => {
const { webRtcPeer } = window.kurentoManager.kurentoAudio;
this.hasSuccessfullyStarted = true;
if (webRtcPeer) {
const stream = webRtcPeer.getRemoteStream();
audioTag.pause();
audioTag.srcObject = stream;
audioTag.muted = false;
playElement();
} else {
this.callback({
status: this.baseCallStates.failed,
error: this.baseErrorCodes.CONNECTION_ERROR,
});
}
if (this.reconnectOngoing) {
this.reconnectOngoing = false;
clearTimeout(this.reconnectTimeout);
}
};
const onFail = (error) => {
const { errorMessage, errorCode, errorReason } = KurentoAudioBridge.normalizeError(error);
// Listen only connected successfully already and dropped mid-call.
// Try to reconnect ONCE (binded to reconnectOngoing flag)
if (this.hasSuccessfullyStarted && !this.reconnectOngoing) {
logger.error({
logCode: 'listenonly_error_try_to_reconnect',
extraInfo: { errorMessage, errorCode, errorReason },
}, 'Listen only failed for an ongoing session, try to reconnect');
window.kurentoExitAudio();
this.callback({ status: this.baseCallStates.reconnecting });
this.reconnectOngoing = true;
// Set up a reconnectionTimeout in case the server is unresponsive
// for some reason. If it gets triggered, end the session and stop
// trying to reconnect
this.reconnectTimeout = setTimeout(() => {
this.callback({
status: this.baseCallStates.failed,
error: this.baseErrorCodes.CONNECTION_ERROR,
});
this.reconnectOngoing = false;
this.hasSuccessfullyStarted = false;
window.kurentoExitAudio();
}, RECONNECT_TIMEOUT_MS);
window.kurentoJoinAudio(
MEDIA_TAG,
this.voiceBridge,
this.user.userId,
this.internalMeetingID,
onFail,
onSuccess,
options,
);
} else {
// Already tried reconnecting once OR the user handn't succesfully
// connected firsthand. Just finish the session and reject with error
if (!this.reconnectOngoing) {
logger.error({
logCode: 'listenonly_error_failed_to_connect',
extraInfo: { errorMessage, errorCode, errorReason },
}, `Listen only failed when trying to start due to ${errorMessage}`);
} else {
logger.error({
logCode: 'listenonly_error_reconnect_failed',
extraInfo: { errorMessage, errorCode, errorReason },
}, `Listen only failed when trying to reconnect due to ${errorMessage}`);
}
this.reconnectOngoing = false;
this.hasSuccessfullyStarted = false;
window.kurentoExitAudio();
this.callback({
status: this.baseCallStates.failed,
error: this.baseErrorCodes.CONNECTION_ERROR,
bridgeError: errorReason,
});
reject(errorReason);
}
};
if (!isListenOnly) {
return reject(new Error('Invalid bridge option'));
}
window.kurentoJoinAudio(
MEDIA_TAG,
this.voiceBridge,
this.user.userId,
this.internalMeetingID,
onFail,
onSuccess,
options,
);
}
});
}
async changeOutputDevice(value) {
const audioContext = document.querySelector(`#${MEDIA_TAG}`);
if (audioContext.setSinkId) {
try {
await audioContext.setSinkId(value);
this.media.outputDeviceId = value;
} catch (error) {
logger.error({ logCode: 'sfuaudiobridge_changeoutputdevice_error', extraInfo: { error } },
'SFU audio bridge failed to fetch STUN/TURN info, using default');
throw new Error(this.baseErrorCodes.MEDIA_ERROR);
}
}
return this.media.outputDeviceId || value;
}
exitAudio() {
return new Promise((resolve) => {
this.hasSuccessfullyStarted = false;
window.kurentoExitAudio();
return resolve(this.callback({ status: this.baseCallStates.ended }));
});
}
}