worker-client.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  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. export 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, '_send', id)
  129. connector._addCall(this, 'disconnect', id)
  130. let _disconnect = this.disconnect
  131. this.disconnect = () => {
  132. _disconnect.apply(this)
  133. delete connector._clients[id]
  134. }
  135. connector._addCall(this, 'createVoiceStream', id)
  136. let _createVoiceStream = this.createVoiceStream
  137. this.createVoiceStream = function () {
  138. let voiceId = connector._nextVoiceId++
  139. let args = Array.from(arguments)
  140. args.unshift(voiceId)
  141. _createVoiceStream.apply(this, args)
  142. return new Writable({
  143. write (chunk, encoding, callback) {
  144. chunk = toArrayBuffer(chunk)
  145. connector._postMessage({
  146. voiceId: voiceId,
  147. chunk: chunk
  148. })
  149. callback()
  150. },
  151. final (callback) {
  152. connector._postMessage({
  153. voiceId: voiceId
  154. })
  155. callback()
  156. }
  157. })
  158. }
  159. // Dummy client used for bandwidth calculations
  160. this._dummyClient = new MumbleClient({ username: 'dummy' })
  161. let defineDummyMethod = (name) => {
  162. this[name] = function () {
  163. return this._dummyClient[name].apply(this._dummyClient, arguments)
  164. }
  165. }
  166. defineDummyMethod('getMaxBitrate')
  167. defineDummyMethod('getActualBitrate')
  168. let _setAudioQuality = this.setAudioQuality
  169. this.setAudioQuality = function () {
  170. this._dummyClient.setAudioQuality.apply(this._dummyClient, arguments)
  171. _setAudioQuality.apply(this, arguments)
  172. }
  173. }
  174. _user (id) {
  175. let user = this._users[id]
  176. if (!user) {
  177. user = new WorkerBasedMumbleUser(this._connector, this, id)
  178. this._users[id] = user
  179. }
  180. return user
  181. }
  182. _channel (id) {
  183. let channel = this._channels[id]
  184. if (!channel) {
  185. channel = new WorkerBasedMumbleChannel(this._connector, this, id)
  186. this._channels[id] = channel
  187. }
  188. return channel
  189. }
  190. _dispatchEvent (name, args) {
  191. if (name === 'newChannel') {
  192. args[0] = this._channel(args[0])
  193. } else if (name === 'newUser') {
  194. args[0] = this._user(args[0])
  195. } else if (name === 'message') {
  196. args[0] = this._user(args[0])
  197. args[2] = args[2].map((id) => this._user(id))
  198. args[3] = args[3].map((id) => this._channel(id))
  199. args[4] = args[4].map((id) => this._channel(id))
  200. }
  201. args.unshift(name)
  202. this.emit.apply(this, args)
  203. }
  204. _setProp (name, value) {
  205. if (name === 'root') {
  206. name = '_rootId'
  207. }
  208. if (name === 'self') {
  209. name = '_selfId'
  210. }
  211. if (name === 'maxBandwidth') {
  212. this._dummyClient.maxBandwidth = value
  213. }
  214. this[name] = value
  215. }
  216. get root () {
  217. return this._channel(this._rootId)
  218. }
  219. get channels () {
  220. return Object.values(this._channels)
  221. }
  222. get users () {
  223. return Object.values(this._users)
  224. }
  225. get self () {
  226. return this._user(this._selfId)
  227. }
  228. }
  229. class WorkerBasedMumbleChannel extends EventEmitter {
  230. constructor (connector, client, channelId) {
  231. super()
  232. this._connector = connector
  233. this._client = client
  234. this._id = channelId
  235. let id = { client: client._id, channel: channelId }
  236. connector._addCall(this, 'sendMessage', id)
  237. }
  238. _dispatchEvent (name, args) {
  239. if (name === 'update') {
  240. let [props] = args
  241. Object.entries(props).forEach((entry) => {
  242. this._setProp(entry[0], entry[1])
  243. })
  244. if (props.parent != null) {
  245. props.parent = this.parent
  246. }
  247. if (props.links != null) {
  248. props.links = this.links
  249. }
  250. args = [
  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, target] = args
  311. let stream = new PassThrough({
  312. objectMode: true
  313. })
  314. this._connector._voiceStreams[id] = stream
  315. stream.target = target
  316. args = [stream]
  317. } else if (name === 'remove') {
  318. delete this._client._users[this._id]
  319. }
  320. args.unshift(name)
  321. this.emit.apply(this, args)
  322. }
  323. _setProp (name, value) {
  324. if (name === 'channel') {
  325. name = '_channelId'
  326. }
  327. if (name === 'texture') {
  328. if (value) {
  329. let buf = ByteBuffer.wrap(value.buffer)
  330. buf.offset = value.offset
  331. buf.limit = value.limit
  332. value = buf
  333. }
  334. }
  335. this[name] = value
  336. }
  337. get channel () {
  338. return this._client._channels[this._channelId]
  339. }
  340. }
  341. export default WorkerBasedMumbleConnector