worker.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. import { Transform } from 'stream'
  2. import mumbleConnect from 'mumble-client-websocket'
  3. import toArrayBuffer from 'to-arraybuffer'
  4. import chunker from 'stream-chunker'
  5. import Resampler from 'libsamplerate.js'
  6. import CodecsBrowser from 'mumble-client-codecs-browser'
  7. // Polyfill nested webworkers for https://bugs.chromium.org/p/chromium/issues/detail?id=31666
  8. import 'subworkers'
  9. // Monkey-patch to allow webworkify-webpack and codecs to work inside of web worker
  10. /* global URL */
  11. //if (typeof window === 'undefined') global.window = {}
  12. //window.URL = URL
  13. // Using require to ensure ordering relative to monkey-patch above
  14. //let CodecsBrowser = require('mumble-client-codecs-browser')
  15. let sampleRate
  16. let nextClientId = 1
  17. let nextVoiceId = 1
  18. let voiceStreams = []
  19. let clients = []
  20. console.log('worker created!')
  21. function postMessage (msg, transfer) {
  22. try {
  23. self.postMessage(msg, transfer)
  24. } catch (err) {
  25. console.error('Failed to postMessage', msg)
  26. throw err
  27. }
  28. }
  29. function resolve (reqId, value, transfer) {
  30. postMessage({
  31. reqId: reqId,
  32. result: value
  33. }, transfer)
  34. }
  35. function reject (reqId, value, transfer) {
  36. console.error(value)
  37. let jsonValue = JSON.parse(JSON.stringify(value))
  38. if (value.$type) {
  39. jsonValue.$type = { name: value.$type.name }
  40. }
  41. postMessage({
  42. reqId: reqId,
  43. error: jsonValue
  44. }, transfer)
  45. }
  46. function registerEventProxy (id, obj, event, transform) {
  47. obj.on(event, function (_) {
  48. postMessage({
  49. clientId: id.client,
  50. channelId: id.channel,
  51. userId: id.user,
  52. event: event,
  53. value: transform ? transform.apply(null, arguments) : Array.from(arguments)
  54. })
  55. })
  56. }
  57. function pushProp (id, obj, prop, transform) {
  58. let value = obj[prop]
  59. postMessage({
  60. clientId: id.client,
  61. channelId: id.channel,
  62. userId: id.user,
  63. prop: prop,
  64. value: transform ? transform(value) : value
  65. })
  66. }
  67. function setupOutboundVoice (voiceId, samplesPerPacket, stream) {
  68. let resampler = new Resampler({
  69. unsafe: true,
  70. type: Resampler.Type.SINC_FASTEST,
  71. ratio: 48000 / sampleRate
  72. })
  73. let buffer2Float32Array = new Transform({
  74. transform (data, _, callback) {
  75. callback(null, new Float32Array(data.buffer, data.byteOffset, data.byteLength / 4))
  76. },
  77. readableObjectMode: true
  78. })
  79. resampler
  80. .pipe(chunker(4 * samplesPerPacket))
  81. .pipe(buffer2Float32Array)
  82. .pipe(stream)
  83. voiceStreams[voiceId] = resampler
  84. }
  85. function setupChannel (id, channel) {
  86. id = Object.assign({}, id, { channel: channel.id })
  87. registerEventProxy(id, channel, 'update', (actor, props) => {
  88. if (actor) {
  89. actor = actor.id
  90. }
  91. if (props.parent) {
  92. props.parent = props.parent.id
  93. }
  94. if (props.links) {
  95. props.links = props.links.map((it) => it.id)
  96. }
  97. return [actor, props]
  98. })
  99. registerEventProxy(id, channel, 'remove')
  100. pushProp(id, channel, 'parent', (it) => it ? it.id : it)
  101. pushProp(id, channel, 'links', (it) => it.map((it) => it.id))
  102. let props = [
  103. 'position', 'name', 'description'
  104. ]
  105. for (let prop of props) {
  106. pushProp(id, channel, prop)
  107. }
  108. for (let child of channel.children) {
  109. setupChannel(id, child)
  110. }
  111. return channel.id
  112. }
  113. function setupUser (id, user) {
  114. id = Object.assign({}, id, { user: user.id })
  115. registerEventProxy(id, user, 'update', (actor, props) => {
  116. if (actor) {
  117. actor = actor.id
  118. }
  119. if (props.channel != null) {
  120. props.channel = props.channel.id
  121. }
  122. return [actor, props]
  123. })
  124. registerEventProxy(id, user, 'voice', (stream) => {
  125. let voiceId = nextVoiceId++
  126. let target
  127. // We want to do as little on the UI thread as possible, so do resampling here as well
  128. var resampler = new Resampler({
  129. unsafe: true,
  130. type: Resampler.Type.ZERO_ORDER_HOLD,
  131. ratio: sampleRate / 48000
  132. })
  133. // Pipe stream into resampler
  134. stream.on('data', (data) => {
  135. // store target so we can pass it on after resampling
  136. target = data.target
  137. resampler.write(Buffer.from(data.pcm.buffer))
  138. }).on('end', () => {
  139. resampler.end()
  140. })
  141. // Pipe resampler into output stream on UI thread
  142. resampler.on('data', (data) => {
  143. data = toArrayBuffer(data) // postMessage can't transfer node's Buffer
  144. postMessage({
  145. voiceId: voiceId,
  146. target: target,
  147. buffer: data
  148. }, [data])
  149. }).on('end', () => {
  150. postMessage({
  151. voiceId: voiceId
  152. })
  153. })
  154. return [voiceId]
  155. })
  156. registerEventProxy(id, user, 'remove')
  157. pushProp(id, user, 'channel', (it) => it ? it.id : it)
  158. let props = [
  159. 'uniqueId', 'username', 'mute', 'deaf', 'suppress', 'selfMute', 'selfDeaf',
  160. 'texture', 'textureHash', 'comment'
  161. ]
  162. for (let prop of props) {
  163. pushProp(id, user, prop)
  164. }
  165. return user.id
  166. }
  167. function setupClient (id, client) {
  168. id = { client: id }
  169. registerEventProxy(id, client, 'error')
  170. registerEventProxy(id, client, 'newChannel', (it) => [setupChannel(id, it)])
  171. registerEventProxy(id, client, 'newUser', (it) => [setupUser(id, it)])
  172. registerEventProxy(id, client, 'message', (sender, message, users, channels, trees) => {
  173. return [
  174. sender.id,
  175. message,
  176. users.map((it) => it.id),
  177. channels.map((it) => it.id),
  178. trees.map((it) => it.id)
  179. ]
  180. })
  181. client.on('dataPing', () => {
  182. pushProp(id, client, 'dataStats')
  183. })
  184. setupChannel(id, client.root)
  185. for (let user of client.users) {
  186. setupUser(id, user)
  187. }
  188. pushProp(id, client, 'root', (it) => it.id)
  189. pushProp(id, client, 'self', (it) => it.id)
  190. pushProp(id, client, 'welcomeMessage')
  191. pushProp(id, client, 'serverVersion')
  192. pushProp(id, client, 'maxBandwidth')
  193. }
  194. function onMessage (data) {
  195. let { reqId, method, payload } = data
  196. if (method === '_init') {
  197. sampleRate = data.sampleRate
  198. } else if (method === '_connect') {
  199. payload.args.codecs = CodecsBrowser
  200. mumbleConnect(payload.host, payload.args).then((client) => {
  201. let id = nextClientId++
  202. clients[id] = client
  203. setupClient(id, client)
  204. return id
  205. }).done((id) => {
  206. resolve(reqId, id)
  207. }, (err) => {
  208. reject(reqId, err)
  209. })
  210. } else if (data.clientId != null) {
  211. let client = clients[data.clientId]
  212. let target
  213. if (data.userId != null) {
  214. target = client.getUserById(data.userId)
  215. if (method === 'setChannel') {
  216. payload = [client.getChannelById(payload)]
  217. }
  218. } else if (data.channelId != null) {
  219. target = client.getChannelById(data.channelId)
  220. } else {
  221. target = client
  222. if (method === 'createVoiceStream') {
  223. let voiceId = payload.shift()
  224. let samplesPerPacket = payload.shift()
  225. let stream = target.createVoiceStream.apply(target, payload)
  226. setupOutboundVoice(voiceId, samplesPerPacket, stream)
  227. return
  228. }
  229. if (method === 'disconnect') {
  230. delete clients[data.clientId]
  231. }
  232. }
  233. target[method].apply(target, payload)
  234. } else if (data.voiceId != null) {
  235. let stream = voiceStreams[data.voiceId]
  236. let buffer = data.chunk
  237. if (buffer) {
  238. stream.write(Buffer.from(buffer))
  239. } else {
  240. delete voiceStreams[data.voiceId]
  241. stream.end()
  242. }
  243. }
  244. }
  245. self.addEventListener('message', (ev) => {
  246. try {
  247. onMessage(ev.data)
  248. } catch (ex) {
  249. console.error('exception during message event', ev.data, ex)
  250. }
  251. })
  252. export default null