index.js 29 KB

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