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

406 lines
12 KiB
JavaScript
Raw Normal View History

2018-06-12 04:20:32 +08:00
import _ from 'lodash';
2017-10-13 03:22:10 +08:00
import VoiceUsers from '/imports/api/voice-users';
import { Tracker } from 'meteor/tracker';
import BaseAudioBridge from './base';
import logger from '/imports/startup/client/logger';
import { fetchStunTurnServers } from '/imports/utils/fetchStunTurnServers';
2017-08-01 04:54:18 +08:00
2017-10-18 03:16:42 +08:00
const MEDIA = Meteor.settings.public.media;
const MEDIA_TAG = MEDIA.mediaTag;
const CALL_TRANSFER_TIMEOUT = MEDIA.callTransferTimeout;
2017-10-27 01:14:56 +08:00
const CALL_HANGUP_TIMEOUT = MEDIA.callHangupTimeout;
const CALL_HANGUP_MAX_RETRIES = MEDIA.callHangupMaximumRetries;
const CONNECTION_TERMINATED_EVENTS = ['iceConnectionFailed', 'iceConnectionClosed'];
const CALL_CONNECT_NOTIFICATION_TIMEOUT = 500;
2017-10-12 05:04:10 +08:00
const logConnector = (level, category, label, content) => {
if (level === 'log')
level = "info";
logger[level]({logCode: 'sipjs_log'}, '[' + category + '] ' + content);
};
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-27 01:14:56 +08:00
const {
causes,
} = window.SIP.C;
2017-10-27 01:14:56 +08:00
2017-10-12 05:04:10 +08:00
this.errorCodes = {
[causes.REQUEST_TIMEOUT]: this.baseErrorCodes.REQUEST_TIMEOUT,
[causes.INVALID_TARGET]: this.baseErrorCodes.INVALID_TARGET,
[causes.CONNECTION_ERROR]: this.baseErrorCodes.CONNECTION_ERROR,
2018-06-27 21:56:03 +08:00
[causes.WEBRTC_NOT_SUPPORTED]: this.baseErrorCodes.WEBRTC_NOT_SUPPORTED,
};
this.webRtcError = {
1001: '1001',
1002: '1002',
1003: '1003',
1004: '1004',
1005: '1005',
1006: '1006',
1007: '1007',
1008: '1008',
1009: '1009',
1010: '1010',
1011: '1011',
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) => {
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-27 01:14:56 +08:00
return this.doCall({ callExtension, isListenOnly, inputStream })
.catch((reason) => {
callback({
status: this.baseCallStates.failed,
error: this.baseErrorCodes.GENERIC_ERROR,
bridgeError: reason,
});
reject(reason);
});
2017-09-30 04:42:34 +08:00
});
2017-07-24 22:15:46 +08:00
}
2017-10-20 18:11:51 +08:00
doCall(options) {
const {
isListenOnly,
} = options;
const {
userId,
name,
sessionToken,
} = this.user;
const callerIdName = [
userId,
'bbbID',
isListenOnly ? `LISTENONLY-${name}` : name,
].join('-');
this.user.callerIdName = callerIdName;
this.callOptions = options;
return fetchStunTurnServers(sessionToken)
.then(this.createUserAgent.bind(this))
.then(this.inviteUserAgent.bind(this))
.then(this.setupEventHandlers.bind(this));
2017-10-20 18:11:51 +08:00
}
2017-10-18 03:16:42 +08:00
transferCall(onTransferSuccess) {
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',
});
2017-10-20 18:11:51 +08:00
reject(this.baseErrorCodes.REQUEST_TIMEOUT);
2017-10-18 03:16:42 +08:00
}, CALL_TRANSFER_TIMEOUT);
// This is is the call transfer code ask @chadpilkey
this.currentSession.dtmf(1);
Tracker.autorun((c) => {
trackerControl = c;
const selector = { meetingId: this.userData.meetingId, intId: this.userData.userId };
const query = VoiceUsers.find(selector);
query.observeChanges({
changed: (id, fields) => {
if (fields.joined) {
clearTimeout(timeout);
onTransferSuccess();
c.stop();
resolve();
}
},
});
});
});
}
2017-09-29 21:38:10 +08:00
exitAudio() {
2017-10-27 01:14:56 +08:00
return new Promise((resolve, reject) => {
let hangupRetries = 0;
let hangup = false;
const { mediaHandler } = this.currentSession;
// Removing termination events to avoid triggering an error
CONNECTION_TERMINATED_EVENTS.forEach(e => mediaHandler.off(e));
2017-10-27 01:14:56 +08:00
const tryHangup = () => {
this.currentSession.bye();
hangupRetries += 1;
setTimeout(() => {
if (hangupRetries > CALL_HANGUP_MAX_RETRIES) {
this.callback({
status: this.baseCallStates.failed,
error: this.baseErrorCodes.REQUEST_TIMEOUT,
bridgeError: 'Timeout on call hangup',
});
return reject(this.baseErrorCodes.REQUEST_TIMEOUT);
}
if (!hangup) return tryHangup();
return resolve();
}, CALL_HANGUP_TIMEOUT);
};
2017-10-05 04:49:11 +08:00
this.currentSession.on('bye', () => {
2017-10-27 01:14:56 +08:00
hangup = true;
2017-10-05 04:49:11 +08:00
resolve();
});
2017-10-27 01:14:56 +08:00
return tryHangup();
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`,
2017-11-17 19:52:48 +08:00
log: {
builtinEnabled: false,
connector: logConnector
2017-11-17 19:52:48 +08:00
},
2017-10-18 03:16:42 +08:00
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);
};
2018-06-27 21:56:03 +08:00
const handleUserAgentDisconnection = (event) => {
2017-10-18 03:16:42 +08:00
userAgent.stop();
userAgent = null;
2018-06-27 21:56:03 +08:00
const { lastTransportError } = event.transport;
const errorCode = lastTransportError.code;
const error = this.webRtcError[errorCode] || this.baseErrorCodes.CONNECTION_ERROR;
2017-10-18 03:16:42 +08:00
this.callback({
status: this.baseCallStates.failed,
2018-06-27 21:56:03 +08:00
error,
bridgeError: 'User Agent Disconnected',
});
2017-10-20 18:11:51 +08:00
reject(this.baseErrorCodes.CONNECTION_ERROR);
2017-10-18 03:16:42 +08:00
};
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: {
2018-12-22 01:14:05 +08:00
offerToReceiveAudio: true,
offerToReceiveVideo: false,
2017-10-18 03:16:42 +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) => {
2018-04-04 00:49:45 +08:00
const { mediaHandler } = currentSession;
2017-10-27 01:14:56 +08:00
2018-04-04 00:49:45 +08:00
const connectionCompletedEvents = ['iceConnectionCompleted', 'iceConnectionConnected'];
2017-10-18 03:16:42 +08:00
const handleConnectionCompleted = () => {
2018-04-04 00:49:45 +08:00
connectionCompletedEvents.forEach(e => mediaHandler.off(e, handleConnectionCompleted));
// We have to delay notifying that the call is connected because it is sometimes not
// actually ready and if the user says "Yes they can hear themselves" too quickly the
// B-leg transfer will fail
const that = this;
setTimeout(() => {
that.callback({ status: that.baseCallStates.started });
that.connectionCompleted = true;
resolve();
}, CALL_CONNECT_NOTIFICATION_TIMEOUT);
2017-10-18 03:16:42 +08:00
};
2018-04-04 00:49:45 +08:00
connectionCompletedEvents.forEach(e => mediaHandler.on(e, handleConnectionCompleted));
2017-10-18 03:16:42 +08:00
const handleSessionTerminated = (message, cause) => {
if (!message && !cause) {
return this.callback({
status: this.baseCallStates.ended,
});
}
2018-12-22 01:14:05 +08:00
const mappedCause = cause in this.errorCodes
? this.errorCodes[cause]
: this.baseErrorCodes.GENERIC_ERROR;
2017-10-20 18:11:51 +08:00
2017-10-18 03:16:42 +08:00
return this.callback({
status: this.baseCallStates.failed,
error: mappedCause,
bridgeError: cause,
});
};
currentSession.on('terminated', handleSessionTerminated);
2018-04-04 00:49:45 +08:00
const handleConnectionTerminated = (peer) => {
CONNECTION_TERMINATED_EVENTS.forEach(e => mediaHandler.off(e, handleConnectionTerminated));
2018-04-04 00:49:45 +08:00
this.callback({
status: this.baseCallStates.failed,
2018-06-29 02:14:35 +08:00
error: this.baseErrorCodes.ICE_NEGOTIATION_FAILED,
2018-04-04 00:49:45 +08:00
bridgeError: peer,
});
};
CONNECTION_TERMINATED_EVENTS.forEach(e => mediaHandler.on(e, handleConnectionTerminated));
2017-10-05 04:49:11 +08:00
this.currentSession = currentSession;
2017-10-12 05:04:10 +08:00
});
}
setDefaultInputDevice() {
const handleMediaSuccess = (mediaStream) => {
const deviceLabel = mediaStream.getAudioTracks()[0].label;
window.defaultInputStream = mediaStream.getTracks();
return navigator.mediaDevices.enumerateDevices().then((mediaDevices) => {
const device = mediaDevices.find(d => d.label === deviceLabel);
return this.changeInputDevice(device.deviceId);
});
};
2017-10-27 01:14:56 +08:00
return navigator.mediaDevices.getUserMedia({ audio: true }).then(handleMediaSuccess);
2017-10-27 01:14:56 +08:00
}
changeInputDevice(value) {
2017-10-18 03:16:42 +08:00
const {
media,
} = this;
if (media.inputDevice.audioContext) {
const handleAudioContextCloseSuccess = () => {
2017-10-18 03:16:42 +08:00
media.inputDevice.audioContext = null;
media.inputDevice.scriptProcessor = null;
media.inputDevice.source = null;
return this.changeInputDevice(value);
};
return media.inputDevice.audioContext.close().then(handleAudioContextCloseSuccess);
2017-10-18 03:16:42 +08:00
}
2017-10-12 05:04:10 +08:00
2017-10-18 03:16:42 +08:00
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
}
media.inputDevice.id = value;
2017-10-18 03:16:42 +08:00
media.inputDevice.scriptProcessor = media.inputDevice.audioContext
.createScriptProcessor(2048, 1, 1);
2017-10-18 03:16:42 +08:00
media.inputDevice.source = null;
const constraints = {
audio: {
deviceId: value,
},
};
2017-10-12 05:04:10 +08:00
const handleMediaSuccess = (mediaStream) => {
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;
};
2017-10-18 03:16:42 +08:00
return navigator.mediaDevices.getUserMedia(constraints).then(handleMediaSuccess);
2017-10-18 03:16:42 +08:00
}
2017-10-27 01:14:56 +08:00
async changeOutputDevice(value) {
2017-10-18 03:16:42 +08:00
const audioContext = document.querySelector(MEDIA_TAG);
if (audioContext.setSinkId) {
2017-10-27 01:14:56 +08:00
try {
2018-10-25 04:26:20 +08:00
audioContext.srcObject = null;
2017-10-27 01:14:56 +08:00
await audioContext.setSinkId(value);
this.media.outputDeviceId = value;
} catch (err) {
logger.error(err);
2017-10-27 01:14:56 +08:00
throw new Error(this.baseErrorCodes.MEDIA_ERROR);
}
2017-10-18 03:16:42 +08:00
}
2017-11-17 19:52:48 +08:00
return this.media.outputDeviceId || value;
2017-07-24 22:15:46 +08:00
}
}