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

225 lines
6.9 KiB
JavaScript
Raw Normal View History

2017-10-13 03:22:10 +08:00
import VoiceUsers from '/imports/api/voice-users';
import { Tracker } from 'meteor/tracker';
import BaseAudioBridge from './base';
2017-08-01 04:54:18 +08:00
2017-10-12 05:04:10 +08:00
const STUN_TURN_FETCH_URL = Meteor.settings.public.media.stunTurnServersFetchAddress;
const MEDIA_TAG = Meteor.settings.public.media.mediaTag;
const CALL_TRANSFER_TIMEOUT = Meteor.settings.public.media.callTransferTimeout;
2017-10-12 05:04:10 +08:00
const handleStunTurnResponse = ({ result, stunServers, turnServers }) =>
new Promise((resolve) => {
if (result) {
resolve({ error: 404, stun: [], turn: [] });
}
resolve({
stun: stunServers.map(server => server.url),
turn: turnServers.map(server => server.url),
});
});
const fetchStunTurnServers = sessionToken =>
new Promise(async (resolve, reject) => {
const url = `${STUN_TURN_FETCH_URL}?sessionToken=${sessionToken}`;
const response = await fetch(url)
.then(res => res.json())
.then(json => handleStunTurnResponse(json));
if (response.error) return reject('Could not fetch the stuns/turns servers!');
return resolve(response);
});
const inviteUserAgent = (voiceBridge, server, userAgent, inputStream) => {
const options = {
media: {
stream: inputStream,
constraints: {
audio: true,
video: false,
},
render: {
remote: document.querySelector(MEDIA_TAG),
},
},
RTCConstraints: {
mandatory: {
OfferToReceiveAudio: true,
OfferToReceiveVideo: false,
},
},
};
return userAgent.invite(`sip:${voiceBridge}@${server}`, options);
};
2017-07-24 22:15:46 +08:00
export default class SIPBridge extends BaseAudioBridge {
constructor(userData) {
2017-10-12 05:04:10 +08:00
super(userData);
2017-09-30 04:42:34 +08:00
this.isConnected = false;
2017-10-12 05:04:10 +08:00
this.errorCodes = {
'Request Timeout': this.baseErrorCodes.REQUEST_TIMEOUT,
'Invalid Target': this.baseErrorCodes.INVALID_TARGET,
'Connection Error': this.baseErrorCodes.CONNECTION_ERROR,
};
2017-09-29 21:38:10 +08:00
}
2017-07-24 22:15:46 +08:00
2017-09-30 04:42:34 +08:00
joinAudio({ isListenOnly, extension, inputStream }, managerCallback) {
2017-10-05 04:49:11 +08:00
return new Promise((resolve, reject) => {
const callExtension = extension ? `${extension}${this.userData.voiceBridge}` : this.userData.voiceBridge;
2017-07-24 22:15:46 +08:00
2017-09-29 21:38:10 +08:00
const callback = (message) => {
2017-10-12 05:04:10 +08:00
managerCallback(message).then(resolve);
2017-09-30 04:42:34 +08:00
};
2017-07-24 22:15:46 +08:00
2017-10-12 05:30:38 +08:00
this.callback = callback;
return this.doCall({ callExtension, isListenOnly, inputStream }, callback)
.catch((reason) => {
2017-10-12 05:04:10 +08:00
callback({ status: this.baseCallStates.failed, error: reason });
2017-10-05 04:49:11 +08:00
reject(reason);
});
2017-09-30 04:42:34 +08:00
});
2017-07-24 22:15:46 +08:00
}
transferCall(onTransferStart, onTransferSuccess) {
return new Promise((resolve, reject) => {
onTransferStart();
this.currentSession.dtmf(1);
let trackerControl = null;
Tracker.autorun((c) => {
trackerControl = c;
const selector = { meetingId: this.userData.meetingId, intId: this.userData.userId };
const query = VoiceUsers.find(selector);
window.Kappa = query;
query.observeChanges({
changed: (id, fields) => {
if (fields.joined) {
clearTimeout(timeout);
onTransferSuccess();
c.stop();
resolve();
}
},
});
});
const timeout = setTimeout(() => {
clearTimeout(timeout);
trackerControl.stop();
this.callback({
status: this.baseCallStates.failed,
error: this.baseErrorCodes.REQUEST_TIMEOUT,
bridgeError: 'Could not transfer the call' });
reject('Call transfer timeout');
}, CALL_TRANSFER_TIMEOUT);
});
}
2017-09-29 21:38:10 +08:00
exitAudio() {
return new Promise((resolve) => {
2017-10-05 04:49:11 +08:00
this.currentSession.on('bye', () => {
resolve();
});
this.currentSession.bye();
2017-09-30 04:42:34 +08:00
});
}
doCall({ isListenOnly, callExtension, inputStream }, callback) {
2017-09-30 04:42:34 +08:00
const {
userId,
username,
sessionToken,
} = this.userData;
const server = window.document.location.hostname;
2017-10-13 05:39:32 +08:00
const callerIdPrefix = userId;
const callerIdSufix = isListenOnly ? `LINSTENONLY-${username}` : username;
const callerIdName = [
callerIdPrefix,
'bbbID',
callerIdSufix,
].join('-');
2017-09-30 04:42:34 +08:00
2017-10-12 05:04:10 +08:00
return fetchStunTurnServers(sessionToken)
.then(stunTurnServers =>
this.createUserAgent(server, callerIdName, stunTurnServers))
.then(userAgent =>
2017-10-12 05:04:10 +08:00
inviteUserAgent(callExtension, server, userAgent, inputStream))
.then(currentSession =>
this.setupEventHandlers(currentSession, callback));
2017-09-30 04:42:34 +08:00
}
createUserAgent(server, username, { stun, turn }) {
return new Promise((resolve, reject) => {
const protocol = document.location.protocol;
this.userAgent = new window.SIP.UA({
uri: `sip:${encodeURIComponent(username)}@${server}`,
2017-10-12 05:04:10 +08:00
wsServers: `${(protocol === 'https:' ? 'wss://' : 'ws://')}${server}/ws`,
2017-10-13 03:22:10 +08:00
// log: {
// builtinEnabled: false,
// },
2017-09-30 04:42:34 +08:00
displayName: username,
register: false,
traceSip: true,
autostart: false,
userAgentString: 'BigBlueButton',
stunServers: stun,
turnServers: turn,
});
this.userAgent.removeAllListeners('connected');
this.userAgent.removeAllListeners('disconnected');
2017-10-12 05:04:10 +08:00
this.userAgent.on('connected', () => this.handleUserAgentConnection(resolve));
this.userAgent.on('disconnected', () => this.handleUserAgentDisconnection(reject));
2017-09-30 04:42:34 +08:00
this.userAgent.start();
});
}
2017-10-12 05:04:10 +08:00
handleUserAgentConnection(resolve) {
this.isConnected = true;
resolve(this.userAgent);
}
2017-10-12 05:04:10 +08:00
handleUserAgentDisconnection(reject) {
this.userAgent.stop();
this.userAgent = null;
this.callback({
status: this.baseCallStates.failed,
error: this.baseErrorCodes.GENERIC_ERROR,
bridgeError: 'User Agent' });
2017-10-12 05:04:10 +08:00
reject('CONNECTION_ERROR');
2017-09-30 04:42:34 +08:00
}
setupEventHandlers(currentSession, callback) {
2017-10-13 03:22:10 +08:00
return new Promise((resolve) => {
2017-10-12 05:04:10 +08:00
currentSession.on('terminated', (message, cause) => this.handleSessionTerminated(message, cause, callback));
2017-10-05 04:49:11 +08:00
2017-10-13 03:22:10 +08:00
currentSession.mediaHandler.on('iceConnectionCompleted', () => this.handleConnectionCompleted(resolve));
currentSession.mediaHandler.on('iceConnectsionConnected', () => this.handleConnectionCompleted(resolve));
2017-10-05 04:49:11 +08:00
this.currentSession = currentSession;
2017-10-12 05:04:10 +08:00
});
}
2017-10-13 03:22:10 +08:00
handleConnectionCompleted(resolve) {
this.callback({ status: this.baseCallStates.started });
resolve();
2017-10-12 05:04:10 +08:00
}
handleSessionTerminated(message, cause, callback) {
if (!message && !cause) {
return callback({ status: this.baseCallStates.ended });
}
const mappedCause = cause in this.errorCodes ?
this.errorCodes[cause] :
this.baseErrorCodes.GENERIC_ERROR;
return callback({ status: this.baseCallStates.failed, error: mappedCause, bridgeError: cause });
2017-07-24 22:15:46 +08:00
}
}