Browse Source

Re-add UDPTunnel fallback to WebRTC version

Jonas Herzig 3 years ago
parent
commit
506a799592
9 changed files with 138 additions and 101 deletions
  1. 24 39
      README.md
  2. 1 0
      app/config.js
  3. 84 32
      app/index.js
  4. 15 14
      app/voice.js
  5. 3 2
      app/worker-client.js
  6. 1 1
      app/worker.js
  7. 1 0
      loc/en.json
  8. 7 11
      package-lock.json
  9. 2 2
      package.json

+ 24 - 39
README.md

@@ -1,20 +1,18 @@
-**If you do not have specific requirements, please consider using the `webrtc` version instead: https://github.com/Johni0702/mumble-web/tree/webrtc (note that setup instructions differ significantly).
-It should be near identical in features but less susceptible to performance issues. If you are having trouble with the `webrtc` version, please let us know.**
-
-PRs, unless webrtc-specific, should still target `master`.
-
 # mumble-web
 
 mumble-web is an HTML5 [Mumble] client for use in modern browsers.
 
-A live demo is running [here](https://voice.johni0702.de/?address=voice.johni0702.de&port=443/demo).
+A live demo is running [here](https://voice.johni0702.de/?address=voice.johni0702.de&port=443/demo) (or [without WebRTC](https://voice.johni0702.de/?address=voice.johni0702.de&port=443/demo&webrtc=false)).
 
 The Mumble protocol uses TCP for control and UDP for voice.
 Running in a browser, both are unavailable to this client.
-Instead Websockets are used for all communications.
+Instead Websockets are used for control and WebRTC is used for voice (using Websockets as fallback if the server does not support WebRTC).
 
-libopus, libcelt (0.7.1) and libsamplerate, compiled to JS via emscripten, are used for audio decoding.
-Therefore, at the moment only the Opus and CELT Alpha codecs are supported.
+In WebRTC mode (default) only the Opus codec is supported.
+
+In fallback mode, when WebRTC is not supported by the server, only the Opus and CELT Alpha codecs are supported.
+This is accomplished with libopus, libcelt (0.7.1) and libsamplerate, compiled to JS via emscripten.
+Performance is expected to be less reliable (especially on low-end devices) than in WebRTC mode and loading time will be significantly increased.
 
 Quite a few features, most noticeably all
 administrative functionallity, are still missing.
@@ -23,7 +21,7 @@ administrative functionallity, are still missing.
 
 #### Download
 mumble-web can either be installed directly from npm with `npm install -g mumble-web`
-or from git:
+or from git (recommended because the npm version may be out of date):
 
 ```
 git clone https://github.com/johni0702/mumble-web
@@ -38,34 +36,14 @@ to e.g. customize the theme before building it.
 Either way you will end up with a `dist` folder that contains the static page.
 
 #### Setup
-At the time of writing this there seems to be only one Mumble server (which is [grumble](https://github.com/mumble-voip/grumble))
-that natively support Websockets. To use this client with any other standard mumble
-server, websockify must be set up (preferably on the same machine that the
-Mumble server is running on).
-
-You can install websockify via your package manager `apt install websockify` or
-manually from the [websockify GitHub page]. Note that while some versions might
-function better than others, the python version generally seems to be the best.
-
-There are two basic ways you can use websockify with mumble-web:
-- Standalone, use websockify for both, websockets and serving static files
-- Proxied, let your favorite web server serve static files and proxy websocket connections to websockify
-
-##### Standalone
-This is the simplest but at the same time least flexible configuration. Replace `<mumbleserver>` with the URI of your mumble server. If `websockify` is running on the same machine as `mumble-server`, use `localhost`.
-```
-websockify --cert=mycert.crt --key=mykey.key --ssl-only --ssl-target --web=path/to/dist 443 <mumbleserver>:64738
-```
-
-##### Proxied
-This configuration allows you to run websockify on a machine that already has
-another webserver running. Replace `<mumbleserver>` with the URI of your mumble server. If `websockify` is running on the same machine as `mumble-server`, use `localhost`.
+At the time of writing this there do not seem to be any Mumble servers which natively support Websockets+WebRTC.
+[Grumble](https://github.com/mumble-voip/grumble) natively supports Websockets and can run mumble-web in fallback mode but not (on its own) in WebRTC mode.
+To use this client with any standard mumble server in WebRTC mode, [mumble-web-proxy] must be set up (preferably on the same machine that the Mumble server is running on).
 
-```
-websockify --ssl-target 64737 <mumbleserver>:64738
-```
+Additionally you will need some web server to serve static files and terminate the secure websocket connection (mumble-web-proxy only supports insecure ones).
 
-Here are two web server configuration files (one for [NGINX](https://www.nginx.com/) and one for [Caddy server](https://caddyserver.com/)) which will serve the mumble-web interface at `https://voice.example.com` and allow the websocket to connect at `wss://voice.example.com/demo` (similar to the demo server). Replace `<websockify>` with the URI to the machine where `websockify` is running. If `websockify` is running on the same machine as your web server, use `localhost`.
+Here are two web server configuration files (one for [NGINX](https://www.nginx.com/) and one for [Caddy server](https://caddyserver.com/)) which will serve the mumble-web interface at `https://voice.example.com` and allow the websocket to connect at `wss://voice.example.com/demo` (similar to the demo server).
+Replace `<proxybox>` with the host name of the machine where `mumble-web-proxy` is running. If `mumble-web-proxy` is running on the same machine as your web server, use `localhost`.
 
 * NGINX configuration file
 ```Nginx
@@ -79,7 +57,7 @@ server {
                 root /path/to/dist;
         }
         location /demo {
-                proxy_pass http://<websockify>:64737;
+                proxy_pass http://<proxybox>:64737;
                 proxy_http_version 1.1;
                 proxy_set_header Upgrade $http_upgrade;
                 proxy_set_header Connection $connection_upgrade;
@@ -101,12 +79,19 @@ http://voice.example.com {
 https://voice.example.com {
   tls "/etc/letsencrypt/live/voice.example.com/fullchain.pem" "/etc/letsencrypt/live/voice.example.com/privkey.pem"
   root /path/to/dist
-  proxy /demo http://<websockify>:64737 {
+  proxy /demo http://<proxybox>:64737 {
     websocket
   }
 }
 ```
 
+To run `mumble-web-proxy`, execute the following command. Replace `<mumbleserver>` with the host name of your Mumble server (the one you connect to using the normal Mumble client).
+Note that even if your Mumble server is running on the same machine as your `mumble-web-proxy`, you should use the external name because (by default, for disabling see its README) `mumble-web-proxy` will try to verify the certificate provided by the Mumble server and fail if it does not match the given host name.
+```
+mumble-web-proxy --listen-ws 64737 --server <mumbleserver>:64738
+```
+If your mumble-web-proxy is running behind a NAT or firewall, take note of the respective section in its README.
+
 Make sure that your Mumble server is running. You may now open `https://voice.example.com` in a web browser. You will be prompted for server details: choose either `address: voice.example.com/demo` with `port: 443` or `address: voice.example.com` with `port: 443/demo`. You may prefill these values by appending `?address=voice.example.com/demo&port=443`. Choose a username, and click `Connect`: you should now be able to talk and use the chat.
 
 Here is an example of systemd service, put it in `/etc/systemd/system/mumble-web.service` and adapt it to your needs:
@@ -180,6 +165,6 @@ See [here](https://docs.google.com/document/d/1uPF7XWY_dXTKVKV7jZQ2KmsI19wn9-kFR
 ISC
 
 [Mumble]: https://wiki.mumble.info/wiki/Main_Page
-[websockify GitHub page]: https://github.com/novnc/websockify
+[mumble-web-proxy]: https://github.com/johni0702/mumble-web-proxy
 [MetroMumble]: https://github.com/xPoke/MetroMumble
 [Matrix]: https://matrix.org

+ 1 - 0
app/config.js

@@ -32,6 +32,7 @@ window.mumbleWebConfig = {
     'token': '',
     'username': '',
     'password': '',
+    'webrtc': 'auto', // whether to enable (true), disable (false) or auto-detect ('auto') WebRTC support
     'joinDialog': false, // replace whole dialog with single "Join Conference" button
     'matrix': false, // enable Matrix Widget support (mostly auto-detected; implies 'joinDialog')
     'avatarurl': '', // download and set the user's Mumble avatar to the image at this URL

+ 84 - 32
app/index.js

@@ -5,6 +5,7 @@ import ByteBuffer from 'bytebuffer'
 import MumbleClient from 'mumble-client'
 import WorkerBasedMumbleConnector from './worker-client'
 import BufferQueueNode from 'web-audio-buffer-queue'
+import mumbleConnect from 'mumble-client-websocket'
 import audioContext from 'audio-context'
 import ko from 'knockout'
 import _dompurify from 'dompurify'
@@ -118,6 +119,9 @@ function ConnectDialog () {
   self.hide = self.visible.bind(self.visible, false)
   self.connect = function () {
     self.hide()
+    if (ui.detectWebRTC) {
+      ui.webrtc = true
+    }
     ui.connect(self.username(), self.address(), self.port(), self.tokens(), self.password(), self.channelName())
   }
 
@@ -336,7 +340,10 @@ class GlobalBindings {
   constructor (config) {
     this.config = config
     this.settings = new Settings(config.settings)
-    this.connector = new WorkerBasedMumbleConnector()
+    this.detectWebRTC = true
+    this.webrtc = true
+    this.fallbackConnector = new WorkerBasedMumbleConnector()
+    this.webrtcConnector = { connect: mumbleConnect }
     this.client = null
     this.userContextMenu = new ContextMenu()
     this.channelContextMenu = new ContextMenu()
@@ -449,12 +456,27 @@ class GlobalBindings {
 
       // Note: This call needs to be delayed until the user has interacted with
       // the page in some way (which at this point they have), see: https://goo.gl/7K7WLu
-      this.connector.setSampleRate(audioContext().sampleRate)
+      let ctx = audioContext()
+      this.fallbackConnector.setSampleRate(ctx.sampleRate)
+      if (!this._delayedMicNode) {
+        this._micNode = ctx.createMediaStreamSource(this._micStream)
+        this._delayNode = ctx.createDelay()
+        this._delayNode.delayTime.value = 0.15
+        this._delayedMicNode = ctx.createMediaStreamDestination()
+      }
 
       // TODO: token
-      this.connector.connect(`wss://${host}:${port}`, {
+      (this.webrtc ? this.webrtcConnector : this.fallbackConnector).connect(`wss://${host}:${port}`, {
         username: username,
         password: password,
+        webrtc: this.webrtc ? {
+          enabled: true,
+          required: true,
+          mic: this._delayedMicNode.stream,
+          audioContext: ctx
+        } : {
+          enabled: false,
+        },
         tokens: tokens
       }).done(client => {
         log(translate('logentry.connected'))
@@ -535,6 +557,10 @@ class GlobalBindings {
           this.connectErrorDialog.type(err.type)
           this.connectErrorDialog.reason(err.reason)
           this.connectErrorDialog.show()
+        } else if (err === 'server_does_not_support_webrtc' && this.detectWebRTC && this.webrtc) {
+          log(translate('logentry.connection_fallback_mode'))
+          this.webrtc = false
+          this.connect(username, host, port, tokens, password, channelName)
         } else {
           log(translate('logentry.connection_error'), err)
         }
@@ -686,24 +712,32 @@ class GlobalBindings {
         }
       }).on('voice', stream => {
         console.log(`User ${user.username} started takling`)
-        var userNode = new BufferQueueNode({
-          audioContext: audioContext()
-        })
-        userNode.connect(audioContext().destination)
-
+        let userNode
+        if (!this.webrtc) {
+          userNode = new BufferQueueNode({
+            audioContext: audioContext()
+          })
+          userNode.connect(audioContext().destination)
+        }
+        if (stream.target === 'normal') {
+          ui.talking('on')
+        } else if (stream.target === 'shout') {
+          ui.talking('shout')
+        } else if (stream.target === 'whisper') {
+          ui.talking('whisper')
+        }
         stream.on('data', data => {
-          if (data.target === 'normal') {
-            ui.talking('on')
-          } else if (data.target === 'shout') {
-            ui.talking('shout')
-          } else if (data.target === 'whisper') {
-            ui.talking('whisper')
+          if (this.webrtc) {
+            // mumble-client is in WebRTC mode, no pcm data should arrive this way
+          } else {
+            userNode.write(data.buffer)
           }
-          userNode.write(data.buffer)
         }).on('end', () => {
           console.log(`User ${user.username} stopped takling`)
           ui.talking('off')
-          userNode.end()
+          if (!this.webrtc) {
+            userNode.end()
+          }
         })
       })
     }
@@ -825,6 +859,15 @@ class GlobalBindings {
         voiceHandler.setMute(true)
       }
 
+      this._micNode.disconnect()
+      this._delayNode.disconnect()
+      if (mode === 'vad') {
+        this._micNode.connect(this._delayNode)
+        this._delayNode.connect(this._delayedMicNode)
+      } else {
+        this._micNode.connect(this._delayedMicNode)
+      }
+
       this.client.setAudioQuality(
         this.settings.audioBitrate,
         this.settings.samplesPerPacket
@@ -1055,6 +1098,12 @@ function initializeUI () {
   if (queryParams.password) {
     ui.connectDialog.password(queryParams.password)
   }
+  if (queryParams.webrtc !== 'auto') {
+    ui.detectWebRTC = false
+    if (queryParams.webrtc == 'false') {
+      ui.webrtc = false
+    }
+  }
   if (queryParams.channelName) {
     ui.connectDialog.channelName(queryParams.channelName)
   }
@@ -1251,23 +1300,26 @@ function translateEverything() {
 async function main() {
   await localizationInitialize(navigator.language);
   translateEverything();
-  initializeUI();
-  initVoice(data => {
-    if (testVoiceHandler) {
-      testVoiceHandler.write(data)
-    }
-    if (!ui.client) {
-      if (voiceHandler) {
-        voiceHandler.end()
+  try {
+    const userMedia = await initVoice(data => {
+      if (testVoiceHandler) {
+        testVoiceHandler.write(data)
       }
-      voiceHandler = null
-    } else if (voiceHandler) {
-      voiceHandler.write(data)
-    }
-  }, err => {
-    log(translate('logentry.mic_init_error'), err)
-  })
+      if (!ui.client) {
+        if (voiceHandler) {
+          voiceHandler.end()
+        }
+        voiceHandler = null
+      } else if (voiceHandler) {
+        voiceHandler.write(data)
+      }
+    })
+    ui._micStream = userMedia
+  } catch (err) {
+    window.alert('Failed to initialize user media\nRefresh page to retry.\n' + err)
+    return
+  }
+  initializeUI();
 }
 
 window.onload = main
-

+ 15 - 14
app/voice.js

@@ -1,10 +1,10 @@
 import { Writable } from 'stream'
 import MicrophoneStream from 'microphone-stream'
 import audioContext from 'audio-context'
-import getUserMedia from 'getusermedia'
 import keyboardjs from 'keyboardjs'
 import vad from 'voice-activity-detection'
 import DropStream from 'drop-stream'
+import { WorkerBasedMumbleClient } from './worker-client'
 
 class VoiceHandler extends Writable {
   constructor (client, settings) {
@@ -33,8 +33,12 @@ class VoiceHandler extends Writable {
         return this._outbound
       }
 
-      // Note: the samplesPerPacket argument is handled in worker.js and not passed on
-      this._outbound = this._client.createVoiceStream(this._settings.samplesPerPacket)
+      if (this._client instanceof WorkerBasedMumbleClient) {
+        // Note: the samplesPerPacket argument is handled in worker.js and not passed on
+        this._outbound = this._client.createVoiceStream(this._settings.samplesPerPacket)
+      } else {
+        this._outbound = this._client.createVoiceStream()
+      }
 
       this.emit('started_talking')
     }
@@ -160,16 +164,13 @@ export class VADVoiceHandler extends VoiceHandler {
 
 var theUserMedia = null
 
-export function initVoice (onData, onUserMediaError) {
-  getUserMedia({ audio: true }, (err, userMedia) => {
-    if (err) {
-      onUserMediaError(err)
-    } else {
-      theUserMedia = userMedia
-      var micStream = new MicrophoneStream(userMedia, { objectMode: true, bufferSize: 1024 })
-      micStream.on('data', data => {
-        onData(Buffer.from(data.getChannelData(0).buffer))
-      })
-    }
+export function initVoice (onData) {
+  return window.navigator.mediaDevices.getUserMedia({ audio: true }).then((userMedia) => {
+    theUserMedia = userMedia
+    var micStream = new MicrophoneStream(userMedia, { objectMode: true, bufferSize: 1024 })
+    micStream.on('data', data => {
+      onData(Buffer.from(data.getChannelData(0).buffer))
+    })
+    return userMedia
   })
 }

+ 3 - 2
app/worker-client.js

@@ -125,7 +125,7 @@ class WorkerBasedMumbleConnector {
   }
 }
 
-class WorkerBasedMumbleClient extends EventEmitter {
+export class WorkerBasedMumbleClient extends EventEmitter {
   constructor (connector, clientId) {
     super()
     this._connector = connector
@@ -342,11 +342,12 @@ class WorkerBasedMumbleUser extends EventEmitter {
         props
       ]
     } else if (name === 'voice') {
-      let [id] = args
+      let [id, target] = args
       let stream = new PassThrough({
         objectMode: true
       })
       this._connector._voiceStreams[id] = stream
+      stream.target = target
       args = [stream]
     } else if (name === 'remove') {
       delete this._client._users[this._id]

+ 1 - 1
app/worker.js

@@ -164,7 +164,7 @@ import 'subworkers'
         })
       })
 
-      return [voiceId]
+      return [voiceId, stream.target]
     })
     registerEventProxy(id, user, 'remove')
 

+ 1 - 0
loc/en.json

@@ -79,6 +79,7 @@
     "connecting": "Connecting to server",
     "connected": "Connected!",
     "connection_error": "Connection error:",
+    "connection_fallback_mode": "Server does not support WebRTC, re-trying in fallback mode..",
     "unknown_voice_mode": "Unknown voice mode:",
     "mic_init_error": "Cannot initialize user media. Microphone will not work:"
   },

+ 7 - 11
package-lock.json

@@ -5501,13 +5501,12 @@
       "dev": true
     },
     "mumble-client": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/mumble-client/-/mumble-client-1.3.0.tgz",
-      "integrity": "sha512-4z/Frp+XwTsE0u+7g6BUQbYumV17iEaMBCZ5Oo5lQ5Jjq3sBnZYRH9pXDX1bU4/3HFU99/AVGcScH2R67olPPQ==",
+      "version": "github:johni0702/mumble-client#f73a08bcb223c530326d44484a357380dfe3e6ee",
+      "from": "github:johni0702/mumble-client#f73a08b",
       "dev": true,
       "requires": {
         "drop-stream": "^0.1.1",
-        "mumble-streams": "0.0.4",
+        "mumble-streams": "github:johni0702/mumble-streams#47b84d1",
         "promise": "^7.1.1",
         "reduplexer": "^1.1.0",
         "remove-value": "^1.0.0",
@@ -5565,20 +5564,17 @@
       }
     },
     "mumble-client-websocket": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/mumble-client-websocket/-/mumble-client-websocket-1.0.0.tgz",
-      "integrity": "sha1-QFT8SJgnFYo6bP4iw0oYxRdnoL8=",
+      "version": "github:johni0702/mumble-client-websocket#5b0ed8dc2eaa904d21cd9d11ab7a19558f13701a",
+      "from": "github:johni0702/mumble-client-websocket#5b0ed8d",
       "dev": true,
       "requires": {
-        "mumble-client": "^1.0.0",
         "promise": "^7.1.1",
         "websocket-stream": "^3.2.1"
       }
     },
     "mumble-streams": {
-      "version": "0.0.4",
-      "resolved": "https://registry.npmjs.org/mumble-streams/-/mumble-streams-0.0.4.tgz",
-      "integrity": "sha1-p6H50Rx437bPQcT+2V4YnXhT40g=",
+      "version": "github:johni0702/mumble-streams#47b84d190ada23df1035f02735f70b6731f58fa2",
+      "from": "github:johni0702/mumble-streams#47b84d1",
       "dev": true,
       "requires": {
         "protobufjs": "^5.0.1"

+ 2 - 2
package.json

@@ -42,9 +42,9 @@
     "libsamplerate.js": "^1.0.0",
     "lodash.assign": "^4.2.0",
     "microphone-stream": "^5.1.0",
-    "mumble-client": "^1.3.0",
+    "mumble-client": "github:johni0702/mumble-client#f73a08b",
     "mumble-client-codecs-browser": "^1.2.0",
-    "mumble-client-websocket": "^1.0.0",
+    "mumble-client-websocket": "github:johni0702/mumble-client-websocket#5b0ed8d",
     "node-sass": "^4.14.1",
     "patch-package": "^6.2.1",
     "raw-loader": "^4.0.2",