2018-04-28 05:37:41 +08:00
|
|
|
import BaseAudioBridge from './base';
|
2018-07-19 05:04:20 +08:00
|
|
|
import Auth from '/imports/ui/services/auth';
|
2020-05-21 12:20:46 +08:00
|
|
|
import { fetchWebRTCMappedStunTurnServers, getMappedFallbackStun } from '/imports/utils/fetchStunTurnServers';
|
2019-09-07 04:54:48 +08:00
|
|
|
import playAndRetry from '/imports/utils/mediaElementPlayRetry';
|
2018-07-27 02:26:56 +08:00
|
|
|
import logger from '/imports/startup/client/logger';
|
2018-04-28 05:37:41 +08:00
|
|
|
|
2018-07-10 05:29:27 +08:00
|
|
|
const SFU_URL = Meteor.settings.public.kurento.wsUrl;
|
2018-04-29 08:18:54 +08:00
|
|
|
const MEDIA = Meteor.settings.public.media;
|
2018-05-08 01:17:48 +08:00
|
|
|
const MEDIA_TAG = MEDIA.mediaTag.replace(/#/g, '');
|
2019-04-11 04:48:10 +08:00
|
|
|
const GLOBAL_AUDIO_PREFIX = 'GLOBAL_AUDIO_';
|
2019-07-26 17:28:11 +08:00
|
|
|
const RECONNECT_TIMEOUT_MS = 15000;
|
2018-04-29 08:18:54 +08:00
|
|
|
|
2018-04-28 05:37:41 +08:00
|
|
|
export default class KurentoAudioBridge extends BaseAudioBridge {
|
|
|
|
constructor(userData) {
|
|
|
|
super();
|
|
|
|
const {
|
|
|
|
userId,
|
|
|
|
username,
|
|
|
|
voiceBridge,
|
2018-06-29 02:59:36 +08:00
|
|
|
meetingId,
|
2018-07-10 05:29:27 +08:00
|
|
|
sessionToken,
|
2018-04-28 05:37:41 +08:00
|
|
|
} = userData;
|
|
|
|
|
2018-05-08 01:17:48 +08:00
|
|
|
this.user = {
|
|
|
|
userId,
|
|
|
|
name: username,
|
2019-04-11 04:48:10 +08:00
|
|
|
sessionToken,
|
2018-05-08 01:17:48 +08:00
|
|
|
};
|
|
|
|
|
2018-08-30 03:12:34 +08:00
|
|
|
this.media = {
|
|
|
|
inputDevice: {},
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2018-06-29 02:59:36 +08:00
|
|
|
this.internalMeetingID = meetingId;
|
2018-04-28 05:37:41 +08:00
|
|
|
this.voiceBridge = voiceBridge;
|
2019-07-26 17:28:11 +08:00
|
|
|
this.reconnectOngoing = false;
|
|
|
|
this.hasSuccessfullyStarted = false;
|
2018-04-28 05:37:41 +08:00
|
|
|
}
|
|
|
|
|
2019-09-07 02:58:22 +08:00
|
|
|
static normalizeError(error = {}) {
|
2019-09-11 00:20:40 +08:00
|
|
|
const errorMessage = error.name || error.message || error.reason || 'Unknown error';
|
|
|
|
const errorCode = error.code || undefined;
|
2019-12-03 06:15:46 +08:00
|
|
|
let errorReason = error.reason || error.id || 'Undefined reason';
|
|
|
|
|
|
|
|
// HOPEFULLY TEMPORARY
|
|
|
|
// The errors are often just strings so replace the errorReason if that's the case
|
|
|
|
if (typeof error === 'string') {
|
|
|
|
errorReason = error;
|
|
|
|
}
|
|
|
|
// END OF HOPEFULLY TEMPORARY
|
2019-09-07 02:58:22 +08:00
|
|
|
|
|
|
|
return { errorMessage, errorCode, errorReason };
|
|
|
|
}
|
|
|
|
|
2019-09-11 00:20:40 +08:00
|
|
|
|
2018-08-28 23:24:06 +08:00
|
|
|
joinAudio({ isListenOnly, inputStream }, callback) {
|
2018-07-10 05:29:27 +08:00
|
|
|
return new Promise(async (resolve, reject) => {
|
2018-04-29 08:18:54 +08:00
|
|
|
this.callback = callback;
|
2018-07-10 05:29:27 +08:00
|
|
|
let iceServers = [];
|
2018-05-07 21:39:39 +08:00
|
|
|
|
2018-07-10 05:29:27 +08:00
|
|
|
try {
|
|
|
|
iceServers = await fetchWebRTCMappedStunTurnServers(this.user.sessionToken);
|
|
|
|
} catch (error) {
|
2019-07-16 04:59:00 +08:00
|
|
|
logger.error({ logCode: 'sfuaudiobridge_stunturn_fetch_failed' },
|
|
|
|
'SFU audio bridge failed to fetch STUN/TURN info, using default servers');
|
2020-05-21 12:20:46 +08:00
|
|
|
iceServers = getMappedFallbackStun();
|
2018-07-10 05:29:27 +08:00
|
|
|
} finally {
|
2019-07-16 04:59:00 +08:00
|
|
|
logger.debug({ logCode: 'sfuaudiobridge_stunturn_fetch_sucess', extraInfo: { iceServers } },
|
2019-07-26 07:29:52 +08:00
|
|
|
'SFU audio bridge got STUN/TURN servers');
|
2018-07-10 05:29:27 +08:00
|
|
|
const options = {
|
2019-01-08 00:54:37 +08:00
|
|
|
wsUrl: Auth.authenticateURL(SFU_URL),
|
2018-07-10 05:29:27 +08:00
|
|
|
userName: this.user.name,
|
|
|
|
caleeName: `${GLOBAL_AUDIO_PREFIX}${this.voiceBridge}`,
|
|
|
|
iceServers,
|
2019-07-16 04:59:00 +08:00
|
|
|
logger,
|
2018-08-28 23:24:06 +08:00
|
|
|
inputStream,
|
2018-07-10 05:29:27 +08:00
|
|
|
};
|
2018-05-07 21:39:39 +08:00
|
|
|
|
2019-09-07 02:58:22 +08:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2019-04-11 04:48:10 +08:00
|
|
|
const onSuccess = () => {
|
2018-10-04 02:39:55 +08:00
|
|
|
const { webRtcPeer } = window.kurentoManager.kurentoAudio;
|
2019-07-26 17:28:11 +08:00
|
|
|
|
|
|
|
this.hasSuccessfullyStarted = true;
|
2018-10-04 02:39:55 +08:00
|
|
|
if (webRtcPeer) {
|
|
|
|
const stream = webRtcPeer.getRemoteStream();
|
|
|
|
audioTag.pause();
|
|
|
|
audioTag.srcObject = stream;
|
|
|
|
audioTag.muted = false;
|
2019-09-07 02:58:22 +08:00
|
|
|
playElement();
|
2019-08-03 05:32:42 +08:00
|
|
|
} else {
|
|
|
|
this.callback({
|
|
|
|
status: this.baseCallStates.failed,
|
|
|
|
error: this.baseErrorCodes.CONNECTION_ERROR,
|
2019-12-03 06:15:46 +08:00
|
|
|
bridgeError: 'No WebRTC Peer',
|
2019-07-26 01:36:19 +08:00
|
|
|
});
|
2018-10-04 02:39:55 +08:00
|
|
|
}
|
2019-07-26 17:28:11 +08:00
|
|
|
|
|
|
|
if (this.reconnectOngoing) {
|
|
|
|
this.reconnectOngoing = false;
|
|
|
|
clearTimeout(this.reconnectTimeout);
|
|
|
|
}
|
2018-10-04 02:39:55 +08:00
|
|
|
};
|
2018-07-10 05:29:27 +08:00
|
|
|
|
2019-04-11 04:48:10 +08:00
|
|
|
const onFail = (error) => {
|
2019-09-07 02:58:22 +08:00
|
|
|
const { errorMessage, errorCode, errorReason } = KurentoAudioBridge.normalizeError(error);
|
2019-12-03 06:15:46 +08:00
|
|
|
|
2019-07-26 17:28:11 +08:00
|
|
|
// Listen only connected successfully already and dropped mid-call.
|
|
|
|
// Try to reconnect ONCE (binded to reconnectOngoing flag)
|
|
|
|
if (this.hasSuccessfullyStarted && !this.reconnectOngoing) {
|
|
|
|
logger.error({
|
2019-09-07 02:58:22 +08:00
|
|
|
logCode: 'listenonly_error_try_to_reconnect',
|
|
|
|
extraInfo: { errorMessage, errorCode, errorReason },
|
2019-12-03 06:15:46 +08:00
|
|
|
}, `Listen only failed for an ongoing session, try to reconnect. - reason: ${errorReason}`);
|
2019-07-26 17:28:11 +08:00
|
|
|
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,
|
2019-12-03 06:15:46 +08:00
|
|
|
bridgeError: 'Reconnect Timeout',
|
2019-07-26 17:28:11 +08:00
|
|
|
});
|
|
|
|
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
|
2019-09-07 02:58:22 +08:00
|
|
|
if (!this.reconnectOngoing) {
|
|
|
|
logger.error({
|
|
|
|
logCode: 'listenonly_error_failed_to_connect',
|
|
|
|
extraInfo: { errorMessage, errorCode, errorReason },
|
2019-12-03 06:15:46 +08:00
|
|
|
}, `Listen only failed when trying to start due to ${errorReason}`);
|
2019-09-07 02:58:22 +08:00
|
|
|
} else {
|
|
|
|
logger.error({
|
|
|
|
logCode: 'listenonly_error_reconnect_failed',
|
|
|
|
extraInfo: { errorMessage, errorCode, errorReason },
|
2019-12-03 06:15:46 +08:00
|
|
|
}, `Listen only failed when trying to reconnect due to ${errorReason}`);
|
2019-09-07 02:58:22 +08:00
|
|
|
}
|
|
|
|
|
2019-07-26 17:28:11 +08:00
|
|
|
this.reconnectOngoing = false;
|
|
|
|
this.hasSuccessfullyStarted = false;
|
|
|
|
window.kurentoExitAudio();
|
|
|
|
|
|
|
|
this.callback({
|
|
|
|
status: this.baseCallStates.failed,
|
|
|
|
error: this.baseErrorCodes.CONNECTION_ERROR,
|
2019-09-07 02:58:22 +08:00
|
|
|
bridgeError: errorReason,
|
2019-07-26 17:28:11 +08:00
|
|
|
});
|
2018-08-30 10:41:02 +08:00
|
|
|
|
2019-09-07 02:58:22 +08:00
|
|
|
reject(errorReason);
|
2019-07-26 17:28:11 +08:00
|
|
|
}
|
2018-08-30 10:41:02 +08:00
|
|
|
};
|
2018-04-29 08:18:54 +08:00
|
|
|
|
2018-07-10 05:29:27 +08:00
|
|
|
if (!isListenOnly) {
|
2019-09-07 02:58:22 +08:00
|
|
|
return reject(new Error('Invalid bridge option'));
|
2018-07-10 05:29:27 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
window.kurentoJoinAudio(
|
|
|
|
MEDIA_TAG,
|
|
|
|
this.voiceBridge,
|
|
|
|
this.user.userId,
|
|
|
|
this.internalMeetingID,
|
|
|
|
onFail,
|
|
|
|
onSuccess,
|
2019-04-11 04:48:10 +08:00
|
|
|
options,
|
2018-07-10 05:29:27 +08:00
|
|
|
);
|
|
|
|
}
|
2018-04-29 08:18:54 +08:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-08-30 03:12:34 +08:00
|
|
|
async changeOutputDevice(value) {
|
2019-04-11 04:48:10 +08:00
|
|
|
const audioContext = document.querySelector(`#${MEDIA_TAG}`);
|
2018-08-30 03:12:34 +08:00
|
|
|
if (audioContext.setSinkId) {
|
|
|
|
try {
|
|
|
|
await audioContext.setSinkId(value);
|
|
|
|
this.media.outputDeviceId = value;
|
2019-07-16 04:59:00 +08:00
|
|
|
} catch (error) {
|
2019-07-26 07:29:52 +08:00
|
|
|
logger.error({ logCode: 'sfuaudiobridge_changeoutputdevice_error', extraInfo: { error } },
|
2019-07-16 04:59:00 +08:00
|
|
|
'SFU audio bridge failed to fetch STUN/TURN info, using default');
|
2018-08-30 03:12:34 +08:00
|
|
|
throw new Error(this.baseErrorCodes.MEDIA_ERROR);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.media.outputDeviceId || value;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-04-29 08:18:54 +08:00
|
|
|
exitAudio() {
|
2019-04-11 04:48:10 +08:00
|
|
|
return new Promise((resolve) => {
|
2019-07-26 17:28:11 +08:00
|
|
|
this.hasSuccessfullyStarted = false;
|
2018-04-29 08:18:54 +08:00
|
|
|
window.kurentoExitAudio();
|
2018-05-07 21:39:39 +08:00
|
|
|
return resolve(this.callback({ status: this.baseCallStates.ended }));
|
2018-04-29 08:18:54 +08:00
|
|
|
});
|
2018-04-28 05:37:41 +08:00
|
|
|
}
|
|
|
|
}
|