linx-simulator2/node_modules/mumble-client/lib/client.js

860 lines
28 KiB
JavaScript
Raw Normal View History

2019-09-18 08:11:16 +00:00
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
var _mumbleStreams = require('mumble-streams');
var _mumbleStreams2 = _interopRequireDefault(_mumbleStreams);
var _reduplexer = require('reduplexer');
var _reduplexer2 = _interopRequireDefault(_reduplexer);
var _events = require('events');
var _through = require('through2');
var _through2 = _interopRequireDefault(_through);
var _promise = require('promise');
var _promise2 = _interopRequireDefault(_promise);
var _dropStream = require('drop-stream');
var _dropStream2 = _interopRequireDefault(_dropStream);
var _utils = require('./utils.js');
var _user2 = require('./user');
var _user3 = _interopRequireDefault(_user2);
var _channel = require('./channel');
var _channel2 = _interopRequireDefault(_channel);
var _removeValue = require('remove-value');
var _removeValue2 = _interopRequireDefault(_removeValue);
var _statsIncremental = require('stats-incremental');
var _statsIncremental2 = _interopRequireDefault(_statsIncremental);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
var DenyType = _mumbleStreams2.default.data.messages.PermissionDenied.DenyType;
/*
* @typedef {'Opus'} Codec
*/
/**
* Number of the voice target when outgoing (0 for normal talking, 1-31 for
* a voice target).
* String describing the source when incoming.
* @typedef {number|'normal'|'shout'|'whisper'} VoiceTarget
*/
/**
* @typedef {object} VoiceData
* @property {VoiceTarget} target - Target of the audio
* @property {Codec} codec - The codec of the audio packet
* @property {Buffer} frame - Encoded audio frame, null indicates a lost frame
* @property {?Position} position - Position of audio source
*/
/**
* Interleaved 32-bit float PCM frames in [-1; 1] range with sample rate of 48k.
* @typedef {object} PCMData
* @property {VoiceTarget} target - Target of the audio
* @property {Float32Array} pcm - The pcm data
* @property {number} numberOfChannels - Number of channels
* @property {?Position} position - Position of audio source
* @property {?number} bitrate - Target bitrate hint for encoder, see for default {@link MumbleClient#setAudioQuality}
*/
/**
* Transforms {@link VoiceData} to {@link PCMData}.
* Should ignore any unknown codecs.
*
* @interface DecoderStream
* @extends stream.Transform
*/
/**
* Transforms {@link PCMData} to {@link VoiceData}.
*
* @interface EncoderStream
* @extends stream.Transform
*/
/**
* @interface Codecs
* @property {number[]} celt - List of celt versions supported by this implementation
* @property {boolean} opus - Whether this implementation supports the Opus codec
*/
/**
* Returns the duration of encoded voice data without actually decoding it.
*
* @function Codecs#getDuration
* @param {Codec} codec - The codec
* @param {Buffer} buffer - The encoded data
* @return {number} The duration in milliseconds (has to be a multiple of 10)
*/
/**
* Creates a new decoder stream for a transmission of the specified user.
* This method is called for every single transmission (whenever a user starts
* speaking), as such it must not be expensive.
*
* @function Codecs#createDecoderStream
* @param {User} user - The user
* @return {DecoderStream} The decoder stream
*/
/**
* Creates a new encoder stream for a outgoing transmission.
* This method is called for every single transmission (whenever the user
* starts speaking), as such it must not be expensive.
*
* @function Codecs#createEncoderStream
* @param {Codec} codec - The codec
* @return {EncoderStream} The endecoder stream
*/
/**
* Single use Mumble client.
*/
var MumbleClient = function (_EventEmitter) {
_inherits(MumbleClient, _EventEmitter);
/**
* A mumble client.
* This object may only be connected to one server and cannot be reused.
*
* @param {object} options - Options
* @param {string} options.username - User name of the client
* @param {string} [options.password] - Server password to use
* @param {string[]} [options.tokens] - Array of access tokens to use
* @param {string} [options.clientSoftware] - Client software name/version
* @param {string} [options.osName] - Client operating system name
* @param {string} [options.osVersion] - Client operating system version
* @param {Codecs} [options.codecs] - Codecs used for voice
* @param {number} [options.userVoiceTimeout] - Milliseconds after which an
* inactive voice transmissions is timed out
* @param {number} [options.maxInFlightDataPings] - Amount of data pings without response
* after which the connection is considered timed out
* @param {number} [options.dataPingInterval] - Interval of data pings (in ms)
*/
function MumbleClient(options) {
_classCallCheck(this, MumbleClient);
var _this = _possibleConstructorReturn(this, (MumbleClient.__proto__ || Object.getPrototypeOf(MumbleClient)).call(this));
if (!options.username) {
throw new Error('No username given');
}
_this._options = options || {};
_this._username = options.username;
_this._password = options.password;
_this._tokens = options.tokens;
_this._codecs = options.codecs;
_this._dataPingInterval = options.dataPingInterval || 5000;
_this._maxInFlightDataPings = options.maxInFlightDataPings || 2;
_this._dataStats = new _statsIncremental2.default();
_this._voiceStats = new _statsIncremental2.default();
_this._userById = {};
_this._channelById = {};
_this.users = [];
_this.channels = [];
_this._dataEncoder = new _mumbleStreams2.default.data.Encoder();
_this._dataDecoder = new _mumbleStreams2.default.data.Decoder();
_this._voiceEncoder = new _mumbleStreams2.default.voice.Encoder('server');
_this._voiceDecoder = new _mumbleStreams2.default.voice.Decoder('server');
_this._data = (0, _reduplexer2.default)(_this._dataEncoder, _this._dataDecoder, { objectMode: true });
_this._voice = (0, _reduplexer2.default)(_this._voiceEncoder, _this._voiceDecoder, { objectMode: true });
_this._data.on('data', _this._onData.bind(_this));
_this._voice.on('data', _this._onVoice.bind(_this));
_this._voiceEncoder.on('data', function (data) {
// TODO This should only be the fallback option
_this._data.write({
name: 'UDPTunnel',
payload: data
});
});
_this._voiceDecoder.on('unknown_codec', function (codecId) {
return _this.emit('unknown_codec', codecId);
});
_this._data.on('end', _this.disconnect.bind(_this));
_this._registerErrorHandler(_this._data, _this._voice, _this._dataEncoder, _this._dataDecoder, _this._voiceEncoder, _this._voiceDecoder);
_this._disconnected = false;
return _this;
}
_createClass(MumbleClient, [{
key: '_registerErrorHandler',
value: function _registerErrorHandler() {
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = arguments[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var obj = _step.value;
obj.on('error', this._error.bind(this));
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
}
}, {
key: '_error',
value: function _error(reason) {
this.emit('error', reason);
this.disconnect();
}
}, {
key: '_send',
value: function _send(msg) {
this._data.write(msg);
}
/**
* Connects this client to a duplex stream that is used for the data channel.
* The provided duplex stream is expected to be valid and usable.
* Calling this method will begin the initialization of the connection.
*
* @param stream - The stream used for the data channel.
* @param callback - Optional callback that is invoked when the connection has been established.
*/
}, {
key: 'connectDataStream',
value: function connectDataStream(stream, callback) {
var _this2 = this;
if (this._dataStream) throw Error('Already connected!');
this._dataStream = stream;
// Connect the supplied stream to the data channel encoder and decoder
this._registerErrorHandler(stream);
this._dataEncoder.pipe(stream).pipe(this._dataDecoder);
// Send the initial two packets
this._send({
name: 'Version',
payload: {
version: _mumbleStreams2.default.version.toUInt8(),
release: this._options.clientSoftware || 'Node.js mumble-client',
os: this._options.osName || (0, _utils.getOSName)(),
os_version: this._options.osVersion || (0, _utils.getOSVersion)()
}
});
this._send({
name: 'Authenticate',
payload: {
username: this._username,
password: this._password,
tokens: this._tokens,
celt_versions: (this._codecs || { celt: [] }).celt,
opus: (this._codecs || { opus: false }).opus
}
});
return new _promise2.default(function (resolve, reject) {
_this2.once('connected', function () {
return resolve(_this2);
});
_this2.once('reject', reject);
_this2.once('error', reject);
}).nodeify(callback);
}
/**
* Connects this client to a duplex stream that is used for the voice channel.
* The provided duplex stream is expected to be valid and usable.
* The stream may be unreliable. That is, it may lose packets or deliver them
* out of order.
* It must however gurantee that packets arrive unmodified and/or are dropped
* when corrupted.
* It is also responsible for any encryption that is necessary.
*
* Connecting a voice channel is entirely optional. If no voice channel
* is connected, all voice data is tunneled through the data channel.
*
* @param stream - The stream used for the data channel.
* @returns {undefined}
*/
}, {
key: 'connectVoiceStream',
value: function connectVoiceStream(stream) {
// Connect the stream to the voice channel encoder and decoder
this._registerErrorHandler(stream);
this._voiceEncoder.pipe(stream).pipe(this._voiceDecoder);
// TODO: Ping packet
}
}, {
key: 'createVoiceStream',
value: function createVoiceStream() {
var _this3 = this;
var target = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
var numberOfChannels = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1;
if (!this._codecs) {
return _dropStream2.default.obj();
}
var voiceStream = _through2.default.obj(function (chunk, encoding, callback) {
if (chunk instanceof Buffer) {
chunk = new Float32Array(chunk.buffer, chunk.byteOffset, chunk.byteLength / 4);
}
if (chunk instanceof Float32Array) {
chunk = {
target: target,
pcm: chunk,
numberOfChannels: numberOfChannels
};
} else {
chunk = {
target: target,
pcm: chunk.pcm,
numberOfChannels: numberOfChannels,
position: { x: chunk.x, y: chunk.y, z: chunk.z }
};
}
var samples = _this3._samplesPerPacket || chunk.pcm.length / numberOfChannels;
chunk.bitrate = _this3.getActualBitrate(samples, chunk.position != null);
callback(null, chunk);
});
var codec = 'Opus'; // TODO
var seqNum = 0;
voiceStream.pipe(this._codecs.createEncoderStream(codec)).on('data', function (data) {
var duration = _this3._codecs.getDuration(codec, data.frame) / 10;
_this3._voice.write({
seqNum: seqNum,
codec: codec,
mode: target,
frames: [data.frame],
position: data.position,
end: false
});
seqNum += duration;
}).on('end', function () {
_this3._voice.write({
seqNum: seqNum,
codec: codec,
mode: target,
frames: [],
end: true
});
});
return voiceStream;
}
/**
* Method called when new voice packets arrive.
* Forwards the packet to the source user.
*/
}, {
key: '_onVoice',
value: function _onVoice(chunk) {
var user = this._userById[chunk.source];
user._onVoice(chunk.seqNum, chunk.codec, chunk.target, chunk.frames, chunk.position, chunk.end);
}
/**
* Method called when new data packets arrive.
* If there is a method named '_onPacketName', the data is forwarded to
* that method, otherwise it is logged as unhandled.
*
* @param {object} chunk - The data packet
*/
}, {
key: '_onData',
value: function _onData(chunk) {
if (this['_on' + chunk.name]) {
this['_on' + chunk.name](chunk.payload);
} else {
console.log('Unhandled data packet:', chunk);
}
}
}, {
key: '_onUDPTunnel',
value: function _onUDPTunnel(payload) {
// Forward tunneled udp packets to the voice pipeline
this._voiceDecoder.write(payload);
}
}, {
key: '_onVersion',
value: function _onVersion(payload) {
this.serverVersion = {
major: payload.version >> 16,
minor: payload.version >> 8 & 0xff,
patch: payload.version >> 0 & 0xff,
release: payload.release,
os: payload.os,
osVersion: payload.os_version
};
}
}, {
key: '_onServerSync',
value: function _onServerSync(payload) {
var _this4 = this;
// This packet finishes the initialization phase
this.self = this._userById[payload.session];
this.maxBandwidth = payload.max_bandwidth;
this.welcomeMessage = payload.welcome_text;
// Make sure we send regular ping packets to not get disconnected
this._pinger = setInterval(function () {
if (_this4._inFlightDataPings >= _this4._maxInFlightDataPings) {
_this4._error('timeout');
return;
}
var dataStats = _this4._dataStats.getAll();
var voiceStats = _this4._voiceStats.getAll();
var timestamp = new Date().getTime();
var payload = {
timestamp: timestamp
};
if (dataStats) {
payload.tcp_packets = dataStats.n;
payload.tcp_ping_avg = dataStats.mean;
payload.tcp_ping_var = dataStats.variance;
}
if (voiceStats) {
payload.udp_packets = voiceStats.n;
payload.udp_ping_avg = voiceStats.mean;
payload.udp_ping_var = voiceStats.variance;
}
_this4._send({
name: 'Ping',
payload: payload
});
_this4._inFlightDataPings++;
}, this._dataPingInterval);
// We are now connected
this.emit('connected');
}
}, {
key: '_onPing',
value: function _onPing(payload) {
if (this._inFlightDataPings <= 0) {
console.warn('Got unexpected ping message:', payload);
return;
}
this._inFlightDataPings--;
var now = new Date().getTime();
var duration = now - payload.timestamp.toNumber();
this._dataStats.update(duration);
this.emit('dataPing', duration);
}
}, {
key: '_onReject',
value: function _onReject(payload) {
// We got rejected from the server for some reason.
this.emit('reject', payload);
this.disconnect();
}
}, {
key: '_onPermissionDenied',
value: function _onPermissionDenied(payload) {
if (payload.type === DenyType.Text) {
this.emit('denied', 'Text', null, null, payload.reason);
} else if (payload.type === DenyType.Permission) {
var user = this._userById[payload.session];
var channel = this._channelById[payload.channel_id];
this.emit('denied', 'Permission', user, channel, payload.permission);
} else if (payload.type === DenyType.SuperUser) {
this.emit('denied', 'SuperUser', null, null, null);
} else if (payload.type === DenyType.ChannelName) {
this.emit('denied', 'ChannelName', null, null, payload.name);
} else if (payload.type === DenyType.TextTooLong) {
this.emit('denied', 'TextTooLong', null, null, null);
} else if (payload.type === DenyType.TemporaryChannel) {
this.emit('denied', 'TemporaryChannel', null, null, null);
} else if (payload.type === DenyType.MissingCertificate) {
var _user = this._userById[payload.session];
this.emit('denied', 'MissingCertificate', _user, null, null);
} else if (payload.type === DenyType.UserName) {
this.emit('denied', 'UserName', null, null, payload.name);
} else if (payload.type === DenyType.ChannelFull) {
this.emit('denied', 'ChannelFull', null, null, null);
} else if (payload.type === DenyType.NestingLimit) {
this.emit('denied', 'NestingLimit', null, null, null);
} else {
throw Error('Invalid DenyType: ' + payload.type);
}
}
}, {
key: '_onTextMessage',
value: function _onTextMessage(payload) {
var _this5 = this;
this.emit('message', this._userById[payload.actor], payload.message, payload.session.map(function (id) {
return _this5._userById[id];
}), payload.channel_id.map(function (id) {
return _this5._channelById[id];
}), payload.tree_id.map(function (id) {
return _this5._channelById[id];
}));
}
}, {
key: '_onChannelState',
value: function _onChannelState(payload) {
var _this6 = this;
var channel = this._channelById[payload.channel_id];
if (!channel) {
channel = new _channel2.default(this, payload.channel_id);
this._channelById[channel._id] = channel;
this.channels.push(channel);
this.emit('newChannel', channel);
}
(payload.links_remove || []).forEach(function (otherId) {
var otherChannel = _this6._channelById[otherId];
if (otherChannel && otherChannel.links.indexOf(channel) !== -1) {
otherChannel._update({
links_remove: [payload.channel_id]
});
}
});
channel._update(payload);
}
}, {
key: '_onChannelRemove',
value: function _onChannelRemove(payload) {
var channel = this._channelById[payload.channel_id];
if (channel) {
channel._remove();
delete this._channelById[channel._id];
(0, _removeValue2.default)(this.channels, channel);
}
}
}, {
key: '_onUserState',
value: function _onUserState(payload) {
var user = this._userById[payload.session];
if (!user) {
user = new _user3.default(this, payload.session);
this._userById[user._id] = user;
this.users.push(user);
this.emit('newUser', user);
// For some reason, the mumble protocol does not send the initial
// channel of a client if it is the root channel
payload.channel_id = payload.channel_id || 0;
}
user._update(payload);
}
}, {
key: '_onUserRemove',
value: function _onUserRemove(payload) {
var user = this._userById[payload.session];
if (user) {
user._remove(this._userById[payload.actor], payload.reason, payload.ban);
delete this._userById[user._id];
(0, _removeValue2.default)(this.users, user);
}
}
/**
* Disconnect from the remote server.
* Once disconnected, this client may not be used again.
* Does nothing when not connected.
*/
}, {
key: 'disconnect',
value: function disconnect() {
if (this._disconnected) {
return;
}
this._disconnected = true;
this._voice.end();
this._data.end();
clearInterval(this._pinger);
this.emit('disconnected');
}
/**
* Set preferred audio bitrate and samples per packet.
*
* The {@link PCMData} passed to the stream returned by {@link createVoiceStream} must
* contain the appropriate amount of samples per channel for bandwidth control to
* function as expected.
*
* If this method is never called or false is passed as one of the values, then the
* samplesPerPacket are determined by inspecting the {@link PCMData} passed and the
* bitrate is calculated from the maximum bitrate advertised by the server.
*
* @param {number} bitrate - Preferred audio bitrate, sensible values are 8k to 96k
* @param {number} samplesPerPacket - Amount of samples per packet, valid values depend on the codec used but all should support 10ms (i.e. 480), 20ms, 40ms and 60ms
*/
}, {
key: 'setAudioQuality',
value: function setAudioQuality(bitrate, samplesPerPacket) {
this._preferredBitrate = bitrate;
this._samplesPerPacket = samplesPerPacket;
}
/**
* Calculate the actual bitrate taking into account maximum and preferred bitrate.
*/
}, {
key: 'getActualBitrate',
value: function getActualBitrate(samplesPerPacket, sendPosition) {
var bitrate = this.getPreferredBitrate(samplesPerPacket, sendPosition);
var bandwidth = MumbleClient.calcEnforcableBandwidth(bitrate, samplesPerPacket, sendPosition);
if (bandwidth <= this.maxBandwidth) {
return bitrate;
} else {
return this.getMaxBitrate(samplesPerPacket, sendPosition);
}
}
/**
* Returns the preferred bitrate set by {@link setAudioQuality} or
* {@link getMaxBitrate} if not set.
*/
}, {
key: 'getPreferredBitrate',
value: function getPreferredBitrate(samplesPerPacket, sendPosition) {
if (this._preferredBitrate) {
return this._preferredBitrate;
}
return this.getMaxBitrate(samplesPerPacket, sendPosition);
}
/**
* Calculate the maximum bitrate possible given the current server bandwidth limit.
*/
}, {
key: 'getMaxBitrate',
value: function getMaxBitrate(samplesPerPacket, sendPosition) {
var overhead = MumbleClient.calcEnforcableBandwidth(0, samplesPerPacket, sendPosition);
return this.maxBandwidth - overhead;
}
/**
* Calculate the bandwidth used if IP/UDP packets were used to transmit audio.
* This matches the value used by Mumble servers to enforce bandwidth limits.
* @returns {number} bits per second
*/
}, {
key: 'getChannel',
/**
* Find a channel by name.
* If no such channel exists, return null.
*
* @param {string} name - The full name of the channel
* @returns {?Channel}
*/
value: function getChannel(name) {
var _iteratorNormalCompletion2 = true;
var _didIteratorError2 = false;
var _iteratorError2 = undefined;
try {
for (var _iterator2 = this.channels[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
var channel = _step2.value;
if (channel.name === name) {
return channel;
}
}
} catch (err) {
_didIteratorError2 = true;
_iteratorError2 = err;
} finally {
try {
if (!_iteratorNormalCompletion2 && _iterator2.return) {
_iterator2.return();
}
} finally {
if (_didIteratorError2) {
throw _iteratorError2;
}
}
}
return null;
}
}, {
key: 'setSelfMute',
value: function setSelfMute(mute) {
var message = {
name: 'UserState',
payload: {
session: this.self._id,
self_mute: mute
}
};
if (!mute) message.payload.self_deaf = false;
this._send(message);
}
}, {
key: 'setSelfDeaf',
value: function setSelfDeaf(deaf) {
var message = {
name: 'UserState',
payload: {
session: this.self._id,
self_deaf: deaf
}
};
if (deaf) message.payload.self_mute = true;
this._send(message);
}
}, {
key: 'setSelfTexture',
value: function setSelfTexture(texture) {
this._send({
name: 'UserState',
payload: {
session: this.self._id,
texture: texture
}
});
}
}, {
key: 'setSelfComment',
value: function setSelfComment(comment) {
this._send({
name: 'UserState',
payload: {
session: this.self._id,
comment: comment
}
});
}
}, {
key: 'setPluginContext',
value: function setPluginContext(context) {
this._send({
name: 'UserState',
payload: {
session: this.self._id,
plugin_context: context
}
});
}
}, {
key: 'setPluginIdentity',
value: function setPluginIdentity(identity) {
this._send({
name: 'UserState',
payload: {
session: this.self._id,
plugin_identity: identity
}
});
}
}, {
key: 'setRecording',
value: function setRecording(recording) {
this._send({
name: 'UserState',
payload: {
session: this.self._id,
recording: recording
}
});
}
}, {
key: 'getChannelById',
value: function getChannelById(id) {
return this._channelById[id];
}
}, {
key: 'getUserById',
value: function getUserById(id) {
return this._userById[id];
}
}, {
key: 'root',
get: function get() {
return this._channelById[0];
}
}, {
key: 'connected',
get: function get() {
return !this._disconnected && this._dataStream != null;
}
}, {
key: 'dataStats',
get: function get() {
return this._dataStats.getAll();
}
}, {
key: 'voiceStats',
get: function get() {
return this._voiceStats.getAll();
}
}], [{
key: 'calcEnforcableBandwidth',
value: function calcEnforcableBandwidth(bitrate, samplesPerPacket, sendPosition) {
// IP + UDP + Crypt + Header + SeqNum (VarInt) + Codec Header + Optional Position
// Codec Header depends on codec:
// - Opus is always 4 (just the length as VarInt)
// - CELT/Speex depends on frames (10ms) per packet (1 byte each)
var codecHeaderBytes = Math.max(4, samplesPerPacket / 480);
var packetBytes = 20 + 8 + 4 + 1 + 4 + codecHeaderBytes + (sendPosition ? 12 : 0);
var packetsPerSecond = 48000 / samplesPerPacket;
return Math.round(packetBytes * 8 * packetsPerSecond + bitrate);
}
}]);
return MumbleClient;
}(_events.EventEmitter);
exports.default = MumbleClient;