index.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987
  1. import 'stream-browserify' // see https://github.com/ericgundrum/pouch-websocket-sync-example/commit/2a4437b013092cc7b2cd84cf1499172c84a963a3
  2. import url from 'url'
  3. import ByteBuffer from 'bytebuffer'
  4. import MumbleClient from 'mumble-client'
  5. import WorkerBasedMumbleConnector from './worker-client'
  6. import BufferQueueNode from 'web-audio-buffer-queue'
  7. import audioContext from 'audio-context'
  8. import ko from 'knockout'
  9. import _dompurify from 'dompurify'
  10. import keyboardjs from 'keyboardjs'
  11. import { ContinuousVoiceHandler, PushToTalkVoiceHandler, VADVoiceHandler, initVoice } from './voice'
  12. const dompurify = _dompurify(window)
  13. function sanitize (html) {
  14. return dompurify.sanitize(html, {
  15. ALLOWED_TAGS: ['br', 'b', 'i', 'u', 'a', 'span', 'p']
  16. })
  17. }
  18. function openContextMenu (event, contextMenu, target) {
  19. contextMenu.posX(event.clientX)
  20. contextMenu.posY(event.clientY)
  21. contextMenu.target(target)
  22. const closeListener = (event) => {
  23. // Always close, no matter where they clicked
  24. setTimeout(() => { // delay to allow click to be actually processed
  25. contextMenu.target(null)
  26. unregister()
  27. })
  28. }
  29. const unregister = () => document.removeEventListener('click', closeListener)
  30. document.addEventListener('click', closeListener)
  31. event.stopPropagation()
  32. event.preventDefault()
  33. }
  34. // GUI
  35. function ContextMenu () {
  36. var self = this
  37. self.posX = ko.observable()
  38. self.posY = ko.observable()
  39. self.target = ko.observable()
  40. }
  41. function ConnectDialog () {
  42. var self = this
  43. self.address = ko.observable('')
  44. self.port = ko.observable('443')
  45. self.token = ko.observable('')
  46. self.username = ko.observable('')
  47. self.password = ko.observable('')
  48. self.joinOnly = ko.observable(false)
  49. self.visible = ko.observable(true)
  50. self.show = self.visible.bind(self.visible, true)
  51. self.hide = self.visible.bind(self.visible, false)
  52. self.connect = function () {
  53. self.hide()
  54. ui.connect(self.username(), self.address(), self.port(), self.token(), self.password())
  55. }
  56. }
  57. function ConnectErrorDialog (connectDialog) {
  58. var self = this
  59. self.type = ko.observable(0)
  60. self.reason = ko.observable('')
  61. self.username = connectDialog.username
  62. self.password = connectDialog.password
  63. self.joinOnly = connectDialog.joinOnly
  64. self.visible = ko.observable(false)
  65. self.show = self.visible.bind(self.visible, true)
  66. self.hide = self.visible.bind(self.visible, false)
  67. self.connect = () => {
  68. self.hide()
  69. connectDialog.connect()
  70. }
  71. }
  72. class ConnectionInfo {
  73. constructor (ui) {
  74. this._ui = ui
  75. this.visible = ko.observable(false)
  76. this.serverVersion = ko.observable()
  77. this.latencyMs = ko.observable(NaN)
  78. this.latencyDeviation = ko.observable(NaN)
  79. this.remoteHost = ko.observable()
  80. this.remotePort = ko.observable()
  81. this.maxBitrate = ko.observable(NaN)
  82. this.currentBitrate = ko.observable(NaN)
  83. this.maxBandwidth = ko.observable(NaN)
  84. this.currentBandwidth = ko.observable(NaN)
  85. this.codec = ko.observable()
  86. this.show = () => {
  87. if (!ui.thisUser()) return
  88. this.update()
  89. this.visible(true)
  90. }
  91. this.hide = () => this.visible(false)
  92. }
  93. update () {
  94. let client = this._ui.client
  95. this.serverVersion(client.serverVersion)
  96. let dataStats = client.dataStats
  97. if (dataStats) {
  98. this.latencyMs(dataStats.mean)
  99. this.latencyDeviation(Math.sqrt(dataStats.variance))
  100. }
  101. this.remoteHost(this._ui.remoteHost())
  102. this.remotePort(this._ui.remotePort())
  103. let spp = this._ui.settings.samplesPerPacket
  104. let maxBitrate = client.getMaxBitrate(spp, false)
  105. let maxBandwidth = client.maxBandwidth
  106. let actualBitrate = client.getActualBitrate(spp, false)
  107. let actualBandwidth = MumbleClient.calcEnforcableBandwidth(actualBitrate, spp, false)
  108. this.maxBitrate(maxBitrate)
  109. this.currentBitrate(actualBitrate)
  110. this.maxBandwidth(maxBandwidth)
  111. this.currentBandwidth(actualBandwidth)
  112. this.codec('Opus') // only one supported for sending
  113. }
  114. }
  115. function CommentDialog () {
  116. var self = this
  117. self.visible = ko.observable(false)
  118. self.show = function () {
  119. self.visible(true)
  120. }
  121. }
  122. class SettingsDialog {
  123. constructor (settings) {
  124. this.voiceMode = ko.observable(settings.voiceMode)
  125. this.pttKey = ko.observable(settings.pttKey)
  126. this.pttKeyDisplay = ko.observable(settings.pttKey)
  127. this.vadLevel = ko.observable(settings.vadLevel)
  128. this.testVadLevel = ko.observable(0)
  129. this.testVadActive = ko.observable(false)
  130. this.showAvatars = ko.observable(settings.showAvatars())
  131. // Need to wrap this in a pureComputed to make sure it's always numeric
  132. let audioBitrate = ko.observable(settings.audioBitrate)
  133. this.audioBitrate = ko.pureComputed({
  134. read: audioBitrate,
  135. write: (value) => audioBitrate(Number(value))
  136. })
  137. this.samplesPerPacket = ko.observable(settings.samplesPerPacket)
  138. this.msPerPacket = ko.pureComputed({
  139. read: () => this.samplesPerPacket() / 48,
  140. write: (value) => this.samplesPerPacket(value * 48)
  141. })
  142. this._setupTestVad()
  143. this.vadLevel.subscribe(() => this._setupTestVad())
  144. }
  145. _setupTestVad () {
  146. if (this._testVad) {
  147. this._testVad.end()
  148. }
  149. let dummySettings = new Settings()
  150. this.applyTo(dummySettings)
  151. this._testVad = new VADVoiceHandler(null, dummySettings)
  152. this._testVad.on('started_talking', () => this.testVadActive(true))
  153. .on('stopped_talking', () => this.testVadActive(false))
  154. .on('level', level => this.testVadLevel(level))
  155. testVoiceHandler = this._testVad
  156. }
  157. applyTo (settings) {
  158. settings.voiceMode = this.voiceMode()
  159. settings.pttKey = this.pttKey()
  160. settings.vadLevel = this.vadLevel()
  161. settings.showAvatars(this.showAvatars())
  162. settings.audioBitrate = this.audioBitrate()
  163. settings.samplesPerPacket = this.samplesPerPacket()
  164. }
  165. end () {
  166. this._testVad.end()
  167. testVoiceHandler = null
  168. }
  169. recordPttKey () {
  170. var combo = []
  171. const keydown = e => {
  172. combo = e.pressedKeys
  173. let comboStr = combo.join(' + ')
  174. this.pttKeyDisplay('> ' + comboStr + ' <')
  175. }
  176. const keyup = () => {
  177. keyboardjs.unbind('', keydown, keyup)
  178. let comboStr = combo.join(' + ')
  179. if (comboStr) {
  180. this.pttKey(comboStr).pttKeyDisplay(comboStr)
  181. } else {
  182. this.pttKeyDisplay(this.pttKey())
  183. }
  184. }
  185. keyboardjs.bind('', keydown, keyup)
  186. this.pttKeyDisplay('> ? <')
  187. }
  188. totalBandwidth () {
  189. return MumbleClient.calcEnforcableBandwidth(
  190. this.audioBitrate(),
  191. this.samplesPerPacket(),
  192. true
  193. )
  194. }
  195. positionBandwidth () {
  196. return this.totalBandwidth() - MumbleClient.calcEnforcableBandwidth(
  197. this.audioBitrate(),
  198. this.samplesPerPacket(),
  199. false
  200. )
  201. }
  202. overheadBandwidth () {
  203. return MumbleClient.calcEnforcableBandwidth(
  204. 0,
  205. this.samplesPerPacket(),
  206. false
  207. )
  208. }
  209. }
  210. class Settings {
  211. constructor () {
  212. const load = key => window.localStorage.getItem('mumble.' + key)
  213. this.voiceMode = load('voiceMode') || 'vad'
  214. this.pttKey = load('pttKey') || 'ctrl + shift'
  215. this.vadLevel = load('vadLevel') || 0.3
  216. this.toolbarVertical = load('toolbarVertical') || false
  217. this.showAvatars = ko.observable(load('showAvatars') || 'always')
  218. this.audioBitrate = Number(load('audioBitrate')) || 40000
  219. this.samplesPerPacket = Number(load('samplesPerPacket')) || 960
  220. }
  221. save () {
  222. const save = (key, val) => window.localStorage.setItem('mumble.' + key, val)
  223. save('voiceMode', this.voiceMode)
  224. save('pttKey', this.pttKey)
  225. save('vadLevel', this.vadLevel)
  226. save('toolbarVertical', this.toolbarVertical)
  227. save('showAvatars', this.showAvatars())
  228. save('audioBitrate', this.audioBitrate)
  229. save('samplesPerPacket', this.samplesPerPacket)
  230. }
  231. }
  232. class GlobalBindings {
  233. constructor () {
  234. this.settings = new Settings()
  235. this.connector = new WorkerBasedMumbleConnector(audioContext.sampleRate)
  236. this.client = null
  237. this.userContextMenu = new ContextMenu()
  238. this.channelContextMenu = new ContextMenu()
  239. this.connectDialog = new ConnectDialog()
  240. this.connectErrorDialog = new ConnectErrorDialog(this.connectDialog)
  241. this.connectionInfo = new ConnectionInfo(this)
  242. this.commentDialog = new CommentDialog()
  243. this.settingsDialog = ko.observable()
  244. this.minimalView = ko.observable(false)
  245. this.log = ko.observableArray()
  246. this.remoteHost = ko.observable()
  247. this.remotePort = ko.observable()
  248. this.thisUser = ko.observable()
  249. this.root = ko.observable()
  250. this.avatarView = ko.observable()
  251. this.messageBox = ko.observable('')
  252. this.toolbarHorizontal = ko.observable(!this.settings.toolbarVertical)
  253. this.selected = ko.observable()
  254. this.selfMute = ko.observable()
  255. this.selfDeaf = ko.observable()
  256. this.selfMute.subscribe(mute => {
  257. if (voiceHandler) {
  258. voiceHandler.setMute(mute)
  259. }
  260. })
  261. this.toggleToolbarOrientation = () => {
  262. this.toolbarHorizontal(!this.toolbarHorizontal())
  263. this.settings.toolbarVertical = !this.toolbarHorizontal()
  264. this.settings.save()
  265. }
  266. this.select = element => {
  267. this.selected(element)
  268. }
  269. this.openSettings = () => {
  270. this.settingsDialog(new SettingsDialog(this.settings))
  271. }
  272. this.applySettings = () => {
  273. const settingsDialog = this.settingsDialog()
  274. settingsDialog.applyTo(this.settings)
  275. this._updateVoiceHandler()
  276. this.settings.save()
  277. this.closeSettings()
  278. }
  279. this.closeSettings = () => {
  280. if (this.settingsDialog()) {
  281. this.settingsDialog().end()
  282. }
  283. this.settingsDialog(null)
  284. }
  285. this.getTimeString = () => {
  286. return '[' + new Date().toLocaleTimeString('en-US') + ']'
  287. }
  288. this.connect = (username, host, port, token, password) => {
  289. this.resetClient()
  290. this.remoteHost(host)
  291. this.remotePort(port)
  292. log('Connecting to server ', host)
  293. // TODO: token
  294. this.connector.connect(`wss://${host}:${port}`, {
  295. username: username,
  296. password: password
  297. }).done(client => {
  298. log('Connected!')
  299. this.client = client
  300. // Prepare for connection errors
  301. client.on('error', (err) => {
  302. log('Connection error:', err)
  303. this.resetClient()
  304. })
  305. // Make sure we stay open if we're running as Matrix widget
  306. window.matrixWidget.setAlwaysOnScreen(true)
  307. // Register all channels, recursively
  308. const registerChannel = channel => {
  309. this._newChannel(channel)
  310. channel.children.forEach(registerChannel)
  311. }
  312. registerChannel(client.root)
  313. // Register all users
  314. client.users.forEach(user => this._newUser(user))
  315. // Register future channels
  316. client.on('newChannel', channel => this._newChannel(channel))
  317. // Register future users
  318. client.on('newUser', user => this._newUser(user))
  319. // Handle messages
  320. client.on('message', (sender, message, users, channels, trees) => {
  321. sender = sender || { __ui: 'Server' }
  322. ui.log.push({
  323. type: 'chat-message',
  324. user: sender.__ui,
  325. channel: channels.length > 0,
  326. message: sanitize(message)
  327. })
  328. })
  329. // Set own user and root channel
  330. this.thisUser(client.self.__ui)
  331. this.root(client.root.__ui)
  332. // Upate linked channels
  333. this._updateLinks()
  334. // Log welcome message
  335. if (client.welcomeMessage) {
  336. this.log.push({
  337. type: 'welcome-message',
  338. message: sanitize(client.welcomeMessage)
  339. })
  340. }
  341. // Startup audio input processing
  342. this._updateVoiceHandler()
  343. // Tell server our mute/deaf state (if necessary)
  344. if (this.selfDeaf()) {
  345. this.client.setSelfDeaf(true)
  346. } else if (this.selfMute()) {
  347. this.client.setSelfMute(true)
  348. }
  349. }, err => {
  350. if (err.$type && err.$type.name === 'Reject') {
  351. this.connectErrorDialog.type(err.type)
  352. this.connectErrorDialog.reason(err.reason)
  353. this.connectErrorDialog.show()
  354. } else {
  355. log('Connection error:', err)
  356. }
  357. })
  358. }
  359. this._newUser = user => {
  360. const simpleProperties = {
  361. uniqueId: 'uid',
  362. username: 'name',
  363. mute: 'mute',
  364. deaf: 'deaf',
  365. suppress: 'suppress',
  366. selfMute: 'selfMute',
  367. selfDeaf: 'selfDeaf',
  368. texture: 'rawTexture',
  369. textureHash: 'textureHash',
  370. comment: 'comment'
  371. }
  372. var ui = user.__ui = {
  373. model: user,
  374. talking: ko.observable('off'),
  375. channel: ko.observable()
  376. }
  377. ui.texture = ko.pureComputed(() => {
  378. let raw = ui.rawTexture()
  379. if (!raw || raw.offset >= raw.limit) return null
  380. return 'data:image/*;base64,' + ByteBuffer.wrap(raw).toBase64()
  381. })
  382. ui.show_avatar = () => {
  383. let setting = this.settings.showAvatars()
  384. switch (setting) {
  385. case 'always':
  386. break
  387. case 'own_channel':
  388. if (this.thisUser().channel() !== ui.channel()) return false
  389. break
  390. case 'linked_channel':
  391. if (!ui.channel().linked()) return false
  392. break
  393. case 'minimal_only':
  394. if (!this.minimalView()) return false
  395. if (this.thisUser().channel() !== ui.channel()) return false
  396. break
  397. case 'never':
  398. default: return false
  399. }
  400. if (!ui.texture()) {
  401. if (ui.textureHash()) {
  402. // The user has an avatar set but it's of sufficient size to not be
  403. // included by default, so we need to fetch it explicitly now.
  404. // mumble-client should make sure we only send one request per hash
  405. user.requestTexture()
  406. }
  407. return false
  408. }
  409. return true
  410. }
  411. ui.openContextMenu = (_, event) => openContextMenu(event, this.userContextMenu, ui)
  412. ui.canChangeMute = () => {
  413. return false // TODO check for perms and implement
  414. }
  415. ui.canChangeDeafen = () => {
  416. return false // TODO check for perms and implement
  417. }
  418. ui.canChangePrioritySpeaker = () => {
  419. return false // TODO check for perms and implement
  420. }
  421. ui.canLocalMute = () => {
  422. return false // TODO implement local mute
  423. // return this.thisUser() !== ui
  424. }
  425. ui.canIgnoreMessages = () => {
  426. return false // TODO implement ignore messages
  427. // return this.thisUser() !== ui
  428. }
  429. ui.canChangeComment = () => {
  430. return false // TODO implement changing of comments
  431. // return this.thisUser() === ui // TODO check for perms
  432. }
  433. ui.canChangeAvatar = () => {
  434. return this.thisUser() === ui // TODO check for perms
  435. }
  436. ui.toggleMute = () => {
  437. if (ui.selfMute()) {
  438. this.requestUnmute(ui)
  439. } else {
  440. this.requestMute(ui)
  441. }
  442. }
  443. ui.toggleDeaf = () => {
  444. if (ui.selfDeaf()) {
  445. this.requestUndeaf(ui)
  446. } else {
  447. this.requestDeaf(ui)
  448. }
  449. }
  450. ui.viewAvatar = () => {
  451. this.avatarView(ui.texture())
  452. }
  453. ui.changeAvatar = () => {
  454. let input = document.createElement('input')
  455. input.type = 'file'
  456. input.addEventListener('change', () => {
  457. let reader = new window.FileReader()
  458. reader.onload = () => {
  459. this.client.setSelfTexture(reader.result)
  460. }
  461. reader.readAsArrayBuffer(input.files[0])
  462. })
  463. input.click()
  464. }
  465. ui.removeAvatar = () => {
  466. user.clearTexture()
  467. }
  468. Object.entries(simpleProperties).forEach(key => {
  469. ui[key[1]] = ko.observable(user[key[0]])
  470. })
  471. ui.state = ko.pureComputed(userToState, ui)
  472. if (user.channel) {
  473. ui.channel(user.channel.__ui)
  474. ui.channel().users.push(ui)
  475. ui.channel().users.sort(compareUsers)
  476. }
  477. user.on('update', (actor, properties) => {
  478. Object.entries(simpleProperties).forEach(key => {
  479. if (properties[key[0]] !== undefined) {
  480. ui[key[1]](properties[key[0]])
  481. }
  482. })
  483. if (properties.channel !== undefined) {
  484. if (ui.channel()) {
  485. ui.channel().users.remove(ui)
  486. }
  487. ui.channel(properties.channel.__ui)
  488. ui.channel().users.push(ui)
  489. ui.channel().users.sort(compareUsers)
  490. this._updateLinks()
  491. }
  492. if (properties.textureHash !== undefined) {
  493. // Invalidate avatar texture when its hash has changed
  494. // If the avatar is still visible, this will trigger a fetch of the new one.
  495. ui.rawTexture(null)
  496. }
  497. }).on('remove', () => {
  498. if (ui.channel()) {
  499. ui.channel().users.remove(ui)
  500. }
  501. }).on('voice', stream => {
  502. console.log(`User ${user.username} started takling`)
  503. var userNode = new BufferQueueNode({
  504. audioContext: audioContext
  505. })
  506. userNode.connect(audioContext.destination)
  507. stream.on('data', data => {
  508. if (data.target === 'normal') {
  509. ui.talking('on')
  510. } else if (data.target === 'shout') {
  511. ui.talking('shout')
  512. } else if (data.target === 'whisper') {
  513. ui.talking('whisper')
  514. }
  515. userNode.write(data.buffer)
  516. }).on('end', () => {
  517. console.log(`User ${user.username} stopped takling`)
  518. ui.talking('off')
  519. userNode.end()
  520. })
  521. })
  522. }
  523. this._newChannel = channel => {
  524. const simpleProperties = {
  525. position: 'position',
  526. name: 'name',
  527. description: 'description'
  528. }
  529. var ui = channel.__ui = {
  530. model: channel,
  531. expanded: ko.observable(true),
  532. parent: ko.observable(),
  533. channels: ko.observableArray(),
  534. users: ko.observableArray(),
  535. linked: ko.observable(false)
  536. }
  537. ui.openContextMenu = (_, event) => openContextMenu(event, this.channelContextMenu, ui)
  538. ui.canJoin = () => {
  539. return true // TODO check for perms
  540. }
  541. ui.canAdd = () => {
  542. return false // TODO check for perms and implement
  543. }
  544. ui.canEdit = () => {
  545. return false // TODO check for perms and implement
  546. }
  547. ui.canRemove = () => {
  548. return false // TODO check for perms and implement
  549. }
  550. ui.canLink = () => {
  551. return false // TODO check for perms and implement
  552. }
  553. ui.canUnlink = () => {
  554. return false // TODO check for perms and implement
  555. }
  556. ui.canSendMessage = () => {
  557. return false // TODO check for perms and implement
  558. }
  559. Object.entries(simpleProperties).forEach(key => {
  560. ui[key[1]] = ko.observable(channel[key[0]])
  561. })
  562. if (channel.parent) {
  563. ui.parent(channel.parent.__ui)
  564. ui.parent().channels.push(ui)
  565. ui.parent().channels.sort(compareChannels)
  566. }
  567. this._updateLinks()
  568. channel.on('update', properties => {
  569. Object.entries(simpleProperties).forEach(key => {
  570. if (properties[key[0]] !== undefined) {
  571. ui[key[1]](properties[key[0]])
  572. }
  573. })
  574. if (properties.parent !== undefined) {
  575. if (ui.parent()) {
  576. ui.parent().channel.remove(ui)
  577. }
  578. ui.parent(properties.parent.__ui)
  579. ui.parent().channels.push(ui)
  580. ui.parent().channels.sort(compareChannels)
  581. }
  582. if (properties.links !== undefined) {
  583. this._updateLinks()
  584. }
  585. }).on('remove', () => {
  586. if (ui.parent()) {
  587. ui.parent().channels.remove(ui)
  588. }
  589. this._updateLinks()
  590. })
  591. }
  592. this.resetClient = () => {
  593. if (this.client) {
  594. this.client.disconnect()
  595. }
  596. this.client = null
  597. this.selected(null).root(null).thisUser(null)
  598. }
  599. this.connected = () => this.thisUser() != null
  600. this._updateVoiceHandler = () => {
  601. if (!this.client) {
  602. return
  603. }
  604. if (voiceHandler) {
  605. voiceHandler.end()
  606. voiceHandler = null
  607. }
  608. let mode = this.settings.voiceMode
  609. if (mode === 'cont') {
  610. voiceHandler = new ContinuousVoiceHandler(this.client, this.settings)
  611. } else if (mode === 'ptt') {
  612. voiceHandler = new PushToTalkVoiceHandler(this.client, this.settings)
  613. } else if (mode === 'vad') {
  614. voiceHandler = new VADVoiceHandler(this.client, this.settings)
  615. } else {
  616. log('Unknown voice mode:', mode)
  617. return
  618. }
  619. voiceHandler.on('started_talking', () => {
  620. if (this.thisUser()) {
  621. this.thisUser().talking('on')
  622. }
  623. })
  624. voiceHandler.on('stopped_talking', () => {
  625. if (this.thisUser()) {
  626. this.thisUser().talking('off')
  627. }
  628. })
  629. if (this.selfMute()) {
  630. voiceHandler.setMute(true)
  631. }
  632. this.client.setAudioQuality(
  633. this.settings.audioBitrate,
  634. this.settings.samplesPerPacket
  635. )
  636. }
  637. this.messageBoxHint = ko.pureComputed(() => {
  638. if (!this.thisUser()) {
  639. return '' // Not yet connected
  640. }
  641. var target = this.selected()
  642. if (!target) {
  643. target = this.thisUser()
  644. }
  645. if (target === this.thisUser()) {
  646. target = target.channel()
  647. }
  648. if (target.users) { // Channel
  649. return "Type message to channel '" + target.name() + "' here"
  650. } else { // User
  651. return "Type message to user '" + target.name() + "' here"
  652. }
  653. })
  654. this.submitMessageBox = () => {
  655. this.sendMessage(this.selected(), this.messageBox())
  656. this.messageBox('')
  657. }
  658. this.sendMessage = (target, message) => {
  659. if (this.connected()) {
  660. // If no target is selected, choose our own user
  661. if (!target) {
  662. target = this.thisUser()
  663. }
  664. // If target is our own user, send to our channel
  665. if (target === this.thisUser()) {
  666. target = target.channel()
  667. }
  668. // Send message
  669. target.model.sendMessage(message)
  670. if (target.users) { // Channel
  671. this.log.push({
  672. type: 'chat-message-self',
  673. message: sanitize(message),
  674. channel: target
  675. })
  676. } else { // User
  677. this.log.push({
  678. type: 'chat-message-self',
  679. message: sanitize(message),
  680. user: target
  681. })
  682. }
  683. }
  684. }
  685. this.requestMove = (user, channel) => {
  686. if (this.connected()) {
  687. user.model.setChannel(channel.model)
  688. }
  689. }
  690. this.requestMute = user => {
  691. if (user === this.thisUser()) {
  692. this.selfMute(true)
  693. }
  694. if (this.connected()) {
  695. if (user === this.thisUser()) {
  696. this.client.setSelfMute(true)
  697. } else {
  698. user.model.setMute(true)
  699. }
  700. }
  701. }
  702. this.requestDeaf = user => {
  703. if (user === this.thisUser()) {
  704. this.selfMute(true)
  705. this.selfDeaf(true)
  706. }
  707. if (this.connected()) {
  708. if (user === this.thisUser()) {
  709. this.client.setSelfDeaf(true)
  710. } else {
  711. user.model.setDeaf(true)
  712. }
  713. }
  714. }
  715. this.requestUnmute = user => {
  716. if (user === this.thisUser()) {
  717. this.selfMute(false)
  718. this.selfDeaf(false)
  719. }
  720. if (this.connected()) {
  721. if (user === this.thisUser()) {
  722. this.client.setSelfMute(false)
  723. } else {
  724. user.model.setMute(false)
  725. }
  726. }
  727. }
  728. this.requestUndeaf = user => {
  729. if (user === this.thisUser()) {
  730. this.selfDeaf(false)
  731. }
  732. if (this.connected()) {
  733. if (user === this.thisUser()) {
  734. this.client.setSelfDeaf(false)
  735. } else {
  736. user.model.setDeaf(false)
  737. }
  738. }
  739. }
  740. this._updateLinks = () => {
  741. if (!this.thisUser()) {
  742. return
  743. }
  744. var allChannels = getAllChannels(this.root(), [])
  745. var ownChannel = this.thisUser().channel().model
  746. var allLinked = findLinks(ownChannel, [])
  747. allChannels.forEach(channel => {
  748. channel.linked(allLinked.indexOf(channel.model) !== -1)
  749. })
  750. function findLinks (channel, knownLinks) {
  751. knownLinks.push(channel)
  752. channel.links.forEach(next => {
  753. if (next && knownLinks.indexOf(next) === -1) {
  754. findLinks(next, knownLinks)
  755. }
  756. })
  757. allChannels.map(c => c.model).forEach(next => {
  758. if (next && knownLinks.indexOf(next) === -1 && next.links.indexOf(channel) !== -1) {
  759. findLinks(next, knownLinks)
  760. }
  761. })
  762. return knownLinks
  763. }
  764. function getAllChannels (channel, channels) {
  765. channels.push(channel)
  766. channel.channels().forEach(next => getAllChannels(next, channels))
  767. return channels
  768. }
  769. }
  770. this.openSourceCode = () => {
  771. var homepage = require('../package.json').homepage
  772. window.open(homepage, '_blank').focus()
  773. }
  774. this.updateSize = () => {
  775. this.minimalView(window.innerWidth < 320)
  776. if (this.minimalView()) {
  777. this.toolbarHorizontal(window.innerWidth < window.innerHeight)
  778. } else {
  779. this.toolbarHorizontal(!this.settings.toolbarVertical)
  780. }
  781. }
  782. }
  783. }
  784. var ui = new GlobalBindings()
  785. // Used only for debugging
  786. window.mumbleUi = ui
  787. window.onload = function () {
  788. var queryParams = url.parse(document.location.href, true).query
  789. var useJoinDialog = queryParams.joinDialog
  790. if (queryParams.matrix) {
  791. useJoinDialog = true
  792. }
  793. if (queryParams.address) {
  794. ui.connectDialog.address(queryParams.address)
  795. } else {
  796. useJoinDialog = false
  797. }
  798. if (queryParams.port) {
  799. ui.connectDialog.port(queryParams.port)
  800. } else {
  801. useJoinDialog = false
  802. }
  803. if (queryParams.token) {
  804. ui.connectDialog.token(queryParams.token)
  805. }
  806. if (queryParams.username) {
  807. ui.connectDialog.username(queryParams.username)
  808. } else {
  809. useJoinDialog = false
  810. }
  811. if (queryParams.password) {
  812. ui.connectDialog.password(queryParams.password)
  813. }
  814. if (queryParams.avatarurl) {
  815. // Download the avatar and upload it to the mumble server when connected
  816. let url = queryParams.avatarurl
  817. console.log('Fetching avatar from', url)
  818. let req = new window.XMLHttpRequest()
  819. req.open('GET', url, true)
  820. req.responseType = 'arraybuffer'
  821. req.onload = () => {
  822. let upload = (avatar) => {
  823. if (req.response) {
  824. console.log('Uploading user avatar to server')
  825. ui.client.setSelfTexture(req.response)
  826. }
  827. }
  828. // On any future connections
  829. ui.thisUser.subscribe((thisUser) => {
  830. if (thisUser) {
  831. upload()
  832. }
  833. })
  834. // And the current one (if already connected)
  835. if (ui.thisUser()) {
  836. upload()
  837. }
  838. }
  839. req.send()
  840. }
  841. ui.connectDialog.joinOnly(useJoinDialog)
  842. ko.applyBindings(ui)
  843. }
  844. window.onresize = () => ui.updateSize()
  845. ui.updateSize()
  846. function log () {
  847. console.log.apply(console, arguments)
  848. var args = []
  849. for (var i = 0; i < arguments.length; i++) {
  850. args.push(arguments[i])
  851. }
  852. ui.log.push({
  853. type: 'generic',
  854. value: args.join(' ')
  855. })
  856. }
  857. function compareChannels (c1, c2) {
  858. if (c1.position() === c2.position()) {
  859. return c1.name() === c2.name() ? 0 : c1.name() < c2.name() ? -1 : 1
  860. }
  861. return c1.position() - c2.position()
  862. }
  863. function compareUsers (u1, u2) {
  864. return u1.name() === u2.name() ? 0 : u1.name() < u2.name() ? -1 : 1
  865. }
  866. function userToState () {
  867. var flags = []
  868. // TODO: Friend
  869. if (this.uid()) {
  870. flags.push('Authenticated')
  871. }
  872. // TODO: Priority Speaker, Recording
  873. if (this.mute()) {
  874. flags.push('Muted (server)')
  875. }
  876. if (this.deaf()) {
  877. flags.push('Deafened (server)')
  878. }
  879. // TODO: Local Ignore (Text messages), Local Mute
  880. if (this.selfMute()) {
  881. flags.push('Muted (self)')
  882. }
  883. if (this.selfDeaf()) {
  884. flags.push('Deafened (self)')
  885. }
  886. return flags.join(', ')
  887. }
  888. var voiceHandler
  889. var testVoiceHandler
  890. initVoice(data => {
  891. if (testVoiceHandler) {
  892. testVoiceHandler.write(data)
  893. }
  894. if (!ui.client) {
  895. if (voiceHandler) {
  896. voiceHandler.end()
  897. }
  898. voiceHandler = null
  899. } else if (voiceHandler) {
  900. voiceHandler.write(data)
  901. }
  902. }, err => {
  903. log('Cannot initialize user media. Microphone will not work:', err)
  904. })