mumble-web/app/worker-client.js
2020-04-14 22:06:08 +02:00

376 line
9.3 KiB
JavaScript

import MumbleClient from 'mumble-client'
import Promise from 'promise'
import EventEmitter from 'events'
import { Writable, PassThrough } from 'stream'
import toArrayBuffer from 'to-arraybuffer'
import ByteBuffer from 'bytebuffer'
import Worker from './worker'
/**
* Creates proxy MumbleClients to a real ones running on a web worker.
* Only stuff which we need in mumble-web is proxied, i.e. this is not a generic solution.
*/
class WorkerBasedMumbleConnector {
constructor () {
this._worker = new Worker()
this._worker.addEventListener('message', this._onMessage.bind(this))
this._reqId = 1
this._requests = {}
this._clients = {}
this._nextVoiceId = 1
this._voiceStreams = {}
}
setSampleRate (sampleRate) {
this._postMessage({
method: '_init',
sampleRate: sampleRate
})
}
_postMessage (msg, transfer) {
try {
this._worker.postMessage(msg, transfer)
} catch (err) {
console.error('Failed to postMessage', msg)
throw err
}
}
_call (id, method, payload, transfer) {
let reqId = this._reqId++
console.debug(method, id, payload)
this._postMessage({
clientId: id.client,
channelId: id.channel,
userId: id.user,
method: method,
reqId: reqId,
payload: payload
}, transfer)
return reqId
}
_query (id, method, payload, transfer) {
let reqId = this._call(id, method, payload, transfer)
return new Promise((resolve, reject) => {
this._requests[reqId] = [resolve, reject]
})
}
_addCall (proxy, name, id) {
let self = this
proxy[name] = function () {
self._call(id, name, Array.from(arguments))
}
}
connect (host, args) {
return this._query({}, '_connect', { host: host, args: args })
.then(id => this._client(id))
}
_client (id) {
let client = this._clients[id]
if (!client) {
client = new WorkerBasedMumbleClient(this, id)
this._clients[id] = client
}
return client
}
_onMessage (ev) {
let data = ev.data
if (data.reqId != null) {
console.debug(data)
let { reqId, result, error } = data
let [ resolve, reject ] = this._requests[reqId]
delete this._requests[reqId]
if (result) {
resolve(result)
} else {
reject(error)
}
} else if (data.clientId != null) {
console.debug(data)
let client = this._client(data.clientId)
let target
if (data.userId != null) {
target = client._user(data.userId)
} else if (data.channelId != null) {
target = client._channel(data.channelId)
} else {
target = client
}
if (data.event) {
target._dispatchEvent(data.event, data.value)
} else if (data.prop) {
target._setProp(data.prop, data.value)
}
} else if (data.voiceId != null) {
let stream = this._voiceStreams[data.voiceId]
let buffer = data.buffer
if (buffer) {
stream.write({
target: data.target,
buffer: Buffer.from(buffer)
})
} else {
delete this._voiceStreams[data.voiceId]
stream.end()
}
}
}
}
class WorkerBasedMumbleClient extends EventEmitter {
constructor (connector, clientId) {
super()
this._connector = connector
this._id = clientId
this._users = {}
this._channels = {}
let id = { client: clientId }
connector._addCall(this, 'setSelfDeaf', id)
connector._addCall(this, 'setSelfMute', id)
connector._addCall(this, 'setSelfTexture', id)
connector._addCall(this, 'setAudioQuality', id)
connector._addCall(this, 'disconnect', id)
let _disconnect = this.disconnect
this.disconnect = () => {
_disconnect.apply(this)
delete connector._clients[id]
}
connector._addCall(this, 'createVoiceStream', id)
let _createVoiceStream = this.createVoiceStream
this.createVoiceStream = function () {
let voiceId = connector._nextVoiceId++
let args = Array.from(arguments)
args.unshift(voiceId)
_createVoiceStream.apply(this, args)
return new Writable({
write (chunk, encoding, callback) {
chunk = toArrayBuffer(chunk)
connector._postMessage({
voiceId: voiceId,
chunk: chunk
})
callback()
},
final (callback) {
connector._postMessage({
voiceId: voiceId
})
callback()
}
})
}
// Dummy client used for bandwidth calculations
this._dummyClient = new MumbleClient({ username: 'dummy' })
let defineDummyMethod = (name) => {
this[name] = function () {
return this._dummyClient[name].apply(this._dummyClient, arguments)
}
}
defineDummyMethod('getMaxBitrate')
defineDummyMethod('getActualBitrate')
let _setAudioQuality = this.setAudioQuality
this.setAudioQuality = function () {
this._dummyClient.setAudioQuality.apply(this._dummyClient, arguments)
_setAudioQuality.apply(this, arguments)
}
}
_user (id) {
let user = this._users[id]
if (!user) {
user = new WorkerBasedMumbleUser(this._connector, this, id)
this._users[id] = user
}
return user
}
_channel (id) {
let channel = this._channels[id]
if (!channel) {
channel = new WorkerBasedMumbleChannel(this._connector, this, id)
this._channels[id] = channel
}
return channel
}
_dispatchEvent (name, args) {
if (name === 'newChannel') {
args[0] = this._channel(args[0])
} else if (name === 'newUser') {
args[0] = this._user(args[0])
} else if (name === 'message') {
args[0] = this._user(args[0])
args[2] = args[2].map((id) => this._user(id))
args[3] = args[3].map((id) => this._channel(id))
args[4] = args[4].map((id) => this._channel(id))
}
args.unshift(name)
this.emit.apply(this, args)
}
_setProp (name, value) {
if (name === 'root') {
name = '_rootId'
}
if (name === 'self') {
name = '_selfId'
}
if (name === 'maxBandwidth') {
this._dummyClient.maxBandwidth = value
}
this[name] = value
}
get root () {
return this._channel(this._rootId)
}
get channels () {
return Object.values(this._channels)
}
get users () {
return Object.values(this._users)
}
get self () {
return this._user(this._selfId)
}
}
class WorkerBasedMumbleChannel extends EventEmitter {
constructor (connector, client, channelId) {
super()
this._connector = connector
this._client = client
this._id = channelId
let id = { client: client._id, channel: channelId }
connector._addCall(this, 'sendMessage', id)
}
_dispatchEvent (name, args) {
if (name === 'update') {
let [props] = args
Object.entries(props).forEach((entry) => {
this._setProp(entry[0], entry[1])
})
if (props.parent != null) {
props.parent = this.parent
}
if (props.links != null) {
props.links = this.links
}
args = [
props
]
} else if (name === 'remove') {
delete this._client._channels[this._id]
}
args.unshift(name)
this.emit.apply(this, args)
}
_setProp (name, value) {
if (name === 'parent') {
name = '_parentId'
}
if (name === 'links') {
value = value.map((id) => this._client._channel(id))
}
this[name] = value
}
get parent () {
if (this._parentId != null) {
return this._client._channel(this._parentId)
}
}
get children () {
return Object.values(this._client._channels).filter((it) => it.parent === this)
}
}
class WorkerBasedMumbleUser extends EventEmitter {
constructor (connector, client, userId) {
super()
this._connector = connector
this._client = client
this._id = userId
let id = { client: client._id, user: userId }
connector._addCall(this, 'requestTexture', id)
connector._addCall(this, 'clearTexture', id)
connector._addCall(this, 'setMute', id)
connector._addCall(this, 'setDeaf', id)
connector._addCall(this, 'sendMessage', id)
this.setChannel = (channel) => {
connector._call(id, 'setChannel', channel._id)
}
}
_dispatchEvent (name, args) {
if (name === 'update') {
let [actor, props] = args
Object.entries(props).forEach((entry) => {
this._setProp(entry[0], entry[1])
})
if (props.channel != null) {
props.channel = this.channel
}
if (props.texture != null) {
props.texture = this.texture
}
args = [
this._client._user(actor),
props
]
} else if (name === 'voice') {
let [id] = args
let stream = new PassThrough({
objectMode: true
})
this._connector._voiceStreams[id] = stream
args = [stream]
} else if (name === 'remove') {
delete this._client._users[this._id]
}
args.unshift(name)
this.emit.apply(this, args)
}
_setProp (name, value) {
if (name === 'channel') {
name = '_channelId'
}
if (name === 'texture') {
if (value) {
let buf = ByteBuffer.wrap(value.buffer)
buf.offset = value.offset
buf.limit = value.limit
value = buf
}
}
this[name] = value
}
get channel () {
return this._client._channels[this._channelId]
}
}
export default WorkerBasedMumbleConnector