worker-client.js 9.3 KB

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