2017-10-13 03:22:10 +08:00
|
|
|
import VoiceUsers from '/imports/api/voice-users';
|
2017-10-12 22:49:50 +08:00
|
|
|
import { Tracker } from 'meteor/tracker';
|
|
|
|
import BaseAudioBridge from './base';
|
2017-08-01 04:54:18 +08:00
|
|
|
|
2017-10-18 03:16:42 +08:00
|
|
|
const MEDIA = Meteor.settings.public.media;
|
|
|
|
const STUN_TURN_FETCH_URL = MEDIA.stunTurnServersFetchAddress;
|
|
|
|
const MEDIA_TAG = MEDIA.mediaTag;
|
|
|
|
const CALL_TRANSFER_TIMEOUT = MEDIA.callTransferTimeout;
|
2017-10-12 05:04:10 +08:00
|
|
|
|
|
|
|
const fetchStunTurnServers = sessionToken =>
|
|
|
|
new Promise(async (resolve, reject) => {
|
2017-10-18 03:16:42 +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),
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2017-10-12 05:04:10 +08:00
|
|
|
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);
|
|
|
|
});
|
|
|
|
|
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-10-18 03:16:42 +08:00
|
|
|
const {
|
|
|
|
userId,
|
|
|
|
username,
|
|
|
|
sessionToken,
|
|
|
|
} = userData;
|
|
|
|
|
|
|
|
this.user = {
|
|
|
|
userId,
|
|
|
|
sessionToken,
|
|
|
|
name: username,
|
|
|
|
};
|
|
|
|
|
|
|
|
this.media = {
|
|
|
|
inputDevice: {},
|
|
|
|
};
|
|
|
|
|
|
|
|
this.protocol = window.document.location.protocol;
|
|
|
|
this.hostname = window.document.location.hostname;
|
2017-10-19 03:40:01 +08:00
|
|
|
const causes = window.SIP.C.causes
|
2017-10-12 05:04:10 +08:00
|
|
|
this.errorCodes = {
|
2017-10-19 03:40:01 +08:00
|
|
|
[causes.REQUEST_TIMEOUT]: this.baseErrorCodes.REQUEST_TIMEOUT,
|
|
|
|
[causes.INVALID_TARGET]: this.baseErrorCodes.INVALID_TARGET,
|
|
|
|
[causes.CONNECTION_ERROR]: this.baseErrorCodes.CONNECTION_ERROR,
|
2017-10-12 05:04:10 +08:00
|
|
|
};
|
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) => {
|
2017-10-12 22:49:50 +08:00
|
|
|
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;
|
|
|
|
|
2017-10-11 02:03:29 +08:00
|
|
|
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
|
|
|
}
|
|
|
|
|
2017-10-18 03:16:42 +08:00
|
|
|
transferCall(onTransferSuccess) {
|
2017-10-12 22:49:50 +08:00
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
let trackerControl = null;
|
2017-10-18 03:16:42 +08:00
|
|
|
|
|
|
|
const timeout = setTimeout(() => {
|
|
|
|
clearTimeout(timeout);
|
|
|
|
trackerControl.stop();
|
|
|
|
this.callback({
|
|
|
|
status: this.baseCallStates.failed,
|
|
|
|
error: this.baseErrorCodes.REQUEST_TIMEOUT,
|
|
|
|
bridgeError: 'Timeout on call transfer' });
|
|
|
|
reject('Timeout on call transfer');
|
|
|
|
}, CALL_TRANSFER_TIMEOUT);
|
|
|
|
|
|
|
|
// This is is the call transfer code ask @chadpilkey
|
|
|
|
this.currentSession.dtmf(1);
|
|
|
|
|
2017-10-12 20:50:23 +08:00
|
|
|
Tracker.autorun((c) => {
|
2017-10-12 22:49:50 +08:00
|
|
|
trackerControl = c;
|
2017-10-12 20:50:23 +08:00
|
|
|
const selector = { meetingId: this.userData.meetingId, intId: this.userData.userId };
|
|
|
|
const query = VoiceUsers.find(selector);
|
|
|
|
|
|
|
|
query.observeChanges({
|
|
|
|
changed: (id, fields) => {
|
|
|
|
if (fields.joined) {
|
2017-10-12 22:49:50 +08:00
|
|
|
clearTimeout(timeout);
|
2017-10-12 20:50:23 +08:00
|
|
|
onTransferSuccess();
|
|
|
|
c.stop();
|
|
|
|
resolve();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
});
|
|
|
|
});
|
2017-10-12 22:49:50 +08:00
|
|
|
});
|
2017-10-12 20:50:23 +08:00
|
|
|
}
|
|
|
|
|
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
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-10-18 03:16:42 +08:00
|
|
|
doCall(options) {
|
|
|
|
const {
|
|
|
|
isListenOnly,
|
|
|
|
} = options;
|
|
|
|
|
2017-09-30 04:42:34 +08:00
|
|
|
const {
|
|
|
|
userId,
|
2017-10-18 03:16:42 +08:00
|
|
|
name,
|
2017-09-30 04:42:34 +08:00
|
|
|
sessionToken,
|
2017-10-18 03:16:42 +08:00
|
|
|
} = this.user;
|
2017-10-13 05:39:32 +08:00
|
|
|
|
|
|
|
const callerIdName = [
|
2017-10-18 03:16:42 +08:00
|
|
|
userId,
|
2017-10-13 05:39:32 +08:00
|
|
|
'bbbID',
|
2017-10-18 03:16:42 +08:00
|
|
|
isListenOnly ? `LISTENONLY-${name}` : name,
|
2017-10-13 05:39:32 +08:00
|
|
|
].join('-');
|
2017-09-30 04:42:34 +08:00
|
|
|
|
2017-10-18 03:16:42 +08:00
|
|
|
this.user.callerIdName = callerIdName;
|
|
|
|
this.callOptions = options;
|
|
|
|
|
2017-10-12 05:04:10 +08:00
|
|
|
return fetchStunTurnServers(sessionToken)
|
2017-10-18 03:16:42 +08:00
|
|
|
.then(this.createUserAgent.bind(this))
|
|
|
|
.then(this.inviteUserAgent.bind(this))
|
|
|
|
.then(this.setupEventHandlers.bind(this));
|
2017-09-30 04:42:34 +08:00
|
|
|
}
|
|
|
|
|
2017-10-18 03:16:42 +08:00
|
|
|
createUserAgent({ stun, turn }) {
|
2017-09-30 04:42:34 +08:00
|
|
|
return new Promise((resolve, reject) => {
|
2017-10-18 03:16:42 +08:00
|
|
|
const {
|
|
|
|
hostname,
|
|
|
|
protocol,
|
|
|
|
} = this;
|
|
|
|
|
|
|
|
const {
|
|
|
|
callerIdName,
|
|
|
|
} = this.user;
|
|
|
|
|
|
|
|
let userAgent = new window.SIP.UA({
|
|
|
|
uri: `sip:${encodeURIComponent(callerIdName)}@${hostname}`,
|
|
|
|
wsServers: `${(protocol === 'https:' ? 'wss://' : 'ws://')}${hostname}/ws`,
|
|
|
|
log: {
|
|
|
|
builtinEnabled: false,
|
|
|
|
},
|
|
|
|
displayName: callerIdName,
|
2017-09-30 04:42:34 +08:00
|
|
|
register: false,
|
|
|
|
traceSip: true,
|
|
|
|
autostart: false,
|
|
|
|
userAgentString: 'BigBlueButton',
|
|
|
|
stunServers: stun,
|
|
|
|
turnServers: turn,
|
|
|
|
});
|
|
|
|
|
2017-10-18 03:16:42 +08:00
|
|
|
userAgent.removeAllListeners('connected');
|
|
|
|
userAgent.removeAllListeners('disconnected');
|
|
|
|
|
|
|
|
const handleUserAgentConnection = () => {
|
|
|
|
resolve(userAgent);
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleUserAgentDisconnection = () => {
|
|
|
|
userAgent.stop();
|
|
|
|
userAgent = null;
|
|
|
|
this.callback({
|
|
|
|
status: this.baseCallStates.failed,
|
|
|
|
error: this.baseErrorCodes.GENERIC_ERROR,
|
|
|
|
bridgeError: 'User Agent' });
|
|
|
|
reject('CONNECTION_ERROR');
|
|
|
|
};
|
2017-09-30 04:42:34 +08:00
|
|
|
|
2017-10-18 03:16:42 +08:00
|
|
|
userAgent.on('connected', handleUserAgentConnection);
|
|
|
|
userAgent.on('disconnected', handleUserAgentDisconnection);
|
2017-09-30 04:42:34 +08:00
|
|
|
|
2017-10-18 03:16:42 +08:00
|
|
|
userAgent.start();
|
2017-09-30 04:42:34 +08:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-10-18 03:16:42 +08:00
|
|
|
inviteUserAgent(userAgent) {
|
|
|
|
const {
|
|
|
|
hostname,
|
|
|
|
} = this;
|
|
|
|
|
|
|
|
const {
|
|
|
|
inputStream,
|
|
|
|
callExtension,
|
|
|
|
} = this.callOptions;
|
|
|
|
|
|
|
|
const options = {
|
|
|
|
media: {
|
|
|
|
stream: inputStream,
|
|
|
|
constraints: {
|
|
|
|
audio: true,
|
|
|
|
video: false,
|
|
|
|
},
|
|
|
|
render: {
|
|
|
|
remote: document.querySelector(MEDIA_TAG),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
RTCConstraints: {
|
|
|
|
mandatory: {
|
|
|
|
OfferToReceiveAudio: true,
|
|
|
|
OfferToReceiveVideo: false,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
};
|
2017-10-11 02:03:29 +08:00
|
|
|
|
2017-10-18 03:16:42 +08:00
|
|
|
return userAgent.invite(`sip:${callExtension}@${hostname}`, options);
|
2017-09-30 04:42:34 +08:00
|
|
|
}
|
|
|
|
|
2017-10-18 03:16:42 +08:00
|
|
|
setupEventHandlers(currentSession) {
|
2017-10-13 03:22:10 +08:00
|
|
|
return new Promise((resolve) => {
|
2017-10-18 03:16:42 +08:00
|
|
|
const handleConnectionCompleted = () => {
|
|
|
|
this.callback({ status: this.baseCallStates.started });
|
|
|
|
resolve();
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleSessionTerminated = (message, cause) => {
|
|
|
|
if (!message && !cause) {
|
|
|
|
return this.callback({
|
|
|
|
status: this.baseCallStates.ended,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
const mappedCause = cause in this.errorCodes ?
|
|
|
|
this.errorCodes[cause] :
|
|
|
|
this.baseErrorCodes.GENERIC_ERROR;
|
|
|
|
return this.callback({
|
|
|
|
status: this.baseCallStates.failed,
|
|
|
|
error: mappedCause,
|
|
|
|
bridgeError: cause,
|
|
|
|
});
|
|
|
|
};
|
2017-10-05 04:49:11 +08:00
|
|
|
|
2017-10-18 03:16:42 +08:00
|
|
|
currentSession.on('terminated', handleSessionTerminated);
|
|
|
|
currentSession.mediaHandler.on('iceConnectionCompleted', handleConnectionCompleted);
|
|
|
|
currentSession.mediaHandler.on('iceConnectsionConnected', handleConnectionCompleted);
|
2017-10-10 04:48:10 +08:00
|
|
|
|
2017-10-05 04:49:11 +08:00
|
|
|
this.currentSession = currentSession;
|
2017-10-12 05:04:10 +08:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-10-18 03:16:42 +08:00
|
|
|
async changeInputDevice(value) {
|
|
|
|
const {
|
|
|
|
media,
|
|
|
|
} = this;
|
|
|
|
|
|
|
|
const getMediaStream = constraints =>
|
|
|
|
navigator.mediaDevices.getUserMedia(constraints);
|
|
|
|
|
|
|
|
if (!value) {
|
|
|
|
const mediaStream = await getMediaStream({ audio: true });
|
|
|
|
const deviceLabel = mediaStream.getAudioTracks()[0].label;
|
|
|
|
const mediaDevices = await navigator.mediaDevices.enumerateDevices();
|
|
|
|
const device = mediaDevices.find(d => d.label === deviceLabel);
|
|
|
|
return this.changeInputDevice(device.deviceId);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (media.inputDevice.audioContext) {
|
|
|
|
media.inputDevice.audioContext.close().then(() => {
|
|
|
|
media.inputDevice.audioContext = null;
|
|
|
|
media.inputDevice.scriptProcessor = null;
|
|
|
|
media.inputDevice.source = null;
|
|
|
|
return this.changeInputDevice(value);
|
|
|
|
});
|
|
|
|
}
|
2017-10-12 05:04:10 +08:00
|
|
|
|
2017-10-18 03:16:42 +08:00
|
|
|
media.inputDevice.id = value;
|
|
|
|
if ('AudioContext' in window) {
|
|
|
|
media.inputDevice.audioContext = new window.AudioContext();
|
|
|
|
} else {
|
|
|
|
media.inputDevice.audioContext = new window.webkitAudioContext();
|
2017-10-12 05:04:10 +08:00
|
|
|
}
|
2017-10-18 03:16:42 +08:00
|
|
|
media.inputDevice.scriptProcessor = media.inputDevice.audioContext
|
|
|
|
.createScriptProcessor(2048, 1, 1);
|
|
|
|
media.inputDevice.source = null;
|
|
|
|
|
|
|
|
const constraints = {
|
|
|
|
audio: {
|
|
|
|
deviceId: value,
|
|
|
|
},
|
|
|
|
};
|
2017-10-12 05:04:10 +08:00
|
|
|
|
2017-10-18 03:16:42 +08:00
|
|
|
const mediaStream = await getMediaStream(constraints);
|
|
|
|
media.inputDevice.stream = mediaStream;
|
|
|
|
media.inputDevice.source = media.inputDevice.audioContext.createMediaStreamSource(mediaStream);
|
|
|
|
media.inputDevice.source.connect(media.inputDevice.scriptProcessor);
|
|
|
|
media.inputDevice.scriptProcessor.connect(media.inputDevice.audioContext.destination);
|
|
|
|
|
|
|
|
return this.media.inputDevice;
|
|
|
|
}
|
|
|
|
|
|
|
|
changeOutputDevice(value) {
|
|
|
|
const audioContext = document.querySelector(MEDIA_TAG);
|
|
|
|
|
|
|
|
if (audioContext.setSinkId) {
|
2017-10-19 03:40:01 +08:00
|
|
|
audioContext.setSinkId(value);
|
2017-10-18 03:16:42 +08:00
|
|
|
}
|
2017-10-19 03:40:01 +08:00
|
|
|
|
|
|
|
return value;
|
2017-07-24 22:15:46 +08:00
|
|
|
}
|
|
|
|
}
|