worker-client.js 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. import MumbleClient from 'mumble-client'
  2. import Promise from 'promise'
  3. import EventEmitter from 'events'
  4. import { Writable, PassThrough } from 'stream'
  5. import toArrayBuffer from 'to-arraybuffer'
  6. import ByteBuffer from 'bytebuffer'
  7. import Worker from './worker'
  8. /**
  9. * Creates proxy MumbleClients to a real ones running on a web worker.
  10. * Only stuff which we need in mumble-web is proxied, i.e. this is not a generic solution.
  11. */
  12. class WorkerBasedMumbleConnector {
  13. constructor () {
  14. this._reqId = 1
  15. this._requests = {}
  16. this._clients = {}
  17. this._nextVoiceId = 1
  18. this._voiceStreams = {}
  19. }
  20. setSampleRate (sampleRate) {
  21. this._postMessage({
  22. method: '_init',
  23. sampleRate: sampleRate
  24. })
  25. }
  26. _postMessage (msg, transfer) {
  27. if (!this._worker) {
  28. this._worker = new Worker()
  29. this._worker.addEventListener('message', this._onMessage.bind(this))
  30. }
  31. try {
  32. this._worker.postMessage(msg, transfer)
  33. } catch (err) {
  34. console.error('Failed to postMessage', msg)
  35. throw err
  36. }
  37. }
  38. _call (id, method, payload, transfer) {
  39. let reqId = this._reqId++
  40. console.debug(method, id, payload)
  41. this._postMessage({
  42. clientId: id.client,
  43. channelId: id.channel,
  44. userId: id.user,
  45. method: method,
  46. reqId: reqId,
  47. payload: payload
  48. }, transfer)
  49. return reqId
  50. }
  51. _query (id, method, payload, transfer) {
  52. let reqId = this._call(id, method, payload, transfer)
  53. return new Promise((resolve, reject) => {
  54. this._requests[reqId] = [resolve, reject]
  55. })
  56. }
  57. _addCall (proxy, name, id) {
  58. let self = this
  59. proxy[name] = function () {
  60. self._call(id, name, Array.from(arguments))
  61. }
  62. }
  63. connect (host, args) {
  64. return this._query({}, '_connect', { host: host, args: args })
  65. .then(id => this._client(id))
  66. }
  67. _client (id) {
  68. let client = this._clients[id]
  69. if (!client) {
  70. client = new WorkerBasedMumbleClient(this, id)
  71. this._clients[id] = client
  72. }
  73. return client
  74. }
  75. _onMessage (ev) {
  76. let data = ev.data
  77. if (data.reqId != null) {
  78. console.debug(data)
  79. let { reqId, result, error } = data
  80. let [ resolve, reject ] = this._requests[reqId]
  81. delete this._requests[reqId]
  82. if (result) {
  83. resolve(result)
  84. } else {
  85. reject(error)
  86. }
  87. } else if (data.clientId != null) {
  88. console.debug(data)
  89. let client = this._client(data.clientId)
  90. let target
  91. if (data.userId != null) {
  92. target = client._user(data.userId)
  93. } else if (data.channelId != null) {
  94. target = client._channel(data.channelId)
  95. } else {
  96. target = client
  97. }
  98. if (data.event) {
  99. target._dispatchEvent(data.event, data.value)
  100. } else if (data.prop) {
  101. target._setProp(data.prop, data.value)
  102. }
  103. } else if (data.voiceId != null) {
  104. let stream = this._voiceStreams[data.voiceId]
  105. let buffer = data.buffer
  106. if (buffer) {
  107. stream.write({
  108. target: data.target,
  109. buffer: Buffer.from(buffer)
  110. })
  111. } else {
  112. delete this._voiceStreams[data.voiceId]
  113. stream.end()
  114. }
  115. }
  116. }
  117. }
  118. export class WorkerBasedMumbleClient extends EventEmitter {
  119. constructor (connector, clientId) {
  120. super()
  121. this._connector = connector
  122. this._id = clientId
  123. this._users = {}
  124. this._channels = {}
  125. let id = { client: clientId }
  126. connector._addCall(this, 'setSelfDeaf', id)
  127. connector._addCall(this, 'setSelfMute', id)
  128. connector._addCall(this, 'setSelfTexture', id)
  129. connector._addCall(this, 'setAudioQuality', id)
  130. connector._addCall(this, '_send', id)
  131. connector._addCall(this, 'disconnect', id)
  132. let _disconnect = this.disconnect
  133. this.disconnect = () => {
  134. _disconnect.apply(this)
  135. delete connector._clients[id]
  136. }
  137. connector._addCall(this, 'createVoiceStream', id)
  138. let _createVoiceStream = this.createVoiceStream
  139. this.createVoiceStream = function () {
  140. let voiceId = connector._nextVoiceId++
  141. let args = Array.from(arguments)
  142. args.unshift(voiceId)
  143. _createVoiceStream.apply(this, args)
  144. return new Writable({
  145. write (chunk, encoding, callback) {
  146. chunk = toArrayBuffer(chunk)
  147. connector._postMessage({
  148. voiceId: voiceId,
  149. chunk: chunk
  150. })
  151. callback()
  152. },
  153. final (callback) {
  154. connector._postMessage({
  155. voiceId: voiceId
  156. })
  157. callback()
  158. }
  159. })
  160. }
  161. // Dummy client used for bandwidth calculations
  162. this._dummyClient = new MumbleClient({ username: 'dummy' })
  163. let defineDummyMethod = (name) => {
  164. this[name] = function () {
  165. return this._dummyClient[name].apply(this._dummyClient, arguments)
  166. }
  167. }
  168. defineDummyMethod('getMaxBitrate')
  169. defineDummyMethod('getActualBitrate')
  170. let _setAudioQuality = this.setAudioQuality
  171. this.setAudioQuality = function () {
  172. this._dummyClient.setAudioQuality.apply(this._dummyClient, arguments)
  173. _setAudioQuality.apply(this, arguments)
  174. }
  175. }
  176. _user (id) {
  177. let user = this._users[id]
  178. if (!user) {
  179. user = new WorkerBasedMumbleUser(this._connector, this, id)
  180. this._users[id] = user
  181. }
  182. return user
  183. }
  184. _channel (id) {
  185. let channel = this._channels[id]
  186. if (!channel) {
  187. channel = new WorkerBasedMumbleChannel(this._connector, this, id)
  188. this._channels[id] = channel
  189. }
  190. return channel
  191. }
  192. _dispatchEvent (name, args) {
  193. if (name === 'newChannel') {
  194. args[0] = this._channel(args[0])
  195. } else if (name === 'newUser') {
  196. args[0] = this._user(args[0])
  197. } else if (name === 'message') {
  198. args[0] = this._user(args[0])
  199. args[2] = args[2].map((id) => this._user(id))
  200. args[3] = args[3].map((id) => this._channel(id))
  201. args[4] = args[4].map((id) => this._channel(id))
  202. } else if (name === 'error') {
  203. this.emit('error', args)
  204. return
  205. }
  206. args.unshift(name)
  207. this.emit.apply(this, args)
  208. }
  209. _setProp (name, value) {
  210. if (name === 'root') {
  211. name = '_rootId'
  212. }
  213. if (name === 'self') {
  214. name = '_selfId'
  215. }
  216. if (name === 'maxBandwidth') {
  217. this._dummyClient.maxBandwidth = value
  218. }
  219. this[name] = value
  220. }
  221. get root () {
  222. return this._channel(this._rootId)
  223. }
  224. get channels () {
  225. return Object.values(this._channels)
  226. }
  227. get users () {
  228. return Object.values(this._users)
  229. }
  230. get self () {
  231. return this._user(this._selfId)
  232. }
  233. }
  234. class WorkerBasedMumbleChannel extends EventEmitter {
  235. constructor (connector, client, channelId) {
  236. super()
  237. this._connector = connector
  238. this._client = client
  239. this._id = channelId
  240. let id = { client: client._id, channel: channelId }
  241. connector._addCall(this, 'sendMessage', id)
  242. }
  243. _dispatchEvent (name, args) {
  244. if (name === 'update') {
  245. let [props] = args
  246. Object.entries(props).forEach((entry) => {
  247. this._setProp(entry[0], entry[1])
  248. })
  249. if (props.parent != null) {
  250. props.parent = this.parent
  251. }
  252. if (props.links != null) {
  253. props.links = this.links
  254. }
  255. args = [
  256. props
  257. ]
  258. } else if (name === 'remove') {
  259. delete this._client._channels[this._id]
  260. }
  261. args.unshift(name)
  262. this.emit.apply(this, args)
  263. }
  264. _setProp (name, value) {
  265. if (name === 'parent') {
  266. name = '_parentId'
  267. }
  268. if (name === 'links') {
  269. value = value.map((id) => this._client._channel(id))
  270. }
  271. this[name] = value
  272. }
  273. get parent () {
  274. if (this._parentId != null) {
  275. return this._client._channel(this._parentId)
  276. }
  277. }
  278. get children () {
  279. return Object.values(this._client._channels).filter((it) => it.parent === this)
  280. }
  281. }
  282. class WorkerBasedMumbleUser extends EventEmitter {
  283. constructor (connector, client, userId) {
  284. super()
  285. this._connector = connector
  286. this._client = client
  287. this._id = userId
  288. let id = { client: client._id, user: userId }
  289. connector._addCall(this, 'requestTexture', id)
  290. connector._addCall(this, 'clearTexture', id)
  291. connector._addCall(this, 'setMute', id)
  292. connector._addCall(this, 'setDeaf', id)
  293. connector._addCall(this, 'sendMessage', id)
  294. this.setChannel = (channel) => {
  295. connector._call(id, 'setChannel', channel._id)
  296. }
  297. }
  298. _dispatchEvent (name, args) {
  299. if (name === 'update') {
  300. let [actor, props] = args
  301. Object.entries(props).forEach((entry) => {
  302. this._setProp(entry[0], entry[1])
  303. })
  304. if (props.channel != null) {
  305. props.channel = this.channel
  306. }
  307. if (props.texture != null) {
  308. props.texture = this.texture
  309. }
  310. args = [
  311. this._client._user(actor),
  312. props
  313. ]
  314. } else if (name === 'voice') {
  315. let [id, target] = args
  316. let stream = new PassThrough({
  317. objectMode: true
  318. })
  319. this._connector._voiceStreams[id] = stream
  320. stream.target = target
  321. args = [stream]
  322. } else if (name === 'remove') {
  323. delete this._client._users[this._id]
  324. }
  325. args.unshift(name)
  326. this.emit.apply(this, args)
  327. }
  328. _setProp (name, value) {
  329. if (name === 'channel') {
  330. name = '_channelId'
  331. }
  332. if (name === 'texture') {
  333. if (value) {
  334. let buf = ByteBuffer.wrap(value.buffer)
  335. buf.offset = value.offset
  336. buf.limit = value.limit
  337. value = buf
  338. }
  339. }
  340. this[name] = value
  341. }
  342. get channel () {
  343. return this._client._channels[this._channelId]
  344. }
  345. }
  346. export default WorkerBasedMumbleConnector