2017-09-20 01:47:57 +08:00
|
|
|
import { Tracker } from 'meteor/tracker';
|
2017-09-29 21:38:10 +08:00
|
|
|
import { makeCall } from '/imports/ui/services/api';
|
2017-10-13 03:22:10 +08:00
|
|
|
import VertoBridge from '/imports/api/audio/client/bridge/verto';
|
2017-11-17 19:52:48 +08:00
|
|
|
import Auth from '/imports/ui/services/auth';
|
2017-11-18 03:01:52 +08:00
|
|
|
import VoiceUsers from '/imports/api/voice-users';
|
2017-10-13 03:22:10 +08:00
|
|
|
import SIPBridge from '/imports/api/audio/client/bridge/sip';
|
2017-10-24 21:19:58 +08:00
|
|
|
import { notify } from '/imports/ui/services/notification';
|
2017-09-20 01:47:57 +08:00
|
|
|
|
2017-10-18 03:16:42 +08:00
|
|
|
const MEDIA = Meteor.settings.public.media;
|
|
|
|
const USE_SIP = MEDIA.useSIPAudio;
|
|
|
|
const ECHO_TEST_NUMBER = MEDIA.echoTestNumber;
|
2017-10-10 04:48:10 +08:00
|
|
|
|
|
|
|
const CALL_STATES = {
|
2017-10-05 04:49:11 +08:00
|
|
|
STARTED: 'started',
|
|
|
|
ENDED: 'ended',
|
|
|
|
FAILED: 'failed',
|
|
|
|
};
|
2017-09-20 01:47:57 +08:00
|
|
|
|
|
|
|
class AudioManager {
|
|
|
|
constructor() {
|
2017-09-30 04:42:34 +08:00
|
|
|
this._inputDevice = {
|
2018-04-13 20:39:26 +08:00
|
|
|
value: 'default',
|
2017-10-11 02:03:29 +08:00
|
|
|
tracker: new Tracker.Dependency(),
|
2017-09-30 04:42:34 +08:00
|
|
|
};
|
|
|
|
|
2017-09-20 01:47:57 +08:00
|
|
|
this.defineProperties({
|
|
|
|
isMuted: false,
|
|
|
|
isConnected: false,
|
|
|
|
isConnecting: false,
|
2017-10-27 01:14:56 +08:00
|
|
|
isHangingUp: false,
|
2017-09-29 21:38:10 +08:00
|
|
|
isListenOnly: false,
|
|
|
|
isEchoTest: false,
|
2017-11-09 02:41:15 +08:00
|
|
|
isWaitingPermissions: false,
|
2017-09-20 01:47:57 +08:00
|
|
|
error: null,
|
2017-10-10 04:48:10 +08:00
|
|
|
outputDeviceId: null,
|
2018-01-16 23:56:31 +08:00
|
|
|
muteHandle: null,
|
2017-09-20 01:47:57 +08:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-10-19 03:40:01 +08:00
|
|
|
init(userData, messages) {
|
2017-10-18 03:16:42 +08:00
|
|
|
this.bridge = USE_SIP ? new SIPBridge(userData) : new VertoBridge(userData);
|
|
|
|
this.userData = userData;
|
2017-10-19 03:40:01 +08:00
|
|
|
this.messages = messages;
|
2017-11-18 03:01:52 +08:00
|
|
|
this.initialized = true;
|
2017-10-18 03:16:42 +08:00
|
|
|
}
|
|
|
|
|
2017-09-20 01:47:57 +08:00
|
|
|
defineProperties(obj) {
|
2017-09-29 21:38:10 +08:00
|
|
|
Object.keys(obj).forEach((key) => {
|
|
|
|
const privateKey = `_${key}`;
|
|
|
|
this[privateKey] = {
|
2017-09-20 01:47:57 +08:00
|
|
|
value: obj[key],
|
2017-10-11 02:03:29 +08:00
|
|
|
tracker: new Tracker.Dependency(),
|
2017-09-29 21:38:10 +08:00
|
|
|
};
|
2017-09-20 01:47:57 +08:00
|
|
|
|
|
|
|
Object.defineProperty(this, key, {
|
|
|
|
set: (value) => {
|
2017-09-29 21:38:10 +08:00
|
|
|
this[privateKey].value = value;
|
|
|
|
this[privateKey].tracker.changed();
|
2017-09-20 01:47:57 +08:00
|
|
|
},
|
|
|
|
get: () => {
|
2017-09-29 21:38:10 +08:00
|
|
|
this[privateKey].tracker.depend();
|
|
|
|
return this[privateKey].value;
|
|
|
|
},
|
|
|
|
});
|
|
|
|
});
|
2017-09-20 01:47:57 +08:00
|
|
|
}
|
|
|
|
|
2018-03-16 02:57:25 +08:00
|
|
|
askDevicesPermissions() {
|
|
|
|
// Only change the isWaitingPermissions for the case where the user didnt allowed it yet
|
|
|
|
const permTimeout = setTimeout(() => {
|
|
|
|
if (!this.devicesInitialized) { this.isWaitingPermissions = true; }
|
2017-11-09 02:41:15 +08:00
|
|
|
}, 100);
|
|
|
|
|
2018-03-16 02:57:25 +08:00
|
|
|
this.isWaitingPermissions = false;
|
|
|
|
this.devicesInitialized = false;
|
2017-09-30 04:42:34 +08:00
|
|
|
|
2017-11-02 20:10:01 +08:00
|
|
|
return Promise.all([
|
2017-11-18 03:01:52 +08:00
|
|
|
this.setDefaultInputDevice(),
|
|
|
|
this.setDefaultOutputDevice(),
|
2018-03-16 02:57:25 +08:00
|
|
|
]).then(() => {
|
|
|
|
this.devicesInitialized = true;
|
|
|
|
this.isWaitingPermissions = false;
|
|
|
|
}).catch((err) => {
|
|
|
|
clearTimeout(permTimeout);
|
|
|
|
this.isConnecting = false;
|
|
|
|
this.isWaitingPermissions = false;
|
|
|
|
throw err;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
joinMicrophone() {
|
|
|
|
this.isListenOnly = false;
|
|
|
|
this.isEchoTest = false;
|
|
|
|
|
|
|
|
const callOptions = {
|
|
|
|
isListenOnly: false,
|
|
|
|
extension: null,
|
|
|
|
inputStream: this.inputStream,
|
|
|
|
};
|
|
|
|
|
|
|
|
return this.askDevicesPermissions()
|
2018-04-03 21:19:50 +08:00
|
|
|
.then(this.onAudioJoining.bind(this))
|
2018-03-16 02:57:25 +08:00
|
|
|
.then(() => this.bridge.joinAudio(callOptions, this.callStateCallback.bind(this)));
|
|
|
|
}
|
|
|
|
|
|
|
|
joinEchoTest() {
|
|
|
|
this.isListenOnly = false;
|
|
|
|
this.isEchoTest = true;
|
|
|
|
|
|
|
|
const callOptions = {
|
|
|
|
isListenOnly: false,
|
|
|
|
extension: ECHO_TEST_NUMBER,
|
|
|
|
inputStream: this.inputStream,
|
|
|
|
};
|
|
|
|
|
|
|
|
return this.askDevicesPermissions()
|
2018-04-03 21:19:50 +08:00
|
|
|
.then(this.onAudioJoining.bind(this))
|
2018-03-16 02:57:25 +08:00
|
|
|
.then(() => this.bridge.joinAudio(callOptions, this.callStateCallback.bind(this)));
|
|
|
|
}
|
|
|
|
|
|
|
|
joinListenOnly() {
|
|
|
|
this.isListenOnly = true;
|
|
|
|
this.isEchoTest = false;
|
|
|
|
|
|
|
|
const callOptions = {
|
|
|
|
isListenOnly: true,
|
|
|
|
extension: null,
|
|
|
|
inputStream: this.createListenOnlyStream(),
|
|
|
|
};
|
|
|
|
|
|
|
|
// We need this until we upgrade to SIP 9x. See #4690
|
|
|
|
const iceGatheringErr = 'ICE_TIMEOUT';
|
|
|
|
const iceGatheringTimeout = new Promise((resolve, reject) => {
|
|
|
|
setTimeout(reject, 12000, iceGatheringErr);
|
|
|
|
});
|
|
|
|
|
|
|
|
return this.onAudioJoining()
|
|
|
|
.then(() => Promise.race([
|
|
|
|
this.bridge.joinAudio(callOptions, this.callStateCallback.bind(this)),
|
|
|
|
iceGatheringTimeout,
|
|
|
|
]))
|
2017-11-18 03:01:52 +08:00
|
|
|
.catch((err) => {
|
2018-03-16 02:57:25 +08:00
|
|
|
// If theres a iceGathering timeout we retry to join after asking device permissions
|
2018-03-16 03:30:50 +08:00
|
|
|
if (err === iceGatheringErr && !this.devicesInitialized) {
|
2018-03-16 02:57:25 +08:00
|
|
|
return this.askDevicesPermissions()
|
|
|
|
.then(() => this.joinListenOnly());
|
|
|
|
}
|
|
|
|
|
|
|
|
throw err;
|
2017-11-02 20:10:01 +08:00
|
|
|
});
|
2017-09-20 01:47:57 +08:00
|
|
|
}
|
|
|
|
|
2018-03-16 02:57:25 +08:00
|
|
|
onAudioJoining() {
|
|
|
|
this.isConnecting = true;
|
|
|
|
this.isMuted = false;
|
|
|
|
this.error = false;
|
|
|
|
|
|
|
|
return Promise.resolve();
|
|
|
|
}
|
|
|
|
|
2017-09-20 01:47:57 +08:00
|
|
|
exitAudio() {
|
2017-11-02 20:10:01 +08:00
|
|
|
if (!this.isConnected) return Promise.resolve();
|
|
|
|
|
2017-10-27 01:14:56 +08:00
|
|
|
this.isHangingUp = true;
|
2017-10-12 22:49:50 +08:00
|
|
|
return this.bridge.exitAudio();
|
2017-09-20 01:47:57 +08:00
|
|
|
}
|
|
|
|
|
2017-10-12 20:50:23 +08:00
|
|
|
transferCall() {
|
2017-10-18 03:16:42 +08:00
|
|
|
this.onTransferStart();
|
|
|
|
return this.bridge.transferCall(this.onAudioJoin.bind(this));
|
2017-10-12 20:50:23 +08:00
|
|
|
}
|
|
|
|
|
2017-09-20 01:47:57 +08:00
|
|
|
toggleMuteMicrophone() {
|
2018-01-16 05:01:57 +08:00
|
|
|
makeCall('toggleSelfVoice');
|
2017-09-20 01:47:57 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
onAudioJoin() {
|
|
|
|
this.isConnecting = false;
|
2017-10-19 03:40:01 +08:00
|
|
|
this.isConnected = true;
|
2017-10-20 18:11:51 +08:00
|
|
|
|
2018-01-16 05:01:57 +08:00
|
|
|
// listen to the VoiceUsers changes and update the flag
|
2018-03-16 02:57:25 +08:00
|
|
|
if (!this.muteHandle) {
|
2018-01-16 05:01:57 +08:00
|
|
|
const query = VoiceUsers.find({ intId: Auth.userID });
|
2018-01-16 05:13:18 +08:00
|
|
|
this.muteHandle = query.observeChanges({
|
2018-01-16 05:01:57 +08:00
|
|
|
changed: (id, fields) => {
|
|
|
|
if (fields.muted === this.isMuted) return;
|
|
|
|
this.isMuted = fields.muted;
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-10-20 18:11:51 +08:00
|
|
|
if (!this.isEchoTest) {
|
2017-10-23 20:41:09 +08:00
|
|
|
this.notify(this.messages.info.JOINED_AUDIO);
|
2017-10-20 18:11:51 +08:00
|
|
|
}
|
2017-09-20 01:47:57 +08:00
|
|
|
}
|
|
|
|
|
2017-10-12 20:50:23 +08:00
|
|
|
onTransferStart() {
|
|
|
|
this.isEchoTest = false;
|
|
|
|
this.isConnecting = true;
|
|
|
|
}
|
|
|
|
|
2017-09-20 01:47:57 +08:00
|
|
|
onAudioExit() {
|
|
|
|
this.isConnected = false;
|
2017-10-05 04:49:11 +08:00
|
|
|
this.isConnecting = false;
|
2017-10-27 01:14:56 +08:00
|
|
|
this.isHangingUp = false;
|
2017-09-29 21:38:10 +08:00
|
|
|
|
2018-04-13 20:39:26 +08:00
|
|
|
this.inputStream.getTracks().forEach(track => track.stop());
|
|
|
|
this.inputDevice = { id: 'default' };
|
|
|
|
|
2017-10-20 18:11:51 +08:00
|
|
|
if (!this.error && !this.isEchoTest) {
|
2017-10-23 20:41:09 +08:00
|
|
|
this.notify(this.messages.info.LEFT_AUDIO);
|
2017-09-29 21:38:10 +08:00
|
|
|
}
|
2017-09-20 01:47:57 +08:00
|
|
|
}
|
|
|
|
|
2017-10-05 04:49:11 +08:00
|
|
|
callStateCallback(response) {
|
2017-09-29 21:38:10 +08:00
|
|
|
return new Promise((resolve) => {
|
|
|
|
const {
|
2017-10-05 04:49:11 +08:00
|
|
|
STARTED,
|
|
|
|
ENDED,
|
|
|
|
FAILED,
|
|
|
|
} = CALL_STATES;
|
2017-09-29 21:38:10 +08:00
|
|
|
|
2017-10-05 04:49:11 +08:00
|
|
|
const {
|
|
|
|
status,
|
|
|
|
error,
|
2017-10-23 20:41:09 +08:00
|
|
|
bridgeError,
|
2017-10-05 04:49:11 +08:00
|
|
|
} = response;
|
|
|
|
|
|
|
|
if (status === STARTED) {
|
2017-09-29 21:38:10 +08:00
|
|
|
this.onAudioJoin();
|
2017-10-05 04:49:11 +08:00
|
|
|
resolve(STARTED);
|
|
|
|
} else if (status === ENDED) {
|
2017-09-29 21:38:10 +08:00
|
|
|
this.onAudioExit();
|
2017-10-05 04:49:11 +08:00
|
|
|
} else if (status === FAILED) {
|
2017-10-12 05:04:10 +08:00
|
|
|
this.error = error;
|
2018-03-16 02:57:25 +08:00
|
|
|
this.notify(this.messages.error[error], true);
|
2017-10-23 20:41:09 +08:00
|
|
|
console.error('Audio Error:', error, bridgeError);
|
2017-09-29 21:38:10 +08:00
|
|
|
this.onAudioExit();
|
|
|
|
}
|
2017-10-11 20:05:57 +08:00
|
|
|
});
|
2017-09-29 21:38:10 +08:00
|
|
|
}
|
|
|
|
|
2017-10-05 04:49:11 +08:00
|
|
|
createListenOnlyStream() {
|
|
|
|
if (this.listenOnlyAudioContext) {
|
|
|
|
this.listenOnlyAudioContext.close();
|
|
|
|
}
|
|
|
|
|
2017-10-18 03:16:42 +08:00
|
|
|
this.listenOnlyAudioContext = window.AudioContext ?
|
2017-11-18 03:01:52 +08:00
|
|
|
new window.AudioContext() :
|
|
|
|
new window.webkitAudioContext();
|
2017-10-10 05:23:05 +08:00
|
|
|
|
2017-10-05 04:49:11 +08:00
|
|
|
return this.listenOnlyAudioContext.createMediaStreamDestination().stream;
|
|
|
|
}
|
|
|
|
|
2017-10-27 01:14:56 +08:00
|
|
|
setDefaultInputDevice() {
|
2017-11-02 20:10:01 +08:00
|
|
|
return this.changeInputDevice();
|
2017-10-27 01:14:56 +08:00
|
|
|
}
|
|
|
|
|
2017-11-02 20:10:01 +08:00
|
|
|
setDefaultOutputDevice() {
|
|
|
|
return this.changeOutputDevice('default');
|
|
|
|
}
|
|
|
|
|
|
|
|
changeInputDevice(deviceId) {
|
|
|
|
const handleChangeInputDeviceSuccess = (inputDevice) => {
|
|
|
|
this.inputDevice = inputDevice;
|
2017-11-09 02:41:15 +08:00
|
|
|
return Promise.resolve(inputDevice);
|
2017-11-02 20:10:01 +08:00
|
|
|
};
|
|
|
|
|
2017-11-18 03:01:52 +08:00
|
|
|
const handleChangeInputDeviceError = () =>
|
|
|
|
Promise.reject({
|
2017-11-17 19:52:48 +08:00
|
|
|
type: 'MEDIA_ERROR',
|
|
|
|
message: this.messages.error.MEDIA_ERROR,
|
|
|
|
});
|
2017-11-02 20:10:01 +08:00
|
|
|
|
|
|
|
if (!deviceId) {
|
2017-11-18 03:01:52 +08:00
|
|
|
return this.bridge.setDefaultInputDevice()
|
|
|
|
.then(handleChangeInputDeviceSuccess)
|
|
|
|
.catch(handleChangeInputDeviceError);
|
2017-10-23 20:41:09 +08:00
|
|
|
}
|
2018-04-13 20:39:26 +08:00
|
|
|
|
2017-11-18 03:01:52 +08:00
|
|
|
return this.bridge.changeInputDevice(deviceId)
|
|
|
|
.then(handleChangeInputDeviceSuccess)
|
|
|
|
.catch(handleChangeInputDeviceError);
|
2017-10-18 03:16:42 +08:00
|
|
|
}
|
2017-09-30 04:42:34 +08:00
|
|
|
|
2017-10-18 03:16:42 +08:00
|
|
|
async changeOutputDevice(deviceId) {
|
|
|
|
this.outputDeviceId = await this.bridge.changeOutputDevice(deviceId);
|
2017-09-30 04:42:34 +08:00
|
|
|
}
|
|
|
|
|
2017-10-18 03:16:42 +08:00
|
|
|
set inputDevice(value) {
|
2018-04-13 20:39:26 +08:00
|
|
|
this._inputDevice.value = value;
|
2017-10-18 03:16:42 +08:00
|
|
|
this._inputDevice.tracker.changed();
|
2017-10-10 04:48:10 +08:00
|
|
|
}
|
|
|
|
|
2017-10-11 02:03:29 +08:00
|
|
|
get inputStream() {
|
2018-04-13 20:39:26 +08:00
|
|
|
this._inputDevice.tracker.depend();
|
|
|
|
return this._inputDevice.value.stream;
|
2017-09-30 04:42:34 +08:00
|
|
|
}
|
|
|
|
|
2017-10-11 02:03:29 +08:00
|
|
|
get inputDeviceId() {
|
2017-10-04 04:42:10 +08:00
|
|
|
this._inputDevice.tracker.depend();
|
2018-04-13 20:39:26 +08:00
|
|
|
return this._inputDevice.value.id;
|
2017-09-30 04:42:34 +08:00
|
|
|
}
|
2017-10-18 03:16:42 +08:00
|
|
|
|
|
|
|
set userData(value) {
|
|
|
|
this._userData = value;
|
|
|
|
}
|
|
|
|
|
|
|
|
get userData() {
|
|
|
|
return this._userData;
|
|
|
|
}
|
2017-10-23 20:41:09 +08:00
|
|
|
|
2018-03-16 02:57:25 +08:00
|
|
|
notify(message, error = false) {
|
2017-11-18 03:01:52 +08:00
|
|
|
notify(
|
|
|
|
message,
|
2018-03-16 02:57:25 +08:00
|
|
|
error ? 'error' : 'info',
|
2017-11-18 03:01:52 +08:00
|
|
|
this.isListenOnly ? 'audio_on' : 'unmute',
|
|
|
|
);
|
2017-10-23 20:41:09 +08:00
|
|
|
}
|
2017-09-20 01:47:57 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
const audioManager = new AudioManager();
|
|
|
|
export default audioManager;
|