├── .gitignore ├── Dockerfile ├── README.md ├── app ├── config.js ├── config.local.js ├── favicon │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── manifest.json │ ├── mstile-144x144.png │ ├── mstile-150x150.png │ ├── mstile-310x150.png │ ├── mstile-310x310.png │ ├── mstile-70x70.png │ └── safari-pinned-tab.svg ├── index.html ├── index.js ├── loc.js ├── matrix.js ├── theme.js ├── voice.js ├── worker-client.js └── worker.js ├── loc ├── de.json ├── en.json ├── eo.json ├── es.json └── oc.json ├── package-lock.json ├── package.json ├── patches └── mumble-client-codecs-browser+1.2.0.patch ├── themes ├── MetroMumbleDark │ ├── loading.scss │ └── main.scss └── MetroMumbleLight │ ├── loading.scss │ ├── main.scss │ └── svg │ ├── applications-internet.svg │ ├── audio-input-microphone-muted.svg │ ├── audio-input-microphone.svg │ ├── audio-output-deafened.svg │ ├── audio-output.svg │ ├── authenticated.svg │ ├── branch_closed.svg │ ├── branch_open.svg │ ├── channel.svg │ ├── channel_active.svg │ ├── channel_linked.svg │ ├── comment.svg │ ├── comment_seen.svg │ ├── config_basic.svg │ ├── deafened_self.svg │ ├── deafened_server.svg │ ├── default_avatar.svg │ ├── filter.svg │ ├── filter_off.svg │ ├── filter_on.svg │ ├── handle_horizontal.svg │ ├── handle_vertical.svg │ ├── information_icon.svg │ ├── layout_classic.svg │ ├── layout_custom.svg │ ├── layout_hybrid.svg │ ├── layout_stacked.svg │ ├── media-record.svg │ ├── mumble.svg │ ├── muted_local.svg │ ├── muted_self.svg │ ├── muted_server.svg │ ├── muted_suppressed.svg │ ├── priority_speaker.svg │ ├── self_comment.svg │ ├── source-code.svg │ ├── talking_alt.svg │ ├── talking_off.svg │ ├── talking_on.svg │ ├── talking_whisper.svg │ └── toolbar-comment.svg └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | 3 | # Created by https://www.gitignore.io/api/node 4 | 5 | ### Node ### 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | *.pid.lock 16 | 17 | # Directory for instrumented libs generated by jscoverage/JSCover 18 | lib-cov 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage 22 | 23 | # nyc test coverage 24 | .nyc_output 25 | 26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 27 | .grunt 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules 37 | jspm_packages 38 | 39 | # Optional npm cache directory 40 | .npm 41 | 42 | # Optional eslint cache 43 | .eslintcache 44 | 45 | # Optional REPL history 46 | .node_repl_history 47 | 48 | # Output of 'npm pack' 49 | *.tgz 50 | 51 | # Yarn Integrity file 52 | .yarn-integrity 53 | 54 | 55 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:edge 2 | 3 | LABEL maintainer="Andreas Peters " 4 | 5 | COPY ./ /home/node 6 | 7 | RUN echo http://nl.alpinelinux.org/alpine/edge/testing >> /etc/apk/repositories && \ 8 | apk add --no-cache git nodejs npm tini websockify && \ 9 | adduser -D -g 1001 -u 1001 -h /home/node node && \ 10 | mkdir -p /home/node && \ 11 | mkdir -p /home/node/.npm-global && \ 12 | mkdir -p /home/node/app && \ 13 | chown -R node: /home/node 14 | 15 | USER node 16 | 17 | ENV PATH=/home/node/.npm-global/bin:$PATH 18 | ENV NPM_CONFIG_PREFIX=/home/node/.npm-global 19 | 20 | RUN cd /home/node && \ 21 | npm install && \ 22 | npm run build 23 | 24 | USER root 25 | 26 | RUN apk del gcc git 27 | 28 | USER node 29 | 30 | EXPOSE 8080 31 | ENV MUMBLE_SERVER=mumble.aventer.biz:64738 32 | 33 | ENTRYPOINT ["/sbin/tini", "--"] 34 | CMD websockify --ssl-target --web=/home/node/dist 8080 "$MUMBLE_SERVER" 35 | 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mumble-web 2 | 3 | mumble-web is an HTML5 [Mumble] client for use in modern browsers. 4 | 5 | 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)). 6 | 7 | The Mumble protocol uses TCP for control and UDP for voice. 8 | Running in a browser, both are unavailable to this client. 9 | Instead Websockets are used for control and WebRTC is used for voice (using Websockets as fallback if the server does not support WebRTC). 10 | 11 | In WebRTC mode (default) only the Opus codec is supported. 12 | 13 | In fallback mode, when WebRTC is not supported by the server, only the Opus and CELT Alpha codecs are supported. 14 | This is accomplished with libopus, libcelt (0.7.1) and libsamplerate, compiled to JS via emscripten. 15 | Performance is expected to be less reliable (especially on low-end devices) than in WebRTC mode and loading time will be significantly increased. 16 | 17 | Quite a few features, most noticeably all 18 | administrative functionallity, are still missing. 19 | 20 | ### Installing 21 | 22 | #### Download 23 | mumble-web can either be installed directly from npm with `npm install -g mumble-web` 24 | or from git (recommended because the npm version may be out of date): 25 | 26 | ``` 27 | git clone https://github.com/johni0702/mumble-web 28 | cd mumble-web 29 | npm install 30 | ``` 31 | Note that npm **must not** be ran as the root user (even in a container) because it will try to do special things which cause the build to fail, use a non-root user account instead. 32 | 33 | The npm version is prebuilt and ready to use whereas the git version allows you 34 | to e.g. customize the theme before building it. 35 | 36 | Either way you will end up with a `dist` folder that contains the static page. 37 | 38 | #### Setup 39 | At the time of writing this there do not seem to be any Mumble servers which natively support Websockets+WebRTC. 40 | [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. 41 | 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). 42 | 43 | Additionally you will need some web server to serve static files and terminate the secure websocket connection (mumble-web-proxy only supports insecure ones). 44 | 45 | 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). 46 | Replace `` 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`. 47 | 48 | * NGINX configuration file 49 | ```Nginx 50 | server { 51 | listen 443 ssl; 52 | server_name voice.example.com; 53 | ssl_certificate /etc/letsencrypt/live/voice.example.com/fullchain.pem; 54 | ssl_certificate_key /etc/letsencrypt/live/voice.example.com/privkey.pem; 55 | 56 | location / { 57 | root /path/to/dist; 58 | } 59 | location /demo { 60 | proxy_pass http://:64737; 61 | proxy_http_version 1.1; 62 | proxy_set_header Upgrade $http_upgrade; 63 | proxy_set_header Connection $connection_upgrade; 64 | } 65 | } 66 | 67 | map $http_upgrade $connection_upgrade { 68 | default upgrade; 69 | '' close; 70 | } 71 | ``` 72 | 73 | * Caddy configuration file (`Caddyfile`) 74 | ``` 75 | http://voice.example.com { 76 | redir https://voice.example.com 77 | } 78 | 79 | https://voice.example.com { 80 | tls "/etc/letsencrypt/live/voice.example.com/fullchain.pem" "/etc/letsencrypt/live/voice.example.com/privkey.pem" 81 | root /path/to/dist 82 | proxy /demo http://:64737 { 83 | websocket 84 | } 85 | } 86 | ``` 87 | 88 | To run `mumble-web-proxy`, execute the following command. Replace `` with the host name of your Mumble server (the one you connect to using the normal Mumble client). 89 | 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. 90 | ``` 91 | mumble-web-proxy --listen-ws 64737 --server :64738 92 | ``` 93 | If your mumble-web-proxy is running behind a NAT or firewall, take note of the respective section in its README. 94 | 95 | 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. 96 | 97 | Here is an example of systemd service, put it in `/etc/systemd/system/mumble-web.service` and adapt it to your needs: 98 | ``` 99 | [Unit] 100 | Description=Mumble web interface 101 | Documentation=https://github.com/johni0702/mumble-web 102 | Requires=network.target mumble-server.service 103 | After=network.target mumble-server.service 104 | 105 | [Service] 106 | Type=simple 107 | User=www-data 108 | ExecStart=/usr/bin/websockify --web=/usr/lib/node_modules/mumble-web/dist --ssl-target localhost:64737 localhost:64738 109 | 110 | [Install] 111 | WantedBy=multi-user.target 112 | ``` 113 | 114 | Then 115 | ``` 116 | systemctl daemon-reload 117 | systemctl start mumble-web 118 | systemctl enable mumble-web 119 | ``` 120 | 121 | ### Configuration 122 | The `app/config.js` file contains default values and descriptions for all configuration options. 123 | You can overwrite those by editing the `config.local.js` file within your `dist` folder. Make sure to back up and restore the file whenever you update to a new version. 124 | 125 | ### Themes 126 | The default theme of mumble-web tries to mimic the excellent [MetroMumble]Light theme. 127 | mumble-web also includes a dark version, named MetroMumbleDark, which is heavily inspired by [MetroMumble]'s dark version. 128 | 129 | To select a theme other than the default one, append a `theme=dark` query parameter (where `dark` is the name of the theme) when accessing the mumble-web page. 130 | E.g. [this](https://voice.johni0702.de/?address=voice.johni0702.de&port=443/demo&theme=dark)is the live demo linked above but using the dark theme (`dark` is an alias for `MetroMumbleDark`). 131 | 132 | Custom themes can be created by deriving them from the MetroMumbleLight/Dark themes just like the MetroMumbleDark theme is derived from the MetroMumbleLight theme. 133 | 134 | ### Matrix Widget 135 | mumble-web has specific support for running as a widget in a [Matrix] room. 136 | 137 | While just using the URL to a mumble-web instance in a Custom Widget should work for most cases, making full use of all supported features will require some additional trickery. Also note that audio may not be functioning properly on newer Chrome versions without these extra steps. 138 | 139 | This assumes you are using the Riot Web or Desktop client. Other clients will probably require different steps. 140 | 1. Type `/devtools` into the message box of the room and press Enter 141 | 2. Click on `Send Custom Event` 142 | 3. Click on `Event` in the bottom right corner (it should change to `State Event`) 143 | 4. Enter `im.vector.modular.widgets` for `Event Type` 144 | 5. Enter `mumble` for `State Key` (this value may be arbitrary but must be unique per room) 145 | 6. For `Event Content` enter (make sure to replace the example values): 146 | ``` 147 | { 148 | "waitForIframeLoad": true, 149 | "name": "Mumble", 150 | "creatorUserId": "@your_user_id:your_home_server.example", 151 | "url": "https://voice.johni0702.de/?address=voice.johni0702.de&port=443/mumble&matrix=true&username=$matrix_display_name&theme=$theme&avatarurl=$matrix_avatar_url", 152 | "data": {}, 153 | "type": "customwidget", 154 | "id": "mumble" 155 | } 156 | ``` 157 | The `$var` parts of the `url` are intentional and will be replaced by Riot whenever a widget is loaded (i.e. they will be different for every user). The `username` query parameter sets the default username to the user's Matrix display name, the `theme` parameter automatically uses the dark theme if it's used in Riot, and the `avatarurl` will automatically download the user's avatar on Matrix and upload it as the avatar in Mumble. 158 | Finally, the `matrix=true` query parameter replaces the whole `Connect to Server` dialog with a single `Join Conference` button, so make sure to remove it if you do not supply default values for all connection parameters as above. 159 | The `type` needs to be `jitsi` to allow the widget to use audio and to stay open when switching to a different room (this will hopefully change once Riot is able to ask for permission from the user by itself). 160 | The `id` should be the same as the `State Key` from step 5. 161 | See [here](https://docs.google.com/document/d/1uPF7XWY_dXTKVKV7jZQ2KmsI19wn9-kFRgQ1tFQP7wQ/edit) for more information on the values of these fields. 162 | 7. Press `Send` 163 | 164 | ### License 165 | ISC 166 | 167 | [Mumble]: https://wiki.mumble.info/wiki/Main_Page 168 | [mumble-web-proxy]: https://github.com/johni0702/mumble-web-proxy 169 | [MetroMumble]: https://github.com/xPoke/MetroMumble 170 | [Matrix]: https://matrix.org 171 | -------------------------------------------------------------------------------- /app/config.js: -------------------------------------------------------------------------------- 1 | // Note: You probably do not want to change any values in here because this 2 | // file might need to be updated with new default values for new 3 | // configuration options. Use the [config.local.js] file instead! 4 | 5 | window.mumbleWebConfig = { 6 | // Which fields to show on the Connect to Server dialog 7 | 'connectDialog': { 8 | 'address': true, 9 | 'port': true, 10 | 'token': true, 11 | 'username': true, 12 | 'password': true, 13 | 'channelName': false 14 | }, 15 | // Default values for user settings 16 | // You can see your current value by typing `localStorage.getItem('mumble.$setting')` in the web console. 17 | 'settings': { 18 | 'voiceMode': 'vad', // one of 'cont' (Continuous), 'ptt' (Push-to-Talk), 'vad' (Voice Activity Detection) 19 | 'pttKey': 'ctrl + shift', 20 | 'vadLevel': 0.3, 21 | 'toolbarVertical': false, 22 | 'showAvatars': 'always', // one of 'always', 'own_channel', 'linked_channel', 'minimal_only', 'never' 23 | 'userCountInChannelName': false, 24 | 'audioBitrate': 40000, // bits per second 25 | 'samplesPerPacket': 960 26 | }, 27 | // Default values (can be changed by passing a query parameter of the same name) 28 | 'defaults': { 29 | // Connect Dialog 30 | 'address': window.location.hostname, 31 | 'port': '443', 32 | 'token': '', 33 | 'username': '', 34 | 'password': '', 35 | 'webrtc': 'auto', // whether to enable (true), disable (false) or auto-detect ('auto') WebRTC support 36 | 'joinDialog': false, // replace whole dialog with single "Join Conference" button 37 | 'matrix': false, // enable Matrix Widget support (mostly auto-detected; implies 'joinDialog') 38 | 'avatarurl': '', // download and set the user's Mumble avatar to the image at this URL 39 | // General 40 | 'theme': 'MetroMumbleLight', 41 | 'startMute': false, 42 | 'startDeaf': false 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/config.local.js: -------------------------------------------------------------------------------- 1 | // You can overwrite the default configuration values set in [config.js] here. 2 | // There should never be any required changes to this file and you can always 3 | // simply copy it over when updating to a new version. 4 | 5 | let config = window.mumbleWebConfig // eslint-disable-line no-unused-vars 6 | 7 | // E.g. changing default address and theme: 8 | // config.defaults.address = 'voice.example.com' 9 | // config.defaults.theme = 'MetroMumbleDark' 10 | -------------------------------------------------------------------------------- /app/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Johni0702/mumble-web/4ef594c8a097d180700d22d91e9a7fea3bab08ac/app/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /app/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Johni0702/mumble-web/4ef594c8a097d180700d22d91e9a7fea3bab08ac/app/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /app/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Johni0702/mumble-web/4ef594c8a097d180700d22d91e9a7fea3bab08ac/app/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /app/favicon/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Johni0702/mumble-web/4ef594c8a097d180700d22d91e9a7fea3bab08ac/app/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /app/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Johni0702/mumble-web/4ef594c8a097d180700d22d91e9a7fea3bab08ac/app/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /app/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Johni0702/mumble-web/4ef594c8a097d180700d22d91e9a7fea3bab08ac/app/favicon/favicon.ico -------------------------------------------------------------------------------- /app/favicon/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Mumble", 3 | "icons": [ 4 | { 5 | "src": "#require('./android-chrome-192x192.png')", 6 | "sizes": "192x192", 7 | "type": "image\/png" 8 | }, 9 | { 10 | "src": "#require('./android-chrome-512x512.png')", 11 | "sizes": "512x512", 12 | "type": "image\/png" 13 | } 14 | ], 15 | "theme_color": "#ffffff", 16 | "display": "standalone" 17 | } 18 | -------------------------------------------------------------------------------- /app/favicon/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Johni0702/mumble-web/4ef594c8a097d180700d22d91e9a7fea3bab08ac/app/favicon/mstile-144x144.png -------------------------------------------------------------------------------- /app/favicon/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Johni0702/mumble-web/4ef594c8a097d180700d22d91e9a7fea3bab08ac/app/favicon/mstile-150x150.png -------------------------------------------------------------------------------- /app/favicon/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Johni0702/mumble-web/4ef594c8a097d180700d22d91e9a7fea3bab08ac/app/favicon/mstile-310x150.png -------------------------------------------------------------------------------- /app/favicon/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Johni0702/mumble-web/4ef594c8a097d180700d22d91e9a7fea3bab08ac/app/favicon/mstile-310x310.png -------------------------------------------------------------------------------- /app/favicon/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Johni0702/mumble-web/4ef594c8a097d180700d22d91e9a7fea3bab08ac/app/favicon/mstile-70x70.png -------------------------------------------------------------------------------- /app/favicon/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 24 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 |
26 |
27 | 679 | 680 | 681 | 682 | -------------------------------------------------------------------------------- /app/loc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * the default language to use 3 | * 4 | * @var {string} 5 | * @author svartoyg 6 | */ 7 | var _languageDefault = null; 8 | 9 | 10 | /** 11 | * the fallback language to use 12 | * 13 | * @var {string} 14 | * @author svartoyg 15 | */ 16 | var _languageFallback = null; 17 | 18 | 19 | /** 20 | * two level map with ISO-639-1 code as first key and translation id as second key 21 | * 22 | * @var {Map>} 23 | * @author svartoyg 24 | */ 25 | var _data = {}; 26 | 27 | 28 | /** 29 | * @param {string} language 30 | * @return Promise> 31 | * @author svartoyg 32 | */ 33 | async function retrieveData (language) { 34 | let json 35 | try { 36 | json = (await import(`../loc/${language}.json`)).default 37 | } catch (exception) { 38 | json = (await import(`../loc/${language.substr(0, language.indexOf('-'))}.json`)).default 39 | } 40 | const map = {} 41 | flatten(json, '', map) 42 | return map 43 | } 44 | 45 | function flatten (tree, prefix, result) { 46 | for (const [key, value] of Object.entries(tree)) { 47 | if (typeof value === 'string') { 48 | result[prefix + key] = value 49 | } else { 50 | flatten(value, prefix + key + '.', result) 51 | } 52 | } 53 | } 54 | 55 | 56 | /** 57 | * @param {string} languageDefault 58 | * @param {string} [languageFallback] 59 | * @author svartoyg 60 | */ 61 | export async function initialize (languageDefault, languageFallback = 'en') { 62 | _languageFallback = languageFallback; 63 | _languageDefault = languageDefault; 64 | for (const language of [_languageFallback, _languageDefault]) { 65 | if (_data.hasOwnProperty(language)) continue; 66 | console.log('--', 'loading localization data for language "' + language + '" ...'); 67 | let data; 68 | try { 69 | data = await retrieveData(language); 70 | } catch (exception) { 71 | console.warn(exception.toString()); 72 | } 73 | _data[language] = data; 74 | } 75 | } 76 | 77 | 78 | /** 79 | * gets a translation by its key for a specific language 80 | * 81 | * @param {string} key 82 | * @param {string} [languageChosen] 83 | * @return {string} 84 | * @author svartoyg 85 | */ 86 | export function translate (key, languageChosen = _languageDefault) { 87 | let result = undefined; 88 | for (const language of [languageChosen, _languageFallback]) { 89 | if (_data.hasOwnProperty(language) && (_data[language] !== undefined) && _data[language].hasOwnProperty(key)) { 90 | result = _data[language][key]; 91 | break; 92 | } 93 | } 94 | if (result === undefined) { 95 | result = ('{{' + key + '}}'); 96 | } 97 | return result; 98 | } 99 | 100 | -------------------------------------------------------------------------------- /app/matrix.js: -------------------------------------------------------------------------------- 1 | // Handle messages coming from [Matrix] client if embedded as a [Widget] in some room. 2 | // [Matrix]: https://matrix.org/ 3 | // [Widget]: https://docs.google.com/document/d/1uPF7XWY_dXTKVKV7jZQ2KmsI19wn9-kFRgQ1tFQP7wQ/edit 4 | 5 | class MatrixWidget { 6 | constructor () { 7 | this.widgetId = null 8 | window.addEventListener('message', this.onMessage.bind(this)) 9 | } 10 | 11 | onMessage (event) { 12 | this.widgetId = this.widgetId || event.data.widgetId 13 | 14 | switch (event.data.api) { 15 | case 'fromWidget': 16 | break 17 | case 'toWidget': 18 | switch (event.data.action) { 19 | case 'capabilities': 20 | this.sendResponse(event, { 21 | capabilities: ['m.always_on_screen'] 22 | }) 23 | break 24 | } 25 | break 26 | default: 27 | break 28 | } 29 | } 30 | 31 | sendContentLoaded () { 32 | this.sendMessage({ 33 | action: 'content_loaded' 34 | }) 35 | } 36 | 37 | setAlwaysOnScreen (value) { 38 | // Extension of main spec, see https://github.com/matrix-org/matrix-doc/issues/1354 39 | this.sendMessage({ 40 | action: 'set_always_on_screen', 41 | value: value, // once for spec compliance 42 | data: { value: value } // and once for Riot 43 | }) 44 | } 45 | 46 | sendMessage (message) { 47 | if (!this.widgetId) return 48 | message.api = message.api || 'fromWidget' 49 | message.widgetId = message.widgetId || this.widgetId 50 | message.requestId = message.requestId || Math.random().toString(36) 51 | window.parent.postMessage(message, '*') 52 | } 53 | 54 | sendResponse (event, response) { 55 | event.data.response = response 56 | event.source.postMessage(event.data, event.origin) 57 | } 58 | } 59 | 60 | window.matrixWidget = new MatrixWidget() 61 | -------------------------------------------------------------------------------- /app/theme.js: -------------------------------------------------------------------------------- 1 | import url from 'url' 2 | 3 | var queryParams = url.parse(document.location.href, true).query 4 | var theme = queryParams.theme || window.localStorage.getItem('mumble.theme') 5 | var themes = { 6 | 'MetroMumbleLight': 'MetroMumbleLight', 7 | 'MetroMumbleDark': 'MetroMumbleDark', 8 | 'light': 'MetroMumbleLight', 9 | 'dark': 'MetroMumbleDark' 10 | } 11 | theme = themes[theme] || window.mumbleWebConfig.defaults.theme 12 | window.theme = theme 13 | 14 | var [loadingTheme, mainTheme] = { 15 | 'MetroMumbleLight': [ 16 | require('../themes/MetroMumbleLight/loading.scss'), 17 | require('../themes/MetroMumbleLight/main.scss') 18 | ], 19 | 'MetroMumbleDark': [ 20 | require('../themes/MetroMumbleDark/loading.scss'), 21 | require('../themes/MetroMumbleDark/main.scss') 22 | ] 23 | }[theme] 24 | 25 | function useStyle (url) { 26 | var style = document.createElement('link') 27 | style.rel = 'stylesheet' 28 | style.type = 'text/css' 29 | style.href = url 30 | document.getElementsByTagName('head')[0].appendChild(style) 31 | } 32 | useStyle(loadingTheme) 33 | useStyle(mainTheme) 34 | -------------------------------------------------------------------------------- /app/voice.js: -------------------------------------------------------------------------------- 1 | import { Writable } from 'stream' 2 | import MicrophoneStream from 'microphone-stream' 3 | import audioContext from 'audio-context' 4 | import keyboardjs from 'keyboardjs' 5 | import vad from 'voice-activity-detection' 6 | import DropStream from 'drop-stream' 7 | import { WorkerBasedMumbleClient } from './worker-client' 8 | 9 | class VoiceHandler extends Writable { 10 | constructor (client, settings) { 11 | super({ objectMode: true }) 12 | this._client = client 13 | this._settings = settings 14 | this._outbound = null 15 | this._mute = false 16 | } 17 | 18 | setMute (mute) { 19 | this._mute = mute 20 | if (mute) { 21 | this._stopOutbound() 22 | } 23 | } 24 | 25 | _getOrCreateOutbound () { 26 | if (this._mute) { 27 | throw new Error('tried to send audio while self-muted') 28 | } 29 | if (!this._outbound) { 30 | if (!this._client) { 31 | this._outbound = DropStream.obj() 32 | this.emit('started_talking') 33 | return this._outbound 34 | } 35 | 36 | if (this._client instanceof WorkerBasedMumbleClient) { 37 | // Note: the samplesPerPacket argument is handled in worker.js and not passed on 38 | this._outbound = this._client.createVoiceStream(this._settings.samplesPerPacket) 39 | } else { 40 | this._outbound = this._client.createVoiceStream() 41 | } 42 | 43 | this.emit('started_talking') 44 | } 45 | return this._outbound 46 | } 47 | 48 | _stopOutbound () { 49 | if (this._outbound) { 50 | this.emit('stopped_talking') 51 | this._outbound.end() 52 | this._outbound = null 53 | } 54 | } 55 | 56 | _final (callback) { 57 | this._stopOutbound() 58 | callback() 59 | } 60 | } 61 | 62 | export class ContinuousVoiceHandler extends VoiceHandler { 63 | constructor (client, settings) { 64 | super(client, settings) 65 | } 66 | 67 | _write (data, _, callback) { 68 | if (this._mute) { 69 | callback() 70 | } else { 71 | this._getOrCreateOutbound().write(data, callback) 72 | } 73 | } 74 | } 75 | 76 | export class PushToTalkVoiceHandler extends VoiceHandler { 77 | constructor (client, settings) { 78 | super(client, settings) 79 | this._key = settings.pttKey 80 | this._pushed = false 81 | this._keydown_handler = () => this._pushed = true 82 | this._keyup_handler = () => { 83 | this._stopOutbound() 84 | this._pushed = false 85 | } 86 | keyboardjs.bind(this._key, this._keydown_handler, this._keyup_handler) 87 | } 88 | 89 | _write (data, _, callback) { 90 | if (this._pushed && !this._mute) { 91 | this._getOrCreateOutbound().write(data, callback) 92 | } else { 93 | callback() 94 | } 95 | } 96 | 97 | _final (callback) { 98 | super._final(e => { 99 | keyboardjs.unbind(this._key, this._keydown_handler, this._keyup_handler) 100 | callback(e) 101 | }) 102 | } 103 | } 104 | 105 | export class VADVoiceHandler extends VoiceHandler { 106 | constructor (client, settings) { 107 | super(client, settings) 108 | let level = settings.vadLevel 109 | const self = this 110 | this._vad = vad(audioContext(), theUserMedia, { 111 | onVoiceStart () { 112 | console.log('vad: start') 113 | self._active = true 114 | }, 115 | onVoiceStop () { 116 | console.log('vad: stop') 117 | self._stopOutbound() 118 | self._active = false 119 | }, 120 | onUpdate (val) { 121 | self._level = val 122 | self.emit('level', val) 123 | }, 124 | noiseCaptureDuration: 0, 125 | minNoiseLevel: level, 126 | maxNoiseLevel: level 127 | }) 128 | // Need to keep a backlog of the last ~150ms (dependent on sample rate) 129 | // because VAD will activate with ~125ms delay 130 | this._backlog = [] 131 | this._backlogLength = 0 132 | this._backlogLengthMin = 1024 * 6 * 4 // vadBufferLen * (vadDelay + 1) * bytesPerSample 133 | } 134 | 135 | _write (data, _, callback) { 136 | if (this._active && !this._mute) { 137 | if (this._backlog.length > 0) { 138 | for (let oldData of this._backlog) { 139 | this._getOrCreateOutbound().write(oldData) 140 | } 141 | this._backlog = [] 142 | this._backlogLength = 0 143 | } 144 | this._getOrCreateOutbound().write(data, callback) 145 | } else { 146 | // Make sure we always keep the backlog filled if we're not (yet) talking 147 | this._backlog.push(data) 148 | this._backlogLength += data.length 149 | // Check if we can discard the oldest element without becoming too short 150 | if (this._backlogLength - this._backlog[0].length > this._backlogLengthMin) { 151 | this._backlogLength -= this._backlog.shift().length 152 | } 153 | callback() 154 | } 155 | } 156 | 157 | _final (callback) { 158 | super._final(e => { 159 | this._vad.destroy() 160 | callback(e) 161 | }) 162 | } 163 | } 164 | 165 | var theUserMedia = null 166 | 167 | export function initVoice (onData) { 168 | return window.navigator.mediaDevices.getUserMedia({ audio: true }).then((userMedia) => { 169 | theUserMedia = userMedia 170 | var micStream = new MicrophoneStream(userMedia, { objectMode: true, bufferSize: 1024 }) 171 | micStream.on('data', data => { 172 | onData(Buffer.from(data.getChannelData(0).buffer)) 173 | }) 174 | return userMedia 175 | }) 176 | } 177 | -------------------------------------------------------------------------------- /app/worker-client.js: -------------------------------------------------------------------------------- 1 | import MumbleClient from 'mumble-client' 2 | import Promise from 'promise' 3 | import EventEmitter from 'events' 4 | import { Writable, PassThrough } from 'stream' 5 | import toArrayBuffer from 'to-arraybuffer' 6 | import ByteBuffer from 'bytebuffer' 7 | import Worker from './worker' 8 | 9 | /** 10 | * Creates proxy MumbleClients to a real ones running on a web worker. 11 | * Only stuff which we need in mumble-web is proxied, i.e. this is not a generic solution. 12 | */ 13 | class WorkerBasedMumbleConnector { 14 | constructor () { 15 | this._reqId = 1 16 | this._requests = {} 17 | this._clients = {} 18 | this._nextVoiceId = 1 19 | this._voiceStreams = {} 20 | } 21 | 22 | setSampleRate (sampleRate) { 23 | this._postMessage({ 24 | method: '_init', 25 | sampleRate: sampleRate 26 | }) 27 | } 28 | 29 | _postMessage (msg, transfer) { 30 | if (!this._worker) { 31 | this._worker = new Worker() 32 | this._worker.addEventListener('message', this._onMessage.bind(this)) 33 | } 34 | try { 35 | this._worker.postMessage(msg, transfer) 36 | } catch (err) { 37 | console.error('Failed to postMessage', msg) 38 | throw err 39 | } 40 | } 41 | 42 | _call (id, method, payload, transfer) { 43 | let reqId = this._reqId++ 44 | console.debug(method, id, payload) 45 | this._postMessage({ 46 | clientId: id.client, 47 | channelId: id.channel, 48 | userId: id.user, 49 | method: method, 50 | reqId: reqId, 51 | payload: payload 52 | }, transfer) 53 | return reqId 54 | } 55 | 56 | _query (id, method, payload, transfer) { 57 | let reqId = this._call(id, method, payload, transfer) 58 | return new Promise((resolve, reject) => { 59 | this._requests[reqId] = [resolve, reject] 60 | }) 61 | } 62 | 63 | _addCall (proxy, name, id) { 64 | let self = this 65 | proxy[name] = function () { 66 | self._call(id, name, Array.from(arguments)) 67 | } 68 | } 69 | 70 | connect (host, args) { 71 | return this._query({}, '_connect', { host: host, args: args }) 72 | .then(id => this._client(id)) 73 | } 74 | 75 | _client (id) { 76 | let client = this._clients[id] 77 | if (!client) { 78 | client = new WorkerBasedMumbleClient(this, id) 79 | this._clients[id] = client 80 | } 81 | return client 82 | } 83 | 84 | _onMessage (ev) { 85 | let data = ev.data 86 | if (data.reqId != null) { 87 | console.debug(data) 88 | let { reqId, result, error } = data 89 | let [ resolve, reject ] = this._requests[reqId] 90 | delete this._requests[reqId] 91 | if (result) { 92 | resolve(result) 93 | } else { 94 | reject(error) 95 | } 96 | } else if (data.clientId != null) { 97 | console.debug(data) 98 | let client = this._client(data.clientId) 99 | 100 | let target 101 | if (data.userId != null) { 102 | target = client._user(data.userId) 103 | } else if (data.channelId != null) { 104 | target = client._channel(data.channelId) 105 | } else { 106 | target = client 107 | } 108 | 109 | if (data.event) { 110 | target._dispatchEvent(data.event, data.value) 111 | } else if (data.prop) { 112 | target._setProp(data.prop, data.value) 113 | } 114 | } else if (data.voiceId != null) { 115 | let stream = this._voiceStreams[data.voiceId] 116 | let buffer = data.buffer 117 | if (buffer) { 118 | stream.write({ 119 | target: data.target, 120 | buffer: Buffer.from(buffer) 121 | }) 122 | } else { 123 | delete this._voiceStreams[data.voiceId] 124 | stream.end() 125 | } 126 | } 127 | } 128 | } 129 | 130 | export class WorkerBasedMumbleClient extends EventEmitter { 131 | constructor (connector, clientId) { 132 | super() 133 | this._connector = connector 134 | this._id = clientId 135 | this._users = {} 136 | this._channels = {} 137 | 138 | let id = { client: clientId } 139 | connector._addCall(this, 'setSelfDeaf', id) 140 | connector._addCall(this, 'setSelfMute', id) 141 | connector._addCall(this, 'setSelfTexture', id) 142 | connector._addCall(this, 'setAudioQuality', id) 143 | connector._addCall(this, '_send', id) 144 | 145 | connector._addCall(this, 'disconnect', id) 146 | let _disconnect = this.disconnect 147 | this.disconnect = () => { 148 | _disconnect.apply(this) 149 | delete connector._clients[id] 150 | } 151 | 152 | connector._addCall(this, 'createVoiceStream', id) 153 | let _createVoiceStream = this.createVoiceStream 154 | this.createVoiceStream = function () { 155 | let voiceId = connector._nextVoiceId++ 156 | 157 | let args = Array.from(arguments) 158 | args.unshift(voiceId) 159 | _createVoiceStream.apply(this, args) 160 | 161 | return new Writable({ 162 | write (chunk, encoding, callback) { 163 | chunk = toArrayBuffer(chunk) 164 | connector._postMessage({ 165 | voiceId: voiceId, 166 | chunk: chunk 167 | }) 168 | callback() 169 | }, 170 | final (callback) { 171 | connector._postMessage({ 172 | voiceId: voiceId 173 | }) 174 | callback() 175 | } 176 | }) 177 | } 178 | 179 | // Dummy client used for bandwidth calculations 180 | this._dummyClient = new MumbleClient({ username: 'dummy' }) 181 | let defineDummyMethod = (name) => { 182 | this[name] = function () { 183 | return this._dummyClient[name].apply(this._dummyClient, arguments) 184 | } 185 | } 186 | defineDummyMethod('getMaxBitrate') 187 | defineDummyMethod('getActualBitrate') 188 | let _setAudioQuality = this.setAudioQuality 189 | this.setAudioQuality = function () { 190 | this._dummyClient.setAudioQuality.apply(this._dummyClient, arguments) 191 | _setAudioQuality.apply(this, arguments) 192 | } 193 | } 194 | 195 | _user (id) { 196 | let user = this._users[id] 197 | if (!user) { 198 | user = new WorkerBasedMumbleUser(this._connector, this, id) 199 | this._users[id] = user 200 | } 201 | return user 202 | } 203 | 204 | _channel (id) { 205 | let channel = this._channels[id] 206 | if (!channel) { 207 | channel = new WorkerBasedMumbleChannel(this._connector, this, id) 208 | this._channels[id] = channel 209 | } 210 | return channel 211 | } 212 | 213 | _dispatchEvent (name, args) { 214 | if (name === 'newChannel') { 215 | args[0] = this._channel(args[0]) 216 | } else if (name === 'newUser') { 217 | args[0] = this._user(args[0]) 218 | } else if (name === 'message') { 219 | args[0] = this._user(args[0]) 220 | args[2] = args[2].map((id) => this._user(id)) 221 | args[3] = args[3].map((id) => this._channel(id)) 222 | args[4] = args[4].map((id) => this._channel(id)) 223 | } 224 | args.unshift(name) 225 | this.emit.apply(this, args) 226 | } 227 | 228 | _setProp (name, value) { 229 | if (name === 'root') { 230 | name = '_rootId' 231 | } 232 | if (name === 'self') { 233 | name = '_selfId' 234 | } 235 | if (name === 'maxBandwidth') { 236 | this._dummyClient.maxBandwidth = value 237 | } 238 | this[name] = value 239 | } 240 | 241 | get root () { 242 | return this._channel(this._rootId) 243 | } 244 | 245 | get channels () { 246 | return Object.values(this._channels) 247 | } 248 | 249 | get users () { 250 | return Object.values(this._users) 251 | } 252 | 253 | get self () { 254 | return this._user(this._selfId) 255 | } 256 | } 257 | 258 | class WorkerBasedMumbleChannel extends EventEmitter { 259 | constructor (connector, client, channelId) { 260 | super() 261 | this._connector = connector 262 | this._client = client 263 | this._id = channelId 264 | 265 | let id = { client: client._id, channel: channelId } 266 | connector._addCall(this, 'sendMessage', id) 267 | } 268 | 269 | _dispatchEvent (name, args) { 270 | if (name === 'update') { 271 | let [props] = args 272 | Object.entries(props).forEach((entry) => { 273 | this._setProp(entry[0], entry[1]) 274 | }) 275 | if (props.parent != null) { 276 | props.parent = this.parent 277 | } 278 | if (props.links != null) { 279 | props.links = this.links 280 | } 281 | args = [ 282 | props 283 | ] 284 | } else if (name === 'remove') { 285 | delete this._client._channels[this._id] 286 | } 287 | args.unshift(name) 288 | this.emit.apply(this, args) 289 | } 290 | 291 | _setProp (name, value) { 292 | if (name === 'parent') { 293 | name = '_parentId' 294 | } 295 | if (name === 'links') { 296 | value = value.map((id) => this._client._channel(id)) 297 | } 298 | this[name] = value 299 | } 300 | 301 | get parent () { 302 | if (this._parentId != null) { 303 | return this._client._channel(this._parentId) 304 | } 305 | } 306 | 307 | get children () { 308 | return Object.values(this._client._channels).filter((it) => it.parent === this) 309 | } 310 | } 311 | 312 | class WorkerBasedMumbleUser extends EventEmitter { 313 | constructor (connector, client, userId) { 314 | super() 315 | this._connector = connector 316 | this._client = client 317 | this._id = userId 318 | 319 | let id = { client: client._id, user: userId } 320 | connector._addCall(this, 'requestTexture', id) 321 | connector._addCall(this, 'clearTexture', id) 322 | connector._addCall(this, 'setMute', id) 323 | connector._addCall(this, 'setDeaf', id) 324 | connector._addCall(this, 'sendMessage', id) 325 | this.setChannel = (channel) => { 326 | connector._call(id, 'setChannel', channel._id) 327 | } 328 | } 329 | 330 | _dispatchEvent (name, args) { 331 | if (name === 'update') { 332 | let [actor, props] = args 333 | Object.entries(props).forEach((entry) => { 334 | this._setProp(entry[0], entry[1]) 335 | }) 336 | if (props.channel != null) { 337 | props.channel = this.channel 338 | } 339 | if (props.texture != null) { 340 | props.texture = this.texture 341 | } 342 | args = [ 343 | this._client._user(actor), 344 | props 345 | ] 346 | } else if (name === 'voice') { 347 | let [id, target] = args 348 | let stream = new PassThrough({ 349 | objectMode: true 350 | }) 351 | this._connector._voiceStreams[id] = stream 352 | stream.target = target 353 | args = [stream] 354 | } else if (name === 'remove') { 355 | delete this._client._users[this._id] 356 | } 357 | args.unshift(name) 358 | this.emit.apply(this, args) 359 | } 360 | 361 | _setProp (name, value) { 362 | if (name === 'channel') { 363 | name = '_channelId' 364 | } 365 | if (name === 'texture') { 366 | if (value) { 367 | let buf = ByteBuffer.wrap(value.buffer) 368 | buf.offset = value.offset 369 | buf.limit = value.limit 370 | value = buf 371 | } 372 | } 373 | this[name] = value 374 | } 375 | 376 | get channel () { 377 | return this._client._channels[this._channelId] 378 | } 379 | } 380 | export default WorkerBasedMumbleConnector 381 | -------------------------------------------------------------------------------- /app/worker.js: -------------------------------------------------------------------------------- 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 | 8 | // Polyfill nested webworkers for https://bugs.chromium.org/p/chromium/issues/detail?id=31666 9 | import 'subworkers' 10 | 11 | let sampleRate 12 | let nextClientId = 1 13 | let nextVoiceId = 1 14 | let voiceStreams = [] 15 | let clients = [] 16 | 17 | function postMessage (msg, transfer) { 18 | try { 19 | self.postMessage(msg, transfer) 20 | } catch (err) { 21 | console.error('Failed to postMessage', msg) 22 | throw err 23 | } 24 | } 25 | 26 | function resolve (reqId, value, transfer) { 27 | postMessage({ 28 | reqId: reqId, 29 | result: value 30 | }, transfer) 31 | } 32 | 33 | function reject (reqId, value, transfer) { 34 | console.error(value) 35 | let jsonValue = JSON.parse(JSON.stringify(value)) 36 | if (value.$type) { 37 | jsonValue.$type = { name: value.$type.name } 38 | } 39 | postMessage({ 40 | reqId: reqId, 41 | error: jsonValue 42 | }, transfer) 43 | } 44 | 45 | function registerEventProxy (id, obj, event, transform) { 46 | obj.on(event, function (_) { 47 | postMessage({ 48 | clientId: id.client, 49 | channelId: id.channel, 50 | userId: id.user, 51 | event: event, 52 | value: transform ? transform.apply(null, arguments) : Array.from(arguments) 53 | }) 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 | 68 | function setupOutboundVoice (voiceId, samplesPerPacket, stream) { 69 | let resampler = new Resampler({ 70 | unsafe: true, 71 | type: Resampler.Type.SINC_FASTEST, 72 | ratio: 48000 / sampleRate 73 | }) 74 | 75 | let buffer2Float32Array = new Transform({ 76 | transform (data, _, callback) { 77 | callback(null, new Float32Array(data.buffer, data.byteOffset, data.byteLength / 4)) 78 | }, 79 | readableObjectMode: true 80 | }) 81 | 82 | resampler 83 | .pipe(chunker(4 * samplesPerPacket)) 84 | .pipe(buffer2Float32Array) 85 | .pipe(stream) 86 | 87 | voiceStreams[voiceId] = resampler 88 | } 89 | 90 | function setupChannel (id, channel) { 91 | id = Object.assign({}, id, { channel: channel.id }) 92 | 93 | registerEventProxy(id, channel, 'update', (props) => { 94 | if (props.parent) { 95 | props.parent = props.parent.id 96 | } 97 | if (props.links) { 98 | props.links = props.links.map((it) => it.id) 99 | } 100 | return [props] 101 | }) 102 | registerEventProxy(id, channel, 'remove') 103 | 104 | pushProp(id, channel, 'parent', (it) => it ? it.id : it) 105 | pushProp(id, channel, 'links', (it) => it.map((it) => it.id)) 106 | let props = [ 107 | 'position', 'name', 'description' 108 | ] 109 | for (let prop of props) { 110 | pushProp(id, channel, prop) 111 | } 112 | 113 | for (let child of channel.children) { 114 | setupChannel(id, child) 115 | } 116 | 117 | return channel.id 118 | } 119 | 120 | function setupUser (id, user) { 121 | id = Object.assign({}, id, { user: user.id }) 122 | 123 | registerEventProxy(id, user, 'update', (actor, props) => { 124 | if (actor) { 125 | actor = actor.id 126 | } 127 | if (props.channel != null) { 128 | props.channel = props.channel.id 129 | } 130 | return [actor, props] 131 | }) 132 | registerEventProxy(id, user, 'voice', (stream) => { 133 | let voiceId = nextVoiceId++ 134 | 135 | let target 136 | 137 | // We want to do as little on the UI thread as possible, so do resampling here as well 138 | var resampler = new Resampler({ 139 | unsafe: true, 140 | type: Resampler.Type.ZERO_ORDER_HOLD, 141 | ratio: sampleRate / 48000 142 | }) 143 | 144 | // Pipe stream into resampler 145 | stream.on('data', (data) => { 146 | // store target so we can pass it on after resampling 147 | target = data.target 148 | resampler.write(Buffer.from(data.pcm.buffer)) 149 | }).on('end', () => { 150 | resampler.end() 151 | }) 152 | 153 | // Pipe resampler into output stream on UI thread 154 | resampler.on('data', (data) => { 155 | data = toArrayBuffer(data) // postMessage can't transfer node's Buffer 156 | postMessage({ 157 | voiceId: voiceId, 158 | target: target, 159 | buffer: data 160 | }, [data]) 161 | }).on('end', () => { 162 | postMessage({ 163 | voiceId: voiceId 164 | }) 165 | }) 166 | 167 | return [voiceId, stream.target] 168 | }) 169 | registerEventProxy(id, user, 'remove') 170 | 171 | pushProp(id, user, 'channel', (it) => it ? it.id : it) 172 | let props = [ 173 | 'uniqueId', 'username', 'mute', 'deaf', 'suppress', 'selfMute', 'selfDeaf', 174 | 'texture', 'textureHash', 'comment' 175 | ] 176 | for (let prop of props) { 177 | pushProp(id, user, prop) 178 | } 179 | 180 | return user.id 181 | } 182 | 183 | function setupClient (id, client) { 184 | id = { client: id } 185 | 186 | registerEventProxy(id, client, 'error') 187 | registerEventProxy(id, client, 'denied', it => [it]) 188 | registerEventProxy(id, client, 'newChannel', (it) => [setupChannel(id, it)]) 189 | registerEventProxy(id, client, 'newUser', (it) => [setupUser(id, it)]) 190 | registerEventProxy(id, client, 'message', (sender, message, users, channels, trees) => { 191 | return [ 192 | sender.id, 193 | message, 194 | users.map((it) => it.id), 195 | channels.map((it) => it.id), 196 | trees.map((it) => it.id) 197 | ] 198 | }) 199 | client.on('dataPing', () => { 200 | pushProp(id, client, 'dataStats') 201 | }) 202 | 203 | setupChannel(id, client.root) 204 | for (let user of client.users) { 205 | setupUser(id, user) 206 | } 207 | 208 | pushProp(id, client, 'root', (it) => it.id) 209 | pushProp(id, client, 'self', (it) => it.id) 210 | pushProp(id, client, 'welcomeMessage') 211 | pushProp(id, client, 'serverVersion') 212 | pushProp(id, client, 'maxBandwidth') 213 | } 214 | 215 | function onMessage (data) { 216 | let { reqId, method, payload } = data 217 | if (method === '_init') { 218 | sampleRate = data.sampleRate 219 | } else if (method === '_connect') { 220 | payload.args.codecs = CodecsBrowser 221 | mumbleConnect(payload.host, payload.args).then((client) => { 222 | let id = nextClientId++ 223 | clients[id] = client 224 | setupClient(id, client) 225 | return id 226 | }).done((id) => { 227 | resolve(reqId, id) 228 | }, (err) => { 229 | reject(reqId, err) 230 | }) 231 | } else if (data.clientId != null) { 232 | let client = clients[data.clientId] 233 | 234 | let target 235 | if (data.userId != null) { 236 | target = client.getUserById(data.userId) 237 | if (method === 'setChannel') { 238 | payload = [client.getChannelById(payload)] 239 | } 240 | } else if (data.channelId != null) { 241 | target = client.getChannelById(data.channelId) 242 | } else { 243 | target = client 244 | if (method === 'createVoiceStream') { 245 | let voiceId = payload.shift() 246 | let samplesPerPacket = payload.shift() 247 | 248 | let stream = target.createVoiceStream.apply(target, payload) 249 | 250 | setupOutboundVoice(voiceId, samplesPerPacket, stream) 251 | return 252 | } 253 | if (method === 'disconnect') { 254 | delete clients[data.clientId] 255 | } 256 | } 257 | 258 | target[method].apply(target, payload) 259 | } else if (data.voiceId != null) { 260 | let stream = voiceStreams[data.voiceId] 261 | let buffer = data.chunk 262 | if (buffer) { 263 | stream.write(Buffer.from(buffer)) 264 | } else { 265 | delete voiceStreams[data.voiceId] 266 | stream.end() 267 | } 268 | } 269 | } 270 | 271 | self.addEventListener('message', (ev) => { 272 | try { 273 | onMessage(ev.data) 274 | } catch (ex) { 275 | console.error('exception during message event', ev.data, ex) 276 | } 277 | }) 278 | 279 | export default null -------------------------------------------------------------------------------- /loc/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "connectdialog": { 3 | "title": "Verbindung herstellen", 4 | "address": "Adresse", 5 | "port": "Port", 6 | "username": "Nutzername", 7 | "password": "Passwort", 8 | "tokens": "Tokens", 9 | "remove": "Entfernen", 10 | "add": "Hinzufügen", 11 | "cancel": "Abbrechen", 12 | "connect": "Verbinden" 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /loc/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "connectdialog": { 3 | "title": "Connect to Server", 4 | "address": "Address", 5 | "port": "Port", 6 | "username": "Username", 7 | "password": "Password", 8 | "tokens": "Tokens", 9 | "remove": "Remove", 10 | "add": "Add", 11 | "cancel": "Cancel", 12 | "connect": "Connect", 13 | "error": { 14 | "title": "Failed to connect", 15 | "reason": { 16 | "refused": "The connection has been refused.", 17 | "version": "The server uses an incompatible version.", 18 | "username": "Your user name was rejected. Maybe try a different one?", 19 | "userpassword": "The given password is incorrect.\nThe user name you have chosen requires a special one.", 20 | "serverpassword": "The given password is incorrect.", 21 | "username_in_use": "The user name you have chosen is already in use.", 22 | "full": "The server is full.", 23 | "clientcert": "The server requires you to provide a client certificate which is not supported by this web application.", 24 | "server": "The server reports:" 25 | }, 26 | "retry": "Retry", 27 | "cancel": "Cancel" 28 | } 29 | }, 30 | "joindialog": { 31 | "title": "Mumble Voice Conference", 32 | "connect": "Join Conference" 33 | }, 34 | "toolbar": { 35 | "orientation": "Switch Orientation", 36 | "connect": "Connection", 37 | "information": "Information", 38 | "mute": "Mute", 39 | "unmute": "Unmute", 40 | "deaf": "Deafen", 41 | "undeaf": "Undeafen", 42 | "record": "Record", 43 | "comment": "Comment", 44 | "settings": "Settings", 45 | "sourcecode": "Open Source Code" 46 | }, 47 | "usercontextmenu": { 48 | "mute": "Mute", 49 | "deafen": "Deafen", 50 | "priority_speaker": "Priority Speaker", 51 | "local_mute": "Local Mute", 52 | "ignore_messages": "Ignore Messages", 53 | "view_comment": "View Comment", 54 | "change_comment": "Change Comment", 55 | "reset_comment": "Reset Comment", 56 | "view_avatar": "View Avatar", 57 | "change_avatar": "Change Avatar", 58 | "reset_avatar": "Reset Avatar", 59 | "send_message": "Send Message", 60 | "information": "Information", 61 | "self_mute": "Self Mute", 62 | "self_deafen": "Self Deafen", 63 | "add_friend": "Add Friend", 64 | "remove_friend": "Remove Friend" 65 | }, 66 | "channelcontextmenu": { 67 | "join": "Join Channel", 68 | "add": "Add", 69 | "edit": "Edit", 70 | "remove": "Remove", 71 | "link": "Link", 72 | "unlink": "Unlink", 73 | "unlink_all": "Unlink All", 74 | "copy_mumble_url": "Copy Mumble URL", 75 | "copy_mumble_web_url": "Copy Mumble-Web URL", 76 | "send_message": "Send Message" 77 | }, 78 | "logentry": { 79 | "connecting": "Connecting to server", 80 | "connected": "Connected!", 81 | "connection_error": "Connection error:", 82 | "connection_fallback_mode": "Server does not support WebRTC, re-trying in fallback mode..", 83 | "unknown_voice_mode": "Unknown voice mode:", 84 | "mic_init_error": "Cannot initialize user media. Microphone will not work:" 85 | }, 86 | "chat": { 87 | "channel_message_placeholder": "Type message to channel '%1' here", 88 | "user_message_placeholder": "Type message to user '%1' here" 89 | } 90 | } 91 | 92 | -------------------------------------------------------------------------------- /loc/eo.json: -------------------------------------------------------------------------------- 1 | { 2 | "connectdialog": { 3 | "title": "Konektado", 4 | "address": "Adreso", 5 | "port": "Pordo", 6 | "username": "Uzantnomo", 7 | "password": "Pasvorto", 8 | "tokens": "Ĵetonoj", 9 | "remove": "Forigi", 10 | "add": "Aldoni", 11 | "cancel": "Nuligi", 12 | "connect": "Konekti" 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /loc/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "connectdialog": { 3 | "title": "Conectar al servidor", 4 | "address": "Dirección", 5 | "port": "Puerto", 6 | "username": "Nombre de usuario", 7 | "password": "Contraseña", 8 | "tokens": "Tokens", 9 | "remove": "Eliminar", 10 | "add": "Añadir", 11 | "cancel": "Cancelar", 12 | "connect": "Conectar", 13 | "error": { 14 | "title": "Fallo al conectar", 15 | "reason": { 16 | "refused": "La conexión ha sido rechazada.", 17 | "version": "El servidor usa una versión incompatible.", 18 | "username": "El nombre de usuario está en uso o no es válido. Prueba con otro.", 19 | "userpassword": "Contraseña incorrecta.\nEl nombre de usuario elegido requiere contraseña.", 20 | "serverpassword": "Contraseña incorrecta.", 21 | "username_in_use": "El nombre de usuario está en uso.", 22 | "full": "El servidor está lleno (completo).", 23 | "clientcert": "El servidor requiere acceder con un certificado, lo que no está soportado en esta aplicación web.", 24 | "server": "El servidor informa:" 25 | }, 26 | "retry": "Reintentar", 27 | "cancel": "Cancelar" 28 | } 29 | }, 30 | "joindialog": { 31 | "title": "Chat de Voz Mumble", 32 | "connect": "Unirse a la conferencia" 33 | }, 34 | "usercontextmenu": { 35 | "mute": "Enmudecer", 36 | "deafen": "Ensordecer", 37 | "priority_speaker": "Orador prioritario", 38 | "local_mute": "Enmudecer localmente", 39 | "ignore_messages": "Ignorar mensajes", 40 | "view_comment": "Ver comentarios", 41 | "change_comment": "Cambiar comentarios", 42 | "reset_comment": "Reiniciar comentarios", 43 | "view_avatar": "Ver Avatar", 44 | "change_avatar": "Cambiar Avatar", 45 | "reset_avatar": "Reiniciar Avatar", 46 | "send_message": "Enviar un mensaje", 47 | "information": "Información", 48 | "self_mute": "Enmudecerse a uno mismo", 49 | "self_deafen": "Ensordecerse a uno mismo", 50 | "add_friend": "Añadir amigo", 51 | "remove_friend": "Eliminar amigo" 52 | }, 53 | "channelcontextmenu": { 54 | "join": "Unirse al canal", 55 | "add": "Añadir", 56 | "edit": "Editar", 57 | "remove": "Eliminar", 58 | "link": "Link", 59 | "unlink": "Unlink", 60 | "unlink_all": "Unlink All", 61 | "copy_mumble_url": "Copiar Mumble URL", 62 | "copy_mumble_web_url": "Copiar Mumble-Web URL", 63 | "send_message": "Enviar mensaje" 64 | }, 65 | "logentry": { 66 | "connecting": "Conectando al servidor", 67 | "connected": "¡Conectado!", 68 | "connection_error": "Error en la conexión:", 69 | "unknown_voice_mode": "Modo de voz desconocido:", 70 | "mic_init_error": "No se pudieron inicializar los medios. El micrófono no funcionará:" 71 | }, 72 | "chat": { 73 | "channel_message_placeholder": "Escribe un mensaje al canal '%1'", 74 | "user_message_placeholder": "Escribe un mensaje al usuario '%1'" 75 | } 76 | } 77 | 78 | -------------------------------------------------------------------------------- /loc/oc.json: -------------------------------------------------------------------------------- 1 | { 2 | "connectdialog": { 3 | "title": "Connexion al servidor", 4 | "address": "Adreça", 5 | "port": "Pòrt", 6 | "username": "Nom d’utilizaire", 7 | "password": "Senhal", 8 | "tokens": "Getons", 9 | "remove": "Suprimir", 10 | "add": "Ajustar", 11 | "cancel": "Anullar", 12 | "connect": "Se connectar", 13 | "error": { 14 | "title": "Connexion impossibla", 15 | "reason": { 16 | "refused": "Lo servidor a refusat la connexion.", 17 | "version": "Lo servidor utiliza una version incompatibla.", 18 | "username": "Vòstre nom d’utilizaire es estat regetat. Ensajatz benlèu un autre ?", 19 | "userpassword": "Lo senhal donat es incorrèct.\nLo nom d’utilizaire qu’avètz causit requerís un senhal especial.", 20 | "serverpassword": "Lo senhal donat es incorrèct.", 21 | "username_in_use": "Lo nom d’utilizaire donat es ja utilizat.", 22 | "full": "Lo servidor es plen.", 23 | "clientcert": "Lo servidor requerís que forniscatz un certificat client qu’es pas compatible amb aquesta web aplicacion.", 24 | "server": "Lo servidor senhala :" 25 | }, 26 | "retry": "Ensajar tornamai", 27 | "cancel": "Anullar" 28 | } 29 | }, 30 | "joindialog": { 31 | "title": "Conferéncia àudio Mumble", 32 | "connect": "Participar a la conferéncia" 33 | }, 34 | "usercontextmenu": { 35 | "mute": "Copar lo son", 36 | "deafen": "Sordina", 37 | "priority_speaker": "Prioritat parlaire", 38 | "local_mute": "Copar lo son localament", 39 | "ignore_messages": "Ignorar los messatges", 40 | "view_comment": "Veire lo comentari", 41 | "change_comment": "Cambiar lo comentari", 42 | "reset_comment": "Escafar lo comentari", 43 | "view_avatar": "Veire l’avatar", 44 | "change_avatar": "Cambiar l’avatar", 45 | "reset_avatar": "Escafar Avatar", 46 | "send_message": "Enviar un messatge", 47 | "information": "Informacions", 48 | "self_mute": "Copar mon son", 49 | "self_deafen": "Me metre en sordina", 50 | "add_friend": "Ajustar coma amic", 51 | "remove_friend": "Tirar dels amics" 52 | }, 53 | "channelcontextmenu": { 54 | "join": "Rejónher la sala", 55 | "add": "Ajustar", 56 | "edit": "Modificar", 57 | "remove": "Suprimir", 58 | "link": "Associar", 59 | "unlink": "Desassociar", 60 | "unlink_all": "Tot desassociar", 61 | "copy_mumble_url": "Copair l’URL Mumble", 62 | "copy_mumble_web_url": "Copiar l’URL Mumble-Web", 63 | "send_message": "Enviar messatge" 64 | }, 65 | "logentry": { 66 | "connecting": "Connexion al servidor", 67 | "connected": "Connectat !", 68 | "connection_error": "Error de connexion :", 69 | "unknown_voice_mode": "Mòde àudio desconegut :", 70 | "mic_init_error": "Aviada del mèdia utilizaire impossibla. Lo microfòn foncionarà pas :" 71 | }, 72 | "chat": { 73 | "channel_message_placeholder": "Escrivètz un messatge per la sala '%1' aquí", 74 | "user_message_placeholder": "Escrivètz un messatge a '%1' aquí" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mumble-web", 3 | "version": "0.5.1", 4 | "description": "An HTML5 Mumble client.", 5 | "scripts": { 6 | "build": "webpack && [ -f dist/config.local.js ] || cp app/config.local.js dist/", 7 | "watch": "webpack --watch", 8 | "prepare": "rm -rf dist && npm run build", 9 | "test": "echo \"Error: no test specified\" && exit 1", 10 | "postinstall": "patch-package" 11 | }, 12 | "author": "Jonas Herzig ", 13 | "license": "ISC", 14 | "repository": "johni0702/mumble-web", 15 | "homepage": "https://github.com/johni0702/mumble-web", 16 | "files": [ 17 | "dist" 18 | ], 19 | "devDependencies": { 20 | "@babel/core": "^7.12.9", 21 | "@babel/plugin-transform-runtime": "^7.12.1", 22 | "@babel/preset-env": "^7.12.7", 23 | "@babel/runtime": "^7.12.5", 24 | "anchorme": "^2.1.2", 25 | "audio-buffer-utils": "^5.1.2", 26 | "audio-context": "^1.0.3", 27 | "babel-loader": "^8.2.1", 28 | "brfs": "^2.0.2", 29 | "bytebuffer": "^5.0.1", 30 | "css-loader": "^3.6.0", 31 | "dompurify": "^2.2.2", 32 | "drop-stream": "^1.0.0", 33 | "duplex-maker": "^1.0.0", 34 | "extract-loader": "^5.1.0", 35 | "file-loader": "^4.3.0", 36 | "fs": "0.0.1-security", 37 | "getusermedia": "^2.0.1", 38 | "html-loader": "^0.5.5", 39 | "json-loader": "^0.5.7", 40 | "keyboardjs": "^2.6.4", 41 | "knockout": "^3.5.1", 42 | "libsamplerate.js": "^1.0.0", 43 | "lodash.assign": "^4.2.0", 44 | "microphone-stream": "^5.1.0", 45 | "mumble-client": "github:johni0702/mumble-client#f73a08b", 46 | "mumble-client-codecs-browser": "^1.2.0", 47 | "mumble-client-websocket": "github:johni0702/mumble-client-websocket#5b0ed8d", 48 | "node-sass": "^4.14.1", 49 | "patch-package": "^6.2.1", 50 | "raw-loader": "^4.0.2", 51 | "regexp-replace-loader": "1.0.1", 52 | "sass-loader": "^8.0.2", 53 | "stream-chunker": "^1.2.8", 54 | "subworkers": "^1.0.1", 55 | "to-arraybuffer": "^1.0.1", 56 | "transform-loader": "^0.2.4", 57 | "voice-activity-detection": "github:johni0702/voice-activity-detection#9f8bd90", 58 | "web-audio-buffer-queue": "^1.1.0", 59 | "webpack": "^4.44.2", 60 | "webpack-cli": "^3.3.12", 61 | "worker-loader": "^2.0.0" 62 | }, 63 | "optionalDependencies": {} 64 | } 65 | -------------------------------------------------------------------------------- /patches/mumble-client-codecs-browser+1.2.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/mumble-client-codecs-browser/lib/decode-worker.js b/node_modules/mumble-client-codecs-browser/lib/decode-worker.js 2 | index 3925f29..be9af92 100644 3 | --- a/node_modules/mumble-client-codecs-browser/lib/decode-worker.js 4 | +++ b/node_modules/mumble-client-codecs-browser/lib/decode-worker.js 5 | @@ -1,10 +1,6 @@ 6 | 'use strict'; 7 | 8 | -Object.defineProperty(exports, "__esModule", { 9 | - value: true 10 | -}); 11 | 12 | -exports.default = function (self) { 13 | var opusDecoder, celt7Decoder; 14 | self.addEventListener('message', function (e) { 15 | var data = e.data; 16 | @@ -55,10 +51,12 @@ exports.default = function (self) { 17 | }, [_decoded.buffer]); 18 | } 19 | }); 20 | -}; 21 | + 22 | 23 | var _libopus = require('libopus.js'); 24 | 25 | var _libcelt = require('libcelt7.js'); 26 | 27 | var MUMBLE_SAMPLE_RATE = 48000; 28 | + 29 | +export default null 30 | \ No newline at end of file 31 | diff --git a/node_modules/mumble-client-codecs-browser/lib/decoder-stream.js b/node_modules/mumble-client-codecs-browser/lib/decoder-stream.js 32 | index 6cfda8b..28a9549 100644 33 | --- a/node_modules/mumble-client-codecs-browser/lib/decoder-stream.js 34 | +++ b/node_modules/mumble-client-codecs-browser/lib/decoder-stream.js 35 | @@ -1,9 +1,5 @@ 36 | 'use strict'; 37 | 38 | -Object.defineProperty(exports, "__esModule", { 39 | - value: true 40 | -}); 41 | - 42 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 43 | 44 | var _stream = require('stream'); 45 | @@ -12,17 +8,11 @@ var _reusePool = require('reuse-pool'); 46 | 47 | var _reusePool2 = _interopRequireDefault(_reusePool); 48 | 49 | -var _webworkify = require('webworkify'); 50 | - 51 | -var _webworkify2 = _interopRequireDefault(_webworkify); 52 | - 53 | var _toArraybuffer = require('to-arraybuffer'); 54 | 55 | var _toArraybuffer2 = _interopRequireDefault(_toArraybuffer); 56 | 57 | -var _decodeWorker = require('./decode-worker'); 58 | - 59 | -var _decodeWorker2 = _interopRequireDefault(_decodeWorker); 60 | +import DecodeWorker from './decode-worker'; 61 | 62 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 63 | 64 | @@ -33,7 +23,7 @@ function _possibleConstructorReturn(self, call) { if (!self) { throw new Referen 65 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 66 | 67 | var pool = (0, _reusePool2.default)(function () { 68 | - return (0, _webworkify2.default)(_decodeWorker2.default); 69 | + return new DecodeWorker(); 70 | }); 71 | // Prepare first worker 72 | pool.recycle(pool.get()); 73 | @@ -48,11 +38,6 @@ var DecoderStream = function (_Transform) { 74 | 75 | _this._worker = pool.get(); 76 | _this._worker.onmessage = function (msg) { 77 | - if (_this._worker.objectURL) { 78 | - // The object URL can now be revoked as the worker has been loaded 79 | - window.URL.revokeObjectURL(_this._worker.objectURL); 80 | - _this._worker.objectURL = null; 81 | - } 82 | _this._onMessage(msg.data); 83 | }; 84 | return _this; 85 | @@ -112,4 +97,5 @@ var DecoderStream = function (_Transform) { 86 | return DecoderStream; 87 | }(_stream.Transform); 88 | 89 | -exports.default = DecoderStream; 90 | \ No newline at end of file 91 | +//exports.default = DecoderStream; 92 | +export default DecoderStream 93 | \ No newline at end of file 94 | diff --git a/node_modules/mumble-client-codecs-browser/lib/encode-worker.js b/node_modules/mumble-client-codecs-browser/lib/encode-worker.js 95 | index f7187ab..c2ebaa3 100644 96 | --- a/node_modules/mumble-client-codecs-browser/lib/encode-worker.js 97 | +++ b/node_modules/mumble-client-codecs-browser/lib/encode-worker.js 98 | @@ -1,10 +1,6 @@ 99 | 'use strict'; 100 | 101 | -Object.defineProperty(exports, "__esModule", { 102 | - value: true 103 | -}); 104 | 105 | -exports.default = function (self) { 106 | var opusEncoder, celt7Encoder; 107 | var bitrate; 108 | self.addEventListener('message', function (e) { 109 | @@ -70,7 +66,7 @@ exports.default = function (self) { 110 | }, [_buffer]); 111 | } 112 | }); 113 | -}; 114 | + 115 | 116 | var _libopus = require('libopus.js'); 117 | 118 | @@ -83,3 +79,5 @@ var _toArraybuffer2 = _interopRequireDefault(_toArraybuffer); 119 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 120 | 121 | var MUMBLE_SAMPLE_RATE = 48000; 122 | + 123 | +export default null 124 | \ No newline at end of file 125 | diff --git a/node_modules/mumble-client-codecs-browser/lib/encoder-stream.js b/node_modules/mumble-client-codecs-browser/lib/encoder-stream.js 126 | index 021f131..eeb9189 100644 127 | --- a/node_modules/mumble-client-codecs-browser/lib/encoder-stream.js 128 | +++ b/node_modules/mumble-client-codecs-browser/lib/encoder-stream.js 129 | @@ -1,9 +1,5 @@ 130 | 'use strict'; 131 | 132 | -Object.defineProperty(exports, "__esModule", { 133 | - value: true 134 | -}); 135 | - 136 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 137 | 138 | var _stream = require('stream'); 139 | @@ -12,13 +8,7 @@ var _reusePool = require('reuse-pool'); 140 | 141 | var _reusePool2 = _interopRequireDefault(_reusePool); 142 | 143 | -var _webworkify = require('webworkify'); 144 | - 145 | -var _webworkify2 = _interopRequireDefault(_webworkify); 146 | - 147 | -var _encodeWorker = require('./encode-worker'); 148 | - 149 | -var _encodeWorker2 = _interopRequireDefault(_encodeWorker); 150 | +import EncodeWorker from './encode-worker' 151 | 152 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 153 | 154 | @@ -29,7 +19,7 @@ function _possibleConstructorReturn(self, call) { if (!self) { throw new Referen 155 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 156 | 157 | var pool = (0, _reusePool2.default)(function () { 158 | - return (0, _webworkify2.default)(_encodeWorker2.default); 159 | + return new EncodeWorker(); 160 | }); 161 | // Prepare first worker 162 | pool.recycle(pool.get()); 163 | @@ -46,11 +36,6 @@ var EncoderStream = function (_Transform) { 164 | 165 | _this._worker = pool.get(); 166 | _this._worker.onmessage = function (msg) { 167 | - if (_this._worker.objectURL) { 168 | - // The object URL can now be revoked as the worker has been loaded 169 | - window.URL.revokeObjectURL(_this._worker.objectURL); 170 | - _this._worker.objectURL = null; 171 | - } 172 | _this._onMessage(msg.data); 173 | }; 174 | return _this; 175 | @@ -96,4 +81,5 @@ var EncoderStream = function (_Transform) { 176 | return EncoderStream; 177 | }(_stream.Transform); 178 | 179 | -exports.default = EncoderStream; 180 | \ No newline at end of file 181 | +//exports.default = EncoderStream; 182 | +export default EncoderStream 183 | \ No newline at end of file 184 | -------------------------------------------------------------------------------- /themes/MetroMumbleDark/loading.scss: -------------------------------------------------------------------------------- 1 | $bg-color: #2c2c2c !default 2 | $spinner-color: #888 !default 3 | $spinner-bg-color: #222 !default 4 | 5 | @import '../MetroMumbleLight/loading'; 6 | -------------------------------------------------------------------------------- /themes/MetroMumbleDark/main.scss: -------------------------------------------------------------------------------- 1 | $black: #bbb !default 2 | $darkgray: #777 !default 3 | $gray: #555 !default 4 | $lightgray: #555 !default 5 | $bg-color: #2c2c2c !default 6 | $white: #1c1c1c !default 7 | 8 | $lightblue: #557 !default 9 | 10 | $toolbar-hover-bg-color: $lightblue !default 11 | $toolbar-hover-border-color: $lightblue !default 12 | $toolbar-active-bg-color: $gray !default 13 | $toolbar-active-border-color: $gray !default 14 | $dialog-color: $black !default 15 | $channel-selected-bg-color: $lightblue !default 16 | 17 | @import '../MetroMumbleLight/main'; 18 | 19 | .dialog-header { 20 | color: #000; 21 | font-weight: bold; 22 | } 23 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/loading.scss: -------------------------------------------------------------------------------- 1 | $bg-color: #eee !default 2 | $spinner-color: #999 !default 3 | $spinner-bg-color: #ddd !default 4 | 5 | .loading-container { 6 | position: absolute; 7 | top: 0; 8 | width: 100%; 9 | height: 100%; 10 | background-color: $bg-color; 11 | z-index: 1000; 12 | } 13 | 14 | .loading-circle { 15 | box-sizing: border-box; 16 | width: 80px; 17 | height: 80px; 18 | position: absolute; 19 | top: calc(50% - 40px); 20 | left: calc(50% - 40px); 21 | border-radius: 100%; 22 | border: 10px solid $spinner-bg-color; 23 | border-top-color: $spinner-color; 24 | animation: spin 1s infinite linear; 25 | } 26 | 27 | @keyframes spin { 28 | 100% { 29 | transform: rotate(360deg); 30 | } 31 | } 32 | 33 | .loaded { 34 | top: -100%; 35 | transition: top 1s; 36 | transition-delay: 2s; 37 | } 38 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/main.scss: -------------------------------------------------------------------------------- 1 | $black: #000 !default 2 | $darkgray: #888 !default 3 | $gray: #a9a9a9 !default 4 | $lightgray: #d3d3d3 !default 5 | $bg-color: #eee !default 6 | $white: #fff !default 7 | 8 | $font-color: $black !default 9 | $font-family: "Segoe UI", Frutiger, "Frutiger Linotype", "Dejavu Sans", "Helvetica Neue", Arial, sans-serif !default 10 | $font-disabled-color: $gray !default 11 | $panel-bg-color: $white !default 12 | $panel-border-color: $lightgray !default 13 | $channel-tree-color: $lightgray !default 14 | $channel-hover-bg-color: $lightgray !default 15 | $channel-selected-bg-color: lightblue !default 16 | $channel-selected-border-color: $darkgray !default 17 | $tooltip-border-color: $darkgray !default 18 | $chat-channel-color: orange !default 19 | $chat-user-color: green !default 20 | $chat-input-color: $font-color !default 21 | $mic-volume-border-color: $black !default 22 | $talk-outline-color: green !default 23 | $whisper-outline-color: purple !default 24 | $shout-outline-color: cyan !default 25 | 26 | $toolbar-hover-bg-color: $lightgray !default 27 | $toolbar-hover-border-color: $gray !default 28 | $toolbar-active-bg-color: $white !default 29 | $toolbar-active-border-color: $toolbar-hover-bg-color !default 30 | $toolbar-divider-color: $lightgray !default 31 | $dialog-header-color: $white !default 32 | $dialog-header-bg-color: $darkgray !default 33 | $dialog-header-border-bottom-color: $gray !default 34 | $dialog-bg-color: $bg-color !default 35 | $dialog-border-color: $darkgray !default 36 | $dialog-color: $font-color !default 37 | $dialog-button-border-color: $darkgray !default 38 | $dialog-button-bg-color: $white !default 39 | $dialog-button-color: $dialog-color !default 40 | $dialog-input-border-color: $darkgray !default 41 | $dialog-input-bg-color: $white !default 42 | $dialog-input-color: $dialog-color !default 43 | $context-menu-bg-color: $bg-color !default 44 | $context-menu-border-color: $panel-border-color !default 45 | $context-menu-hover-bg-color: $toolbar-hover-bg-color !default 46 | 47 | $tooltip-bg-color: $panel-bg-color !default 48 | $channels-bg-color: $panel-bg-color !default 49 | $channels-border-color: $panel-border-color !default 50 | $chat-bg-color: $panel-bg-color !default 51 | $chat-border-color: $panel-border-color !default 52 | 53 | html, body { 54 | background-color: $bg-color; 55 | color: $font-color; 56 | font-family: $font-family; 57 | margin: 0; 58 | overflow: hidden; 59 | height: 100% 60 | } 61 | #container { 62 | height: 100%; 63 | } 64 | .channel-root-container { 65 | text-size: 16px; 66 | margin-left: 2px; 67 | background-color: $channels-bg-color; 68 | border: 1px solid $channels-border-color; 69 | float: left; 70 | border-radius: 3px; 71 | overflow-x: hidden; 72 | overflow-y: auto; 73 | } 74 | .toolbar-horizontal ~ .channel-root-container { 75 | margin-top: 2px; 76 | width: calc(59% - 6px); 77 | height: calc(98% - 38px); 78 | } 79 | .toolbar-vertical ~ .channel-root-container { 80 | margin-top: 1%; 81 | width: calc(59% - 6px); 82 | height: calc(98% - 6px); 83 | } 84 | .chat { 85 | margin-right: 2px; 86 | float: left; 87 | } 88 | .toolbar-horizontal ~ .chat { 89 | margin-top: 2px; 90 | margin-left: 1%; 91 | width: 39%; 92 | height: calc(98% - 38px); 93 | } 94 | .toolbar-vertical ~ .chat { 95 | margin-top: 1%; 96 | margin-left: 2px; 97 | width: calc(39% - 36px); 98 | height: calc(98% - 4px); 99 | } 100 | .log { 101 | background-color: $chat-bg-color; 102 | height: calc(100% - 42px); 103 | padding: 5px; 104 | border: 1px $chat-border-color solid; 105 | border-radius: 3px; 106 | overflow-x: hidden; 107 | overflow-y: scroll; 108 | } 109 | .branch img { 110 | height: 19px; 111 | } 112 | .branch { 113 | position: absolute; 114 | padding-top: 3px; 115 | padding-bottom: 3px; 116 | background-color: $channels-bg-color; 117 | } 118 | .channel-sub { 119 | margin-left: 9px; 120 | border-left: 1px transparent solid; 121 | padding-left: 9px; 122 | } 123 | .channel-wrapper:nth-last-child(n + 2) > .branch:not(:empty) + .channel-sub { 124 | border-left: 1px $channel-tree-color solid; 125 | } 126 | .channel-tree, 127 | .user-wrapper { 128 | margin-left: 9px; 129 | } 130 | .channel-tree, 131 | .user-tree { 132 | position: absolute; 133 | } 134 | .channel-tree::before, 135 | .user-tree::before { 136 | content: ""; 137 | display: block; 138 | position: relative; 139 | width: 9px; 140 | border-left: 1px $channel-tree-color solid; 141 | border-bottom: 1px $channel-tree-color solid; 142 | height: 14px; 143 | } 144 | .channel-wrapper:nth-last-child(n + 2) > .channel-tree:after, 145 | .user-wrapper:nth-last-child(n + 2) .user-tree:after { 146 | content: ""; 147 | display: block; 148 | position: relative; 149 | width: 0px; 150 | border-left: 1px $channel-tree-color solid; 151 | height: 14px; 152 | } 153 | .user { 154 | margin-left: 9px; 155 | } 156 | .user-avatar, .user-talk { 157 | vertical-align: middle; 158 | } 159 | @mixin drop-shadow-4x($size, $blur, $color) { 160 | filter: drop-shadow(#{+$size} #{+$size} $blur $color) 161 | drop-shadow(#{+$size} #{-$size} $blur $color) 162 | drop-shadow(#{-$size} #{+$size} $blur $color) 163 | drop-shadow(#{-$size} #{-$size} $blur $color); 164 | } 165 | @mixin user-avatar-drop-shadow($color) { 166 | @include drop-shadow-4x(1px, 1px, $color); 167 | } 168 | .user-avatar-talk-on { 169 | @include user-avatar-drop-shadow($talk-outline-color); 170 | } 171 | .user-avatar-talk-whisper { 172 | @include user-avatar-drop-shadow($whisper-outline-color); 173 | } 174 | .user-avatar-talk-shout { 175 | @include user-avatar-drop-shadow($shout-outline-color); 176 | } 177 | .user-status, .channel-status { 178 | float: right; 179 | } 180 | .user,.channel{ 181 | height: 23px; 182 | line-height: 23px; 183 | padding: 2px; 184 | border: 1px solid transparent; 185 | } 186 | .selected { 187 | background-color: $channel-selected-bg-color !important; 188 | border: 1px solid $channel-selected-border-color; 189 | border-radius: 3px; 190 | } 191 | .user:hover,.channel:hover { 192 | background-color: $channel-hover-bg-color; 193 | } 194 | .thisClient { 195 | font-weight: bold 196 | } 197 | .currentChannel { 198 | font-weight: bold 199 | } 200 | .user-status img, .channel-status img { 201 | margin-top: 2px; 202 | width: 19px; 203 | height: 19px 204 | } 205 | .channel img, .user img { 206 | width: auto; 207 | height: 19px; 208 | } 209 | .channel-name, .user-name { 210 | display: inline; 211 | } 212 | .channel:hover .tooltip, .user:hover .tooltip { 213 | visibility: visible; 214 | height: auto; 215 | transition-delay: 1s; 216 | } 217 | .tooltip { 218 | visibility: hidden; 219 | height: 0px; 220 | background: $tooltip-bg-color; 221 | border: 1px solid $tooltip-border-color; 222 | margin-top: 16px; 223 | margin-left: 30px; 224 | padding: 10px; 225 | position: absolute; 226 | z-index: 100; 227 | } 228 | .context-menu { 229 | position: absolute; 230 | z-index: 50; 231 | background: $context-menu-bg-color; 232 | border: 1px solid $context-menu-border-color; 233 | margin: 0; 234 | padding: 0; 235 | list-style: none; 236 | 237 | & > li { 238 | padding: 5px 20px; 239 | padding-left: 10px; 240 | &::before { 241 | display: inline-block; 242 | width: 10px; 243 | padding-right: 5px; 244 | content: ''; 245 | } 246 | &.checked::before { 247 | content: '✓'; 248 | } 249 | &:hover { 250 | background: $context-menu-hover-bg-color; 251 | } 252 | &.disabled { 253 | background: $context-menu-bg-color; 254 | color: $font-disabled-color; 255 | } 256 | } 257 | } 258 | .avatar-view { 259 | position: absolute; 260 | z-index: 200; 261 | max-width: 90%; 262 | max-height: 90%; 263 | top: 0; 264 | bottom: 0; 265 | left: 0; 266 | right: 0; 267 | margin: auto; 268 | } 269 | .toolbar { 270 | display: flex; 271 | align-items: center; 272 | } 273 | .toolbar img { 274 | height: 28px; 275 | width: 28px; 276 | padding: 2px; 277 | border: 1px solid transparent; 278 | border-radius: 3px; 279 | cursor: pointer; 280 | } 281 | .toolbar img:hover { 282 | border: 1px solid $toolbar-hover-bg-color; 283 | background-color: $toolbar-hover-border-color; 284 | } 285 | .toolbar .tb-active { 286 | border: 1px solid $toolbar-active-bg-color; 287 | background-color: $toolbar-active-border-color; 288 | } 289 | .toolbar-horizontal { 290 | flex-direction: row; 291 | height: 36px; 292 | margin-top: 4px; 293 | margin-left: 1%; 294 | padding-left: 5px; 295 | } 296 | .toolbar-vertical { 297 | flex-direction: column; 298 | width: 36px; 299 | margin-top: 1%; 300 | margin-left: 4px; 301 | padding-top: 5px; 302 | float: left; 303 | } 304 | .toolbar-horizontal > * { 305 | margin-right: 5px; 306 | } 307 | .toolbar-vertical > * { 308 | margin-bottom: 5px; 309 | } 310 | .divider { 311 | display: inline-block; 312 | } 313 | .toolbar-horizontal .divider { 314 | height: 32px; 315 | border-left: 1px $toolbar-divider-color solid; 316 | } 317 | .toolbar-vertical .divider { 318 | width: 32px; 319 | border-top: 1px $toolbar-divider-color solid; 320 | } 321 | .toolbar-horizontal .handle-horizontal { 322 | width: auto !important; 323 | border: none !important; 324 | background-color: $bg-color !important; 325 | } 326 | .toolbar-horizontal .handle-vertical { 327 | display: none; 328 | } 329 | .toolbar-vertical .handle-vertical { 330 | height: auto !important; 331 | border: none !important; 332 | background-color: $bg-color !important; 333 | } 334 | .toolbar-vertical .handle-horizontal { 335 | display: none; 336 | } 337 | .channel-icon .channel-icon-active { 338 | display: none; 339 | } 340 | .channel-tag { 341 | font-weight: bold; 342 | color: $chat-channel-color; 343 | } 344 | .user-tag { 345 | font-weight: bold; 346 | color: $chat-user-color; 347 | } 348 | #message-box { 349 | width: 100%; 350 | border: none; 351 | background: none; 352 | color: $chat-input-color; 353 | margin: 5px 0 5px 0; 354 | padding: 0; 355 | height: 20px; 356 | } 357 | form { 358 | margin: 0; 359 | padding: 0; 360 | } 361 | .message-content p { 362 | margin: 0; 363 | } 364 | .tb-information.disabled, .tb-record, .tb-comment { 365 | filter: grayscale(100%); 366 | } 367 | .dialog-header { 368 | height: 20px; 369 | width: calc(100% - 10px); 370 | padding: 5px; 371 | text-align: center; 372 | color: $dialog-header-color; 373 | background-color: $dialog-header-bg-color; 374 | border-bottom: 1px solid $dialog-header-border-bottom-color; 375 | } 376 | .dialog-footer { 377 | width: calc(100% - 20px); 378 | margin: 10px; 379 | } 380 | .dialog-submit { 381 | float: right; 382 | } 383 | .dialog-close, .dialog-submit { 384 | width: 45%; 385 | font-size: 15px; 386 | border: 1px $dialog-button-border-color solid; 387 | border-radius: 3px; 388 | background-color: $dialog-button-bg-color; 389 | color: $dialog-button-color; 390 | padding: 1px; 391 | cursor: pointer; 392 | } 393 | .connect-dialog table { 394 | text-align: center; 395 | width: 100% 396 | } 397 | .dialog { 398 | position: absolute; 399 | max-height: calc(100% - 20px); 400 | max-width: calc(100% - 20px); 401 | top: 50%; 402 | left: 50%; 403 | transform: translate(-50%, -50%); 404 | overflow: auto; 405 | background-color: $dialog-bg-color; 406 | color: $dialog-color; 407 | border: 1px $dialog-border-color solid; 408 | box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.25); 409 | z-index: 20; 410 | } 411 | .settings-dialog table { 412 | width: 100%; 413 | padding: 5px; 414 | } 415 | .settings-dialog td { 416 | width: 50%; 417 | } 418 | .settings-dialog table select { 419 | width: 100%; 420 | } 421 | .settings-dialog table input { 422 | width: 100%; 423 | margin: 0px; 424 | } 425 | .settings-dialog table input[type="checkbox"] { 426 | width: auto; 427 | margin: auto; 428 | } 429 | .settings-dialog .mic-volume-container { 430 | height: 10px; 431 | border: 3px solid $mic-volume-border-color; 432 | } 433 | .settings-dialog .mic-volume { 434 | height: 100%; 435 | } 436 | .join-dialog { 437 | width: 300px; 438 | height: 100px; 439 | top: 50%; 440 | left: 50%; 441 | } 442 | .join-dialog .dialog-submit { 443 | float: none; 444 | width: 200px; 445 | position: absolute; 446 | top: calc(50% - 10px); 447 | left: calc(50% - 100px); 448 | } 449 | .connect-dialog input[type=text], select { 450 | font-size: 15px; 451 | border: 1px $dialog-input-border-color solid; 452 | border-radius: 3px; 453 | background-color: $dialog-input-bg-color; 454 | color: $dialog-input-color; 455 | padding: 2px; 456 | width: calc(100% - 8px); 457 | } 458 | .connect-dialog input[type=password] { 459 | font-size: 15px; 460 | border: 1px $dialog-input-border-color solid; 461 | border-radius: 3px; 462 | background-color: $dialog-input-bg-color; 463 | color: $dialog-input-color; 464 | padding: 2px; 465 | width: calc(100% - 8px); 466 | } 467 | .connection-info-dialog { 468 | h3 { 469 | margin-bottom: 5px; 470 | } 471 | .dialog-content { 472 | padding-left: 20px; 473 | } 474 | } 475 | 476 | 477 | /****************/ 478 | /* Minimal view */ 479 | /****************/ 480 | 481 | .minimal .toolbar-horizontal ~ .channel-root-container { 482 | width: calc(98% - 6px); 483 | } 484 | .minimal .toolbar-vertical ~ .channel-root-container { 485 | width: calc(98% - 42px); 486 | } 487 | .minimal .handle-horizontal { 488 | display: none; 489 | } 490 | .minimal .handle-vertical { 491 | display: none; 492 | } 493 | .minimal .divider { 494 | display: none; 495 | } 496 | .minimal .tb-connect { 497 | display: none; 498 | } 499 | .minimal .tb-information { 500 | display: none; 501 | } 502 | .minimal .tb-record { 503 | display: none; 504 | } 505 | .minimal .tb-comment { 506 | display: none; 507 | } 508 | .minimal .tb-settings { 509 | display: none; 510 | } 511 | .minimal .tb-sourcecode { 512 | display: none; 513 | } 514 | .minimal .chat { 515 | display: none; 516 | } 517 | .minimal .channel-wrapper { 518 | display: none; 519 | } 520 | .minimal .channel { 521 | display: none; 522 | } 523 | .minimal .user-tree { 524 | display: none; 525 | } 526 | .minimal .user-wrapper { 527 | margin-left: 0px; 528 | } 529 | .minimal .user { 530 | margin-left: 0px; 531 | padding-top: 0px; 532 | padding-bottom: 0px; 533 | border: none; 534 | height: 19px; 535 | line-height: 19px; 536 | } 537 | .minimal .user-status { 538 | height: 19px; 539 | } 540 | 541 | /* Mobile view */ 542 | 543 | @media only screen and (max-width: 600px) and (min-width: 320px) and (min-height: 600px) { 544 | 545 | .toolbar-horizontal ~ .channel-root-container, .toolbar-vertical ~ .channel-root-container { 546 | height:calc(100% - 440px); 547 | position:static; 548 | width:100%; 549 | } 550 | 551 | .toolbar-horizontal ~ .chat, .toolbar-vertical ~ .chat { 552 | position:fixed; 553 | bottom: 60px; 554 | left:0; 555 | width:100%; 556 | height:330px; 557 | y-overflow:auto; 558 | font-size:0.8em; 559 | z-index:10; 560 | } 561 | 562 | .toolbar-vertical { 563 | flex-direction: row; 564 | height: 36px; 565 | margin-top: 4px; 566 | margin-left: 1%; 567 | padding-left: 5px; 568 | } 569 | 570 | #message-box { 571 | margin: 10px 5px 10px 5px; 572 | padding: 10px; 573 | height: 2em; 574 | font-size: 1.2em; 575 | font-weight: bold; 576 | } 577 | 578 | .handle-vertical, .handle-horizontal { 579 | display: none; 580 | } 581 | 582 | .dialog { 583 | min-width: 350px; 584 | } 585 | } 586 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/svg/applications-internet.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 23 | 24 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/svg/audio-input-microphone-muted.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 9 | 10 | 11 | 16 | 17 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/svg/audio-input-microphone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 12 | 16 | 17 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/svg/audio-output-deafened.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | 10 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/svg/audio-output.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 | 26 | 28 | 30 | 31 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/svg/authenticated.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/svg/branch_closed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/svg/branch_open.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/svg/channel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/svg/channel_active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/svg/channel_linked.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 10 | 13 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/svg/comment.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/svg/comment_seen.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/svg/config_basic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 24 | 25 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/svg/deafened_self.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | 10 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/svg/deafened_server.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | 10 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/svg/default_avatar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/svg/filter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/svg/filter_off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/svg/filter_on.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | 11 | 12 | 13 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/svg/handle_horizontal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/svg/handle_vertical.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/svg/information_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/svg/layout_classic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 13 | 15 | 17 | 18 | 19 | Chatbar 20 | Tree 21 | 22 | 23 | 24 | 25 | Log 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/svg/layout_custom.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 13 | 15 | 17 | 18 | 19 | Chatbar 20 | Tree 21 | 22 | 23 | 24 | 25 | Log 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/svg/layout_hybrid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 13 | 15 | 17 | 18 | 19 | Chatbar 20 | Tree 21 | 22 | 23 | 24 | 25 | Log 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/svg/layout_stacked.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 13 | 15 | 17 | 18 | 19 | Chatbar 20 | Tree 21 | 22 | 23 | 24 | 25 | Log 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/svg/media-record.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/svg/mumble.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 37 | 38 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/svg/muted_local.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 8 | 13 | 14 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/svg/muted_self.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/svg/muted_server.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/svg/muted_suppressed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/svg/priority_speaker.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 19 | 20 | 21 | 22 | 23 | 24 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/svg/self_comment.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/svg/source-code.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/svg/talking_alt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 9 | 11 | 13 | 14 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/svg/talking_off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 10 | 12 | 14 | 15 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/svg/talking_on.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 10 | 12 | 14 | 15 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/svg/talking_whisper.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 9 | 11 | 13 | 14 | -------------------------------------------------------------------------------- /themes/MetroMumbleLight/svg/toolbar-comment.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var theme = '../themes/MetroMumbleLight' 2 | var path = require('path'); 3 | 4 | module.exports = { 5 | mode: 'development', 6 | entry: { 7 | index: [ 8 | './app/index.js', 9 | './app/index.html' 10 | ], 11 | config: './app/config.js', 12 | theme: './app/theme.js', 13 | matrix: './app/matrix.js' 14 | }, 15 | devtool: "cheap-source-map", 16 | output: { 17 | path: path.join(__dirname, 'dist'), 18 | chunkFilename: '[chunkhash].js', 19 | filename: '[name].js' 20 | }, 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.js$/, 25 | exclude: /node_modules/, 26 | use: { 27 | loader: 'babel-loader', 28 | options: { 29 | presets: ['@babel/preset-env'], 30 | plugins: ['@babel/plugin-transform-runtime'] 31 | } 32 | } 33 | }, 34 | { 35 | test: /\.html$/, 36 | use: [ 37 | { 38 | loader: 'file-loader', 39 | options: { 'name': '[name].[ext]' } 40 | }, 41 | { 42 | loader: "extract-loader" 43 | }, 44 | { 45 | loader: 'html-loader', 46 | options: { 47 | attrs: ['img:src', 'link:href'], 48 | root: theme 49 | } 50 | } 51 | ] 52 | }, 53 | { 54 | test: /\.css$/, 55 | use: [ 56 | 'file-loader', 57 | 'extract-loader', 58 | 'css-loader' 59 | ] 60 | }, 61 | { 62 | test: /\.scss$/, 63 | use: [ 64 | 'file-loader?name=[hash].css', 65 | 'extract-loader', 66 | 'css-loader', 67 | 'sass-loader' 68 | ] 69 | }, 70 | { 71 | type: 'javascript/auto', 72 | test: /manifest\.json$|\.xml$/, 73 | use: [ 74 | 'file-loader', 75 | 'extract-loader', 76 | { 77 | loader: 'regexp-replace-loader', 78 | options: { 79 | match: { 80 | pattern: "#require\\('([^']*)'\\)", 81 | flags: 'g' 82 | }, 83 | replaceWith: '"+require("$1")+"' 84 | } 85 | }, 86 | 'raw-loader' 87 | ] 88 | }, 89 | { 90 | test: /\.(svg|png|ico)$/, 91 | use: [ 92 | 'file-loader' 93 | ] 94 | }, 95 | { 96 | test: /worker\.js$/, 97 | use: { loader: 'worker-loader' } 98 | }, 99 | { 100 | enforce: 'post', 101 | test: /mumble-streams\/lib\/data.js/, 102 | use: [ 103 | 'transform-loader?brfs' 104 | ] 105 | } 106 | ] 107 | }, 108 | target: 'web' 109 | } 110 | --------------------------------------------------------------------------------