Merge branch 'master' into webrtc

This commit is contained in:
Jonas Herzig 2020-05-04 18:55:22 +02:00
commit 2fda5fd158
17 changed files with 9794 additions and 204 deletions

View file

@ -23,7 +23,7 @@ RUN cd /home/node && \
USER root USER root
RUN apk del gcc git make g++ RUN apk del gcc git
USER node USER node
@ -31,5 +31,5 @@ EXPOSE 8080
ENV MUMBLE_SERVER=mumble.aventer.biz:64738 ENV MUMBLE_SERVER=mumble.aventer.biz:64738
ENTRYPOINT ["/sbin/tini", "--"] ENTRYPOINT ["/sbin/tini", "--"]
CMD websockify --ssl-target --web /home/node/dist 8080 "$MUMBLE_SERVER" CMD websockify --ssl-target --web=/home/node/dist 8080 "$MUMBLE_SERVER"

View file

@ -25,8 +25,8 @@ or from git (webrtc branch only from git for now):
git clone -b webrtc https://github.com/johni0702/mumble-web git clone -b webrtc https://github.com/johni0702/mumble-web
cd mumble-web cd mumble-web
npm install npm install
npm run build
``` ```
Note that npm should not be ran as root, use an unprivileged user account instead.
The npm version is prebuilt and ready to use whereas the git version allows you The npm version is prebuilt and ready to use whereas the git version allows you
to e.g. customize the theme before building it. to e.g. customize the theme before building it.
@ -41,10 +41,11 @@ Mumble server is running on).
Additionally you will need some web server to serve static files and terminate the secure websocket connection (mumble-web-proxy only supports insecure ones). Additionally you will need some web server to serve static files and terminate the secure websocket connection (mumble-web-proxy only supports insecure ones).
A sample configuration for nginx that allows access to mumble-web at 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).
`https://voice.example.com/` and connecting at `wss://voice.example.com/demo` Replace `<proxybox>` with the host name of the machine where `mumble-web-proxy` is running. If `mumble-web-proxy` is running on the same machine as your web server, use `localhost`.
(similar to the demo server) looks like this:
``` * NGINX configuration file
```Nginx
server { server {
listen 443 ssl; listen 443 ssl;
server_name voice.example.com; server_name voice.example.com;
@ -55,7 +56,7 @@ server {
root /path/to/dist; root /path/to/dist;
} }
location /demo { location /demo {
proxy_pass http://proxybox:64737; proxy_pass http://<proxybox>:64737;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade; proxy_set_header Connection $connection_upgrade;
@ -67,12 +68,55 @@ map $http_upgrade $connection_upgrade {
'' close; '' close;
} }
``` ```
where `proxybox` is the machine running mumble-web-proxy (may be `localhost`):
* Caddy configuration file (`Caddyfile`)
``` ```
mumble-web-proxy --listen-ws 64737 --server mumbleserver:64738 http://voice.example.com {
redir https://voice.example.com
}
https://voice.example.com {
tls "/etc/letsencrypt/live/voice.example.com/fullchain.pem" "/etc/letsencrypt/live/voice.example.com/privkey.pem"
root /path/to/dist
proxy /demo http://<proxybox>:64737 {
websocket
}
}
```
To run `mumble-web-proxy`, execute the following command. Replace `<mumbleserver>` with the host name of your Mumble server (the one you connect to using the normal Mumble client).
Note that even if your Mumble server is running on the same machine as your `mumble-web-proxy`, you should use the external name because (by default, for disabling see its README) `mumble-web-proxy` will try to verify the certificate provided by the Mumble server and fail if it does not match the given host name.
```
mumble-web-proxy --listen-ws 64737 --server <mumbleserver>:64738
``` ```
If your mumble-web-proxy is running behind a NAT or firewall, take note of the respective section in its README. If your mumble-web-proxy is running behind a NAT or firewall, take note of the respective section in its README.
Make sure that your Mumble server is running. You may now open `https://voice.example.com` in a web browser. You will be prompted for server details: choose either `address: voice.example.com/demo` with `port: 443` or `address: voice.example.com` with `port: 443/demo`. You may prefill these values by appending `?address=voice.example.com/demo&port=443`. Choose a username, and click `Connect`: you should now be able to talk and use the chat.
Here is an example of systemd service, put it in `/etc/systemd/system/mumble-web.service` and adapt it to your needs:
```
[Unit]
Description=Mumble web interface
Documentation=https://github.com/johni0702/mumble-web
Requires=network.target mumble-server.service
After=network.target mumble-server.service
[Service]
Type=simple
User=www-data
ExecStart=/usr/bin/websockify --web=/usr/lib/node_modules/mumble-web/dist --ssl-target localhost:64737 localhost:64738
[Install]
WantedBy=multi-user.target
```
Then
```
systemctl daemon-reload
systemctl start mumble-web
systemctl enable mumble-web
```
### Configuration ### Configuration
The `app/config.js` file contains default values and descriptions for all configuration options. The `app/config.js` file contains default values and descriptions for all configuration options.
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. 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.

View file

@ -9,7 +9,8 @@ window.mumbleWebConfig = {
'port': true, 'port': true,
'token': true, 'token': true,
'username': true, 'username': true,
'password': true 'password': true,
'channelName': false
}, },
// Default values for user settings // Default values for user settings
// You can see your current value by typing `localStorage.getItem('mumble.$setting')` in the web console. // You can see your current value by typing `localStorage.getItem('mumble.$setting')` in the web console.

View file

@ -1,6 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8">
<!-- Favicon as generated by realfavicongenerator.net (slightly modified for webpack) --> <!-- Favicon as generated by realfavicongenerator.net (slightly modified for webpack) -->
<link rel="apple-touch-icon" sizes="180x180" href="favicon/apple-touch-icon.png"> <link rel="apple-touch-icon" sizes="180x180" href="favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" href="favicon/favicon-32x32.png" sizes="32x32"> <link rel="icon" type="image/png" href="favicon/favicon-32x32.png" sizes="32x32">
@ -26,35 +27,52 @@
css: { minimal: minimalView }"> css: { minimal: minimalView }">
<!-- ko with: connectDialog --> <!-- ko with: connectDialog -->
<div class="connect-dialog dialog" data-bind="visible: visible() && !joinOnly()"> <div class="connect-dialog dialog" data-bind="visible: visible() && !joinOnly()">
<div class="dialog-header"> <div id="connect-dialog_title" class="dialog-header">
Connect to Server Connect to Server
</div> </div>
<form data-bind="submit: connect"> <form data-bind="submit: connect">
<table> <table>
<tr data-bind="if: $root.config.connectDialog.address"> <tr data-bind="if: $root.config.connectDialog.address">
<td>Address</td> <td id="connect-dialog_input_address">Address</td>
<td><input id="address" type="text" data-bind="value: address" required></td> <td><input id="address" type="text" data-bind="value: address" required></td>
</tr> </tr>
<tr data-bind="if: $root.config.connectDialog.port"> <tr data-bind="if: $root.config.connectDialog.port">
<td>Port</td> <td id="connect-dialog_input_port">Port</td>
<td><input id="port" type="text" data-bind="value: port" required></td> <td><input id="port" type="text" data-bind="value: port" required></td>
</tr> </tr>
<tr data-bind="if: $root.config.connectDialog.token">
<td>Token</td>
<td><input id="token" type="text" data-bind="value: token"></td>
</tr>
<tr data-bind="if: $root.config.connectDialog.username"> <tr data-bind="if: $root.config.connectDialog.username">
<td>Username</td> <td id="connect-dialog_input_username">Username</td>
<td><input id="username" type="text" data-bind="value: username" required></td> <td><input id="username" type="text" data-bind="value: username" required></td>
</tr> </tr>
<tr data-bind="if: $root.config.connectDialog.password"> <tr data-bind="if: $root.config.connectDialog.password">
<td>Password</td> <td id="connect-dialog_input_password">Password</td>
<td><input id="password" type="password" data-bind="value: password"></td> <td><input id="password" type="password" data-bind="value: password"></td>
</tr> </tr>
<tr data-bind="if: $root.config.connectDialog.token">
<td id="connect-dialog_input_tokens">Tokens</td>
<td>
<input type="text" data-bind='value: tokenToAdd, valueUpdate: "afterkeydown"'>
</td>
</tr>
<tr data-bind="if: $root.config.connectDialog.token">
<td></td>
<td>
<button id="connect-dialog_controls_remove" class="dialog-submit" type="button" data-bind="enable: selectedTokens().length > 0, click: removeSelectedTokens()">Remove</button>
<button id="connect-dialog_controls_add" class="dialog-submit" type="button" data-bind="enable: tokenToAdd().length > 0, click: addToken()">Add</button>
</td>
</tr>
<tr data-bind="if: $root.config.connectDialog.token, visible: tokens().length > 0">
<td></td>
<td><select id="token" multiple="multiple" height="5" data-bind="options:tokens, selectedOptions:selectedTokens"></select></td>
</tr>
<tr data-bind="if: $root.config.connectDialog.channelName">
<td>Channel</td>
<td><input id="channelName" type="text" data-bind="value: channelName"></td>
</tr>
</table> </table>
<div class="dialog-footer"> <div class="dialog-footer">
<input class="dialog-close" type="button" data-bind="click: hide" value="Cancel"> <input id="connect-dialog_controls_cancel" class="dialog-close" type="button" data-bind="click: hide" value="Cancel">
<input class="dialog-submit" type="submit" value="Connect"> <input id="connect-dialog_controls_connect" class="dialog-submit" type="submit" value="Connect">
</div> </div>
</form> </form>
</div> </div>
@ -76,46 +94,64 @@
</div> </div>
<form data-bind="submit: connect"> <form data-bind="submit: connect">
<table> <table>
<tr> <tr class="reason">
<td colspan=2> <td colspan=2>
<!-- ko if: type() == 0 || type() == 8 --> <!-- ko if: type() == 0 || type() == 8 -->
<span class="refused">
The connection has been refused. The connection has been refused.
</span>
<!-- /ko --> <!-- /ko -->
<!-- ko if: type() == 1 --> <!-- ko if: type() == 1 -->
<span class="version">
The server uses an incompatible version. The server uses an incompatible version.
</span>
<!-- /ko --> <!-- /ko -->
<!-- ko if: type() == 2 --> <!-- ko if: type() == 2 -->
<span class="username">
Your user name was rejected. Maybe try a different one? Your user name was rejected. Maybe try a different one?
</span>
<!-- /ko --> <!-- /ko -->
<!-- ko if: type() == 3 --> <!-- ko if: type() == 3 -->
<span class="userpassword">
The given password is incorrect. The given password is incorrect.
The user name you have chosen requires a special one. The user name you have chosen requires a special one.
</span>
<!-- /ko --> <!-- /ko -->
<!-- ko if: type() == 4 --> <!-- ko if: type() == 4 -->
<span class="serverpassword">
The given password is incorrect. The given password is incorrect.
</span>
<!-- /ko --> <!-- /ko -->
<!-- ko if: type() == 5 --> <!-- ko if: type() == 5 -->
<span class="username-in-use">
The user name you have chosen is already in use. The user name you have chosen is already in use.
</span>
<!-- /ko --> <!-- /ko -->
<!-- ko if: type() == 6 --> <!-- ko if: type() == 6 -->
<span class="full">
The server is full. The server is full.
</span>
<!-- /ko --> <!-- /ko -->
<!-- ko if: type() == 7 --> <!-- ko if: type() == 7 -->
<span class="clientcert">
The server requires you to provide a client certificate The server requires you to provide a client certificate
which is not supported by this web application. which is not supported by this web application.
</span>
<!-- /ko --> <!-- /ko -->
<br> <br>
<span class="server">
The server reports: The server reports:
</span>
<br> <br>
"<span class="connect-error-reason" data-bind="text: reason"></span>" "<span class="connect-error-reason" data-bind="text: reason"></span>"
</td> </td>
</tr> </tr>
<tr data-bind="if: type() == 2 || type() == 3 || type() == 5"> <tr data-bind="if: type() == 2 || type() == 3 || type() == 5">
<td>Username</td> <td class="alternate-username">Username</td>
<td><input id="username" type="text" data-bind="value: username" required></td> <td><input id="username" type="text" data-bind="value: username" required></td>
</tr> </tr>
<tr data-bind="if: type() == 3 || type() == 4"> <tr data-bind="if: type() == 3 || type() == 4">
<td>Password</td> <td class="alternate-password">Password</td>
<td><input id="password" type="password" data-bind="value: password" required></td> <td><input id="password" type="password" data-bind="value: password" required></td>
</tr> </tr>
</table> </table>
@ -266,80 +302,97 @@
<img class="avatar-view" data-bind="visible: avatarView, attr: { src: avatarView }, <img class="avatar-view" data-bind="visible: avatarView, attr: { src: avatarView },
click: function () { avatarView(null) }"></img> click: function () { avatarView(null) }"></img>
<!-- ko with: userContextMenu --> <!-- ko with: userContextMenu -->
<ul class="context-menu" data-bind="if: target, <ul class="context-menu user-context-menu" data-bind="if: target,
style: { left: posX() + 'px', style: { left: posX() + 'px',
top: posY() + 'px' }"> top: posY() + 'px' }">
<!-- ko with: target --> <!-- ko with: target -->
<li data-bind="css: { disabled: !canChangeMute() }"> <li data-bind="css: { disabled: !canChangeMute() }"
class="mute">
Mute Mute
</li> </li>
<li data-bind="css: { disabled: !canChangeDeafen() }"> <li data-bind="css: { disabled: !canChangeDeafen() }"
class="deafen">
Deafen Deafen
</li> </li>
<li data-bind="css: { disabled: !canChangePrioritySpeaker() }"> <li data-bind="css: { disabled: !canChangePrioritySpeaker() }"
class="priority-speaker">
Priority Speaker Priority Speaker
</li> </li>
<li data-bind="css: { disabled: !canLocalMute() }"> <li data-bind="css: { disabled: !canLocalMute() }"
class="local-mute">
Local Mute Local Mute
</li> </li>
<li data-bind="css: { disabled: !canIgnoreMessages() }"> <li data-bind="css: { disabled: !canIgnoreMessages() }"
class="ignore-messages">
Ignore Messages Ignore Messages
</li> </li>
<li data-bind="css: { disabled: !canChangeComment() }, visible: comment"> <li data-bind="css: { disabled: !canChangeComment() }, visible: comment"
class="view-comment">
View Comment View Comment
</li> </li>
<!-- ko if: $data === $root.thisUser() --> <!-- ko if: $data === $root.thisUser() -->
<li data-bind="css: { disabled: !canChangeComment() }, visible: true"> <li data-bind="css: { disabled: !canChangeComment() }, visible: true"
class="change-comment">
Change Comment Change Comment
</li> </li>
<!-- /ko --> <!-- /ko -->
<!-- ko if: $data !== $root.thisUser() --> <!-- ko if: $data !== $root.thisUser() -->
<li data-bind="css: { disabled: !canChangeComment() }, visible: comment"> <li data-bind="css: { disabled: !canChangeComment() }, visible: comment"
class="reset-comment">
Reset Comment Reset Comment
</li> </li>
<!-- /ko --> <!-- /ko -->
<li data-bind="css: { disabled: !canChangeAvatar() }, visible: texture, <li data-bind="css: { disabled: !canChangeAvatar() }, visible: texture,
click: viewAvatar"> click: viewAvatar"
class="view-avatar">
View Avatar View Avatar
</li> </li>
<!-- ko if: $data === $root.thisUser() --> <!-- ko if: $data === $root.thisUser() -->
<li data-bind="css: { disabled: !canChangeAvatar() }, visible: true, <li data-bind="css: { disabled: !canChangeAvatar() }, visible: true,
click: changeAvatar"> click: changeAvatar"
class="change-avatar">
Change Avatar Change Avatar
</li> </li>
<!-- /ko --> <!-- /ko -->
<li data-bind="css: { disabled: !canChangeAvatar() }, visible: texture, <li data-bind="css: { disabled: !canChangeAvatar() }, visible: texture,
click: removeAvatar"> click: removeAvatar",
class="reset-avatar">
Reset Avatar Reset Avatar
</li> </li>
<li data-bind="css: { disabled: true }, visible: true"> <li data-bind="css: { disabled: true }, visible: true"
class="send-message">
Send Message Send Message
</li> </li>
<li data-bind="css: { disabled: true }, visible: true"> <li data-bind="css: { disabled: true }, visible: true"
class="information">
Information Information
</li> </li>
<li data-bind="visible: $data === $root.thisUser(), <li data-bind="visible: $data === $root.thisUser(),
css: { checked: selfMute }, css: { checked: selfMute },
click: toggleMute"> click: toggleMute"
class="self-mute">
Self Mute Self Mute
</li> </li>
<li data-bind="visible: $data === $root.thisUser(), <li data-bind="visible: $data === $root.thisUser(),
css: { checked: selfDeaf }, css: { checked: selfDeaf },
click: toggleDeaf"> click: toggleDeaf"
class="self-deafen">
Self Deafen Self Deafen
</li> </li>
<!-- ko if: $data !== $root.thisUser() --> <!-- ko if: $data !== $root.thisUser() -->
<li data-bind="css: { disabled: true }, visible: true"> <li data-bind="css: { disabled: true }, visible: true"
class="add-friend">
Add Friend Add Friend
</li> </li>
<li data-bind="css: { disabled: true }, visible: false"> <li data-bind="css: { disabled: true }, visible: false"
class="remove-friend">
Remove Friend Remove Friend
</li> </li>
<!-- /ko --> <!-- /ko -->
@ -348,43 +401,53 @@
</ul> </ul>
<!-- /ko --> <!-- /ko -->
<!-- ko with: channelContextMenu --> <!-- ko with: channelContextMenu -->
<ul class="context-menu" data-bind="if: target, <ul class="context-menu channel-context-menu" data-bind="if: target,
style: { left: posX() + 'px', style: { left: posX() + 'px',
top: posY() + 'px' }"> top: posY() + 'px' }">
<!-- ko with: target --> <!-- ko with: target -->
<li data-bind="visible: users.indexOf($root.thisUser()) === -1, <li data-bind="visible: users.indexOf($root.thisUser()) === -1,
css: { disabled: !canJoin() }, css: { disabled: !canJoin() },
click: $root.requestMove.bind($root, $root.thisUser())"> click: $root.requestMove.bind($root, $root.thisUser())"
class="join">
Join Channel Join Channel
</li> </li>
<li data-bind="css: { disabled: !canAdd() }"> <li data-bind="css: { disabled: !canAdd() }"
class="add">
Add Add
</li> </li>
<li data-bind="css: { disabled: !canEdit() }"> <li data-bind="css: { disabled: !canEdit() }"
class="edit">
Edit Edit
</li> </li>
<li data-bind="css: { disabled: !canRemove() }"> <li data-bind="css: { disabled: !canRemove() }"
class="remove">
Remove Remove
</li> </li>
<li data-bind="css: { disabled: !canLink() }"> <li data-bind="css: { disabled: !canLink() }"
class="link">
Link Link
</li> </li>
<li data-bind="css: { disabled: !canUnlink() }"> <li data-bind="css: { disabled: !canUnlink() }"
class="unlink">
Unlink Unlink
</li> </li>
<li data-bind="css: { disabled: !canUnlink() }"> <li data-bind="css: { disabled: !canUnlink() }"
class="unlink-all">
Unlink All Unlink All
</li> </li>
<li data-bind="css: { disabled: true }"> <li data-bind="css: { disabled: true }"
class="copy-mumble-url">
Copy Mumble URL Copy Mumble URL
</li> </li>
<li data-bind="css: { disabled: true }"> <li data-bind="css: { disabled: true }"
class="copy-mumble-web-url">
Copy Mumble-Web URL Copy Mumble-Web URL
</li> </li>
<li data-bind="css: { disabled: !canSendMessage() }"> <li data-bind="css: { disabled: !canSendMessage() }"
class="send-message">
Send Message Send Message
</li> </li>

View file

@ -9,6 +9,7 @@ import _dompurify from 'dompurify'
import keyboardjs from 'keyboardjs' import keyboardjs from 'keyboardjs'
import { ContinuousVoiceHandler, PushToTalkVoiceHandler, VADVoiceHandler, initVoice } from './voice' import { ContinuousVoiceHandler, PushToTalkVoiceHandler, VADVoiceHandler, initVoice } from './voice'
import {initialize as localizationInitialize, translate} from './loc';
const dompurify = _dompurify(window) const dompurify = _dompurify(window)
@ -50,16 +51,31 @@ function ConnectDialog () {
var self = this var self = this
self.address = ko.observable('') self.address = ko.observable('')
self.port = ko.observable('') self.port = ko.observable('')
self.token = ko.observable('') self.tokenToAdd = ko.observable('')
self.selectedTokens = ko.observableArray([])
self.tokens = ko.observableArray([])
self.username = ko.observable('') self.username = ko.observable('')
self.password = ko.observable('') self.password = ko.observable('')
self.channelName = ko.observable('')
self.joinOnly = ko.observable(false) self.joinOnly = ko.observable(false)
self.visible = ko.observable(true) self.visible = ko.observable(true)
self.show = self.visible.bind(self.visible, true) self.show = self.visible.bind(self.visible, true)
self.hide = self.visible.bind(self.visible, false) self.hide = self.visible.bind(self.visible, false)
self.connect = function () { self.connect = function () {
self.hide() self.hide()
ui.connect(self.username(), self.address(), self.port(), self.token(), self.password()) ui.connect(self.username(), self.address(), self.port(), self.tokens(), self.password(), self.channelName())
}
self.addToken = function() {
if ((self.tokenToAdd() != "") && (self.tokens.indexOf(self.tokenToAdd()) < 0)) {
self.tokens.push(self.tokenToAdd())
}
self.tokenToAdd("")
}
self.removeSelectedTokens = function() {
this.tokens.removeAll(this.selectedTokens())
this.selectedTokens([])
} }
} }
@ -329,7 +345,7 @@ class GlobalBindings {
return '[' + new Date().toLocaleTimeString('en-US') + ']' return '[' + new Date().toLocaleTimeString('en-US') + ']'
} }
this.connect = (username, host, port, token, password) => { this.connect = (username, host, port, tokens = [], password, channelName = "") => {
this.resetClient() this.resetClient()
this.remoteHost(host) this.remoteHost(host)
@ -353,7 +369,8 @@ class GlobalBindings {
enabled: true, enabled: true,
mic: this._delayedMicNode.stream, mic: this._delayedMicNode.stream,
audioContext: ctx audioContext: ctx
} },
tokens: tokens
}).done(client => { }).done(client => {
log('Connected!') log('Connected!')
@ -368,11 +385,17 @@ class GlobalBindings {
window.matrixWidget.setAlwaysOnScreen(true) window.matrixWidget.setAlwaysOnScreen(true)
// Register all channels, recursively // Register all channels, recursively
const registerChannel = channel => { if(channelName.indexOf("/") != 0) {
this._newChannel(channel) channelName = "/"+channelName;
channel.children.forEach(registerChannel)
} }
registerChannel(client.root) const registerChannel = (channel, channelPath) => {
this._newChannel(channel)
if(channelPath === channelName) {
client.self.setChannel(channel)
}
channel.children.forEach(ch => registerChannel(ch, channelPath+"/"+ch.name))
}
registerChannel(client.root, "")
// Register all users // Register all users
client.users.forEach(user => this._newUser(user)) client.users.forEach(user => this._newUser(user))
@ -393,6 +416,14 @@ class GlobalBindings {
}) })
}) })
// Log permission denied error messages
client.on('denied', (type) => {
ui.log.push({
type: 'generic',
value: 'Permission denied : '+ type
})
})
// Set own user and root channel // Set own user and root channel
this.thisUser(client.self.__ui) this.thisUser(client.self.__ui)
this.root(client.root.__ui) this.root(client.root.__ui)
@ -772,6 +803,26 @@ class GlobalBindings {
this.requestMove = (user, channel) => { this.requestMove = (user, channel) => {
if (this.connected()) { if (this.connected()) {
user.model.setChannel(channel.model) user.model.setChannel(channel.model)
let currentUrl = url.parse(document.location.href, true)
// delete search param so that query one can be taken into account
delete currentUrl.search
// get full channel path
if( channel.parent() ){ // in case this channel is not Root
let parent = channel.parent()
currentUrl.query.channelName = channel.name()
while( parent.parent() ){
currentUrl.query.channelName = parent.name() + '/' + currentUrl.query.channelName
parent = parent.parent()
}
} else {
// there is no channelName as we moved to Root
delete currentUrl.query.channelName
}
// reflect this change in URL
window.history.pushState(null, channel.name(), url.format(currentUrl))
} }
} }
@ -883,7 +934,7 @@ var ui = new GlobalBindings(window.mumbleWebConfig)
// Used only for debugging // Used only for debugging
window.mumbleUi = ui window.mumbleUi = ui
window.onload = function () { function initializeUI () {
var queryParams = url.parse(document.location.href, true).query var queryParams = url.parse(document.location.href, true).query
queryParams = Object.assign({}, window.mumbleWebConfig.defaults, queryParams) queryParams = Object.assign({}, window.mumbleWebConfig.defaults, queryParams)
var useJoinDialog = queryParams.joinDialog var useJoinDialog = queryParams.joinDialog
@ -901,7 +952,11 @@ window.onload = function () {
useJoinDialog = false useJoinDialog = false
} }
if (queryParams.token) { if (queryParams.token) {
ui.connectDialog.token(queryParams.token) var tokens = queryParams.token
if (!Array.isArray(tokens)) {
tokens = [tokens]
}
ui.connectDialog.tokens(tokens)
} }
if (queryParams.username) { if (queryParams.username) {
ui.connectDialog.username(queryParams.username) ui.connectDialog.username(queryParams.username)
@ -911,6 +966,9 @@ window.onload = function () {
if (queryParams.password) { if (queryParams.password) {
ui.connectDialog.password(queryParams.password) ui.connectDialog.password(queryParams.password)
} }
if (queryParams.channelName) {
ui.connectDialog.channelName(queryParams.channelName)
}
if (queryParams.avatarurl) { if (queryParams.avatarurl) {
// Download the avatar and upload it to the mumble server when connected // Download the avatar and upload it to the mumble server when connected
let url = queryParams.avatarurl let url = queryParams.avatarurl
@ -939,13 +997,11 @@ window.onload = function () {
req.send() req.send()
} }
ui.connectDialog.joinOnly(useJoinDialog) ui.connectDialog.joinOnly(useJoinDialog)
userMediaPromise.then(() => {
ko.applyBindings(ui) ko.applyBindings(ui)
})
}
window.onresize = () => ui.updateSize() window.onresize = () => ui.updateSize()
ui.updateSize() ui.updateSize()
}
function log () { function log () {
console.log.apply(console, arguments) console.log.apply(console, arguments)
@ -996,7 +1052,93 @@ function userToState () {
var voiceHandler var voiceHandler
var testVoiceHandler var testVoiceHandler
var userMediaPromise = initVoice(data => { /**
* @author svartoyg
*/
function translatePiece(selector, kind, parameters, key) {
let element = document.querySelector(selector);
if (element !== null) {
const translation = translate(key);
switch (kind) {
default:
console.warn('unhandled dom translation kind "' + kind + '"');
break;
case 'textcontent':
element.textContent = translation;
break;
case 'attribute':
element.setAttribute(parameters.name || 'value', translation);
break;
}
} else {
console.warn(`translation selector "${selector}" for "${key}" did not match any element`)
}
}
/**
* @author svartoyg
*/
function translateEverything() {
translatePiece('#connect-dialog_title', 'textcontent', {}, 'connectdialog.title');
translatePiece('#connect-dialog_input_address', 'textcontent', {}, 'connectdialog.address');
translatePiece('#connect-dialog_input_port', 'textcontent', {}, 'connectdialog.port');
translatePiece('#connect-dialog_input_username', 'textcontent', {}, 'connectdialog.username');
translatePiece('#connect-dialog_input_password', 'textcontent', {}, 'connectdialog.password');
translatePiece('#connect-dialog_input_tokens', 'textcontent', {}, 'connectdialog.tokens');
translatePiece('#connect-dialog_controls_remove', 'textcontent', {}, 'connectdialog.remove');
translatePiece('#connect-dialog_controls_add', 'textcontent', {}, 'connectdialog.add');
translatePiece('#connect-dialog_controls_cancel', 'attribute', {'name': 'value'}, 'connectdialog.cancel');
translatePiece('#connect-dialog_controls_connect', 'attribute', {'name': 'value'}, 'connectdialog.connect');
translatePiece('.connect-dialog.error-dialog .dialog-header', 'textcontent', {}, 'connectdialog.error.title');
translatePiece('.connect-dialog.error-dialog .reason .refused', 'textcontent', {}, 'connectdialog.error.reason.refused');
translatePiece('.connect-dialog.error-dialog .reason .version', 'textcontent', {}, 'connectdialog.error.reason.version');
translatePiece('.connect-dialog.error-dialog .reason .username', 'textcontent', {}, 'connectdialog.error.reason.username');
translatePiece('.connect-dialog.error-dialog .reason .userpassword', 'textcontent', {}, 'connectdialog.error.reason.userpassword');
translatePiece('.connect-dialog.error-dialog .reason .serverpassword', 'textcontent', {}, 'connectdialog.error.reason.serverpassword');
translatePiece('.connect-dialog.error-dialog .reason .username-in-use', 'textcontent', {}, 'connectdialog.error.reason.username_in_use');
translatePiece('.connect-dialog.error-dialog .reason .full', 'textcontent', {}, 'connectdialog.error.reason.full');
translatePiece('.connect-dialog.error-dialog .reason .clientcert', 'textcontent', {}, 'connectdialog.error.reason.clientcert');
translatePiece('.connect-dialog.error-dialog .reason .server', 'textcontent', {}, 'connectdialog.error.reason.server');
translatePiece('.connect-dialog.error-dialog .alternate-username', 'textcontent', {}, 'connectdialog.username');
translatePiece('.connect-dialog.error-dialog .alternate-password', 'textcontent', {}, 'connectdialog.password');
translatePiece('.connect-dialog.error-dialog .dialog-submit', 'attribute', {'name': 'value'}, 'connectdialog.error.retry');
translatePiece('.connect-dialog.error-dialog .dialog-close', 'attribute', {'name': 'value'}, 'connectdialog.error.cancel');
translatePiece('.join-dialog .dialog-header', 'textcontent', {}, 'joindialog.title');
translatePiece('.join-dialog .dialog-submit', 'attribute', {'name': 'value'}, 'joindialog.connect');
translatePiece('.user-context-menu .mute', 'textcontent', {}, 'contextmenu.mute');
translatePiece('.user-context-menu .deafen', 'textcontent', {}, 'contextmenu.deafen');
translatePiece('.user-context-menu .priority-speaker', 'textcontent', {}, 'usercontextmenu.priority_speaker');
translatePiece('.user-context-menu .local-mute', 'textcontent', {}, 'usercontextmenu.local_mute');
translatePiece('.user-context-menu .ignore-messages', 'textcontent', {}, 'usercontextmenu.ignore_messages');
translatePiece('.user-context-menu .view-comment', 'textcontent', {}, 'usercontextmenu.view_comment');
translatePiece('.user-context-menu .change-comment', 'textcontent', {}, 'usercontextmenu.change_comment');
translatePiece('.user-context-menu .reset-comment', 'textcontent', {}, 'usercontextmenu.reset_comment');
translatePiece('.user-context-menu .view-avatar', 'textcontent', {}, 'usercontextmenu.view_avatar');
translatePiece('.user-context-menu .change-avatar', 'textcontent', {}, 'usercontextmenu.change_avatar');
translatePiece('.user-context-menu .reset-avatar', 'textcontent', {}, 'usercontextmenu.reset_avatar');
translatePiece('.user-context-menu .send-message', 'textcontent', {}, 'usercontextmenu.send_message');
translatePiece('.user-context-menu .information', 'textcontent', {}, 'usercontextmenu.information');
translatePiece('.user-context-menu .self-mute', 'textcontent', {}, 'usercontextmenu.self_mute');
translatePiece('.user-context-menu .self-deafen', 'textcontent', {}, 'usercontextmenu.self_deafen');
translatePiece('.user-context-menu .add-friend', 'textcontent', {}, 'usercontextmenu.add_friend');
translatePiece('.user-context-menu .remove-friend', 'textcontent', {}, 'usercontextmenu.remove_friend');
translatePiece('.channel-context-menu .join', 'textcontent', {}, 'channelcontextmenu.join');
translatePiece('.channel-context-menu .add', 'textcontent', {}, 'channelcontextmenu.add');
translatePiece('.channel-context-menu .edit', 'textcontent', {}, 'channelcontextmenu.edit');
translatePiece('.channel-context-menu .remove', 'textcontent', {}, 'channelcontextmenu.remove');
translatePiece('.channel-context-menu .link', 'textcontent', {}, 'channelcontextmenu.link');
translatePiece('.channel-context-menu .unlink', 'textcontent', {}, 'channelcontextmenu.unlink');
translatePiece('.channel-context-menu .unlink-all', 'textcontent', {}, 'channelcontextmenu.unlink_all');
translatePiece('.channel-context-menu .copy-mumble-url', 'textcontent', {}, 'channelcontextmenu.copy_mumble_url');
translatePiece('.channel-context-menu .copy-mumble-web-url', 'textcontent', {}, 'channelcontextmenu.copy_mumble_web_url');
translatePiece('.channel-context-menu .send-message', 'textcontent', {}, 'channelcontextmenu.send_message');
}
async function main() {
await localizationInitialize(navigator.language);
translateEverything();
try {
const userMedia = await initVoice(data => {
if (testVoiceHandler) { if (testVoiceHandler) {
testVoiceHandler.write(data) testVoiceHandler.write(data)
} }
@ -1008,8 +1150,13 @@ var userMediaPromise = initVoice(data => {
} else if (voiceHandler) { } else if (voiceHandler) {
voiceHandler.write(data) voiceHandler.write(data)
} }
}).then(userMedia => {
ui._micStream = userMedia
}, err => {
window.alert('Failed to initialize user media\nRefresh page to retry.\n' + err)
}) })
ui._micStream = userMedia
} catch (err) {
window.alert('Failed to initialize user media\nRefresh page to retry.\n' + err)
return
}
initializeUI();
}
window.onload = main

99
app/loc.js Normal file
View file

@ -0,0 +1,99 @@
/**
* the default language to use
*
* @var {string}
* @author svartoyg
*/
var _languageDefault = null;
/**
* the fallback language to use
*
* @var {string}
* @author svartoyg
*/
var _languageFallback = null;
/**
* two level map with ISO-639-1 code as first key and translation id as second key
*
* @var {Map<string,Map<string,string>>}
* @author svartoyg
*/
var _data = {};
/**
* @param {string} language
* @return Promise<Map<string,string>>
* @author svartoyg
*/
async function retrieveData (language) {
let json
try {
json = (await import(`../loc/${language}.json`)).default
} catch (exception) {
json = (await import(`../loc/${language.substr(0, language.indexOf('-'))}.json`)).default
}
const map = {}
flatten(json, '', map)
return map
}
function flatten (tree, prefix, result) {
for (const [key, value] of Object.entries(tree)) {
if (typeof value === 'string') {
result[prefix + key] = value
} else {
flatten(value, prefix + key + '.', result)
}
}
}
/**
* @param {string} languageDefault
* @param {string} [languageFallback]
* @author svartoyg
*/
export async function initialize (languageDefault, languageFallback = 'en') {
_languageFallback = languageFallback;
_languageDefault = languageDefault;
for (const language of [_languageFallback, _languageDefault]) {
if (_data.hasOwnProperty(language)) continue;
console.log('--', 'loading localization data for language "' + language + '" ...');
let data;
try {
data = await retrieveData(language);
} catch (exception) {
console.warn(exception.toString());
}
_data[language] = data;
}
}
/**
* gets a translation by its key for a specific language
*
* @param {string} key
* @param {string} [languageChosen]
* @return {string}
* @author svartoyg
*/
export function translate (key, languageChosen = _languageDefault) {
let result = undefined;
for (const language of [languageChosen, _languageFallback]) {
if (_data.hasOwnProperty(language) && (_data[language] !== undefined) && _data[language].hasOwnProperty(key)) {
result = _data[language][key];
break;
}
}
if (result === undefined) {
result = ('{{' + key + '}}');
}
return result;
}

View file

@ -4,8 +4,7 @@ import EventEmitter from 'events'
import { Writable, PassThrough } from 'stream' import { Writable, PassThrough } from 'stream'
import toArrayBuffer from 'to-arraybuffer' import toArrayBuffer from 'to-arraybuffer'
import ByteBuffer from 'bytebuffer' import ByteBuffer from 'bytebuffer'
import webworkify from 'webworkify' import Worker from './worker'
import worker from './worker'
/** /**
* Creates proxy MumbleClients to a real ones running on a web worker. * Creates proxy MumbleClients to a real ones running on a web worker.
@ -13,7 +12,7 @@ import worker from './worker'
*/ */
class WorkerBasedMumbleConnector { class WorkerBasedMumbleConnector {
constructor () { constructor () {
this._worker = webworkify(worker) this._worker = new Worker()
this._worker.addEventListener('message', this._onMessage.bind(this)) this._worker.addEventListener('message', this._onMessage.bind(this))
this._reqId = 1 this._reqId = 1
this._requests = {} this._requests = {}
@ -266,7 +265,7 @@ class WorkerBasedMumbleChannel extends EventEmitter {
_dispatchEvent (name, args) { _dispatchEvent (name, args) {
if (name === 'update') { if (name === 'update') {
let [actor, props] = args let [props] = args
Object.entries(props).forEach((entry) => { Object.entries(props).forEach((entry) => {
this._setProp(entry[0], entry[1]) this._setProp(entry[0], entry[1])
}) })
@ -277,7 +276,6 @@ class WorkerBasedMumbleChannel extends EventEmitter {
props.links = this.links props.links = this.links
} }
args = [ args = [
this._client._user(actor),
props props
] ]
} else if (name === 'remove') { } else if (name === 'remove') {

View file

@ -3,18 +3,11 @@ import mumbleConnect from 'mumble-client-websocket'
import toArrayBuffer from 'to-arraybuffer' import toArrayBuffer from 'to-arraybuffer'
import chunker from 'stream-chunker' import chunker from 'stream-chunker'
import Resampler from 'libsamplerate.js' import Resampler from 'libsamplerate.js'
import CodecsBrowser from 'mumble-client-codecs-browser'
// Polyfill nested webworkers for https://bugs.chromium.org/p/chromium/issues/detail?id=31666 // Polyfill nested webworkers for https://bugs.chromium.org/p/chromium/issues/detail?id=31666
import 'subworkers' import 'subworkers'
// Monkey-patch to allow webworkify-webpack and codecs to work inside of web worker
/* global URL */
window.URL = URL
// Using require to ensure ordering relative to monkey-patch above
let CodecsBrowser = require('mumble-client-codecs-browser')
export default function (self) {
let sampleRate let sampleRate
let nextClientId = 1 let nextClientId = 1
let nextVoiceId = 1 let nextVoiceId = 1
@ -97,17 +90,14 @@ export default function (self) {
function setupChannel (id, channel) { function setupChannel (id, channel) {
id = Object.assign({}, id, { channel: channel.id }) id = Object.assign({}, id, { channel: channel.id })
registerEventProxy(id, channel, 'update', (actor, props) => { registerEventProxy(id, channel, 'update', (props) => {
if (actor) {
actor = actor.id
}
if (props.parent) { if (props.parent) {
props.parent = props.parent.id props.parent = props.parent.id
} }
if (props.links) { if (props.links) {
props.links = props.links.map((it) => it.id) props.links = props.links.map((it) => it.id)
} }
return [actor, props] return [props]
}) })
registerEventProxy(id, channel, 'remove') registerEventProxy(id, channel, 'remove')
@ -194,6 +184,7 @@ export default function (self) {
id = { client: id } id = { client: id }
registerEventProxy(id, client, 'error') registerEventProxy(id, client, 'error')
registerEventProxy(id, client, 'denied', it => [it])
registerEventProxy(id, client, 'newChannel', (it) => [setupChannel(id, it)]) registerEventProxy(id, client, 'newChannel', (it) => [setupChannel(id, it)])
registerEventProxy(id, client, 'newUser', (it) => [setupUser(id, it)]) registerEventProxy(id, client, 'newUser', (it) => [setupUser(id, it)])
registerEventProxy(id, client, 'message', (sender, message, users, channels, trees) => { registerEventProxy(id, client, 'message', (sender, message, users, channels, trees) => {
@ -284,4 +275,5 @@ export default function (self) {
console.error('exception during message event', ev.data, ex) console.error('exception during message event', ev.data, ex)
} }
}) })
}
export default null

15
loc/de.json Normal file
View file

@ -0,0 +1,15 @@
{
"connectdialog": {
"title": "Verbindung herstellen",
"address": "Adresse",
"port": "Port",
"username": "Nutzername",
"password": "Passwort",
"tokens": "Tokens",
"remove": "Entfernen",
"add": "Hinzufügen",
"cancel": "Abbrechen",
"connect": "Verbinden"
}
}

66
loc/en.json Normal file
View file

@ -0,0 +1,66 @@
{
"connectdialog": {
"title": "Connect to Server",
"address": "Address",
"port": "Port",
"username": "Username",
"password": "Password",
"tokens": "Tokens",
"remove": "Remove",
"add": "Add",
"cancel": "Cancel",
"connect": "Connect",
"error": {
"title": "Failed to connect",
"reason": {
"refused": "The connection has been refused.",
"version": "The server uses an incompatible version.",
"username": "Your user name was rejected. Maybe try a different one?",
"userpassword": "The given password is incorrect.\nThe user name you have chosen requires a special one.",
"serverpassword": "The given password is incorrect.",
"username_in_use": "The user name you have chosen is already in use.",
"full": "The server is full.",
"clientcert": "The server requires you to provide a client certificate which is not supported by this web application.",
"server": "The server reports:"
},
"retry": "Retry",
"cancel": "Cancel"
}
},
"joindialog": {
"title": "Mumble Voice Conference",
"connect": "Join Conference"
},
"usercontextmenu": {
"mute": "Mute",
"deafen": "Deafen",
"priority_speaker": "Priority Speaker",
"local_mute": "Local Mute",
"ignore_messages": "Ignore Messages",
"view_comment": "View Comment",
"change_comment": "Change Comment",
"reset_comment": "Reset Comment",
"view_avatar": "View Avatar",
"change_avatar": "Change Avatar",
"reset_avatar": "Reset Avatar",
"send_message": "Send Message",
"information": "Information",
"self_mute": "Self Mute",
"self_deafen": "Self Deafen",
"add_friend": "Add Friend",
"remove_friend": "Remove Friend"
},
"channelcontextmenu": {
"channelcontextmenu.join": "Join Channel",
"channelcontextmenu.add": "Add",
"channelcontextmenu.edit": "Edit",
"channelcontextmenu.remove": "Remove",
"channelcontextmenu.link": "Link",
"channelcontextmenu.unlink": "Unlink",
"channelcontextmenu.unlink_all": "Unlink All",
"channelcontextmenu.copy_mumble_url": "Copy Mumble URL",
"channelcontextmenu.copy_mumble_web_url": "Copy Mumble-Web URL",
"channelcontextmenu.send_message": "Send Message"
}
}

15
loc/eo.json Normal file
View file

@ -0,0 +1,15 @@
{
"connectdialog": {
"title": "Konektado",
"address": "Adreso",
"port": "Pordo",
"username": "Uzantnomo",
"password": "Pasvorto",
"tokens": "Ĵetonoj",
"remove": "Forigi",
"add": "Aldoni",
"cancel": "Nuligi",
"connect": "Konekti"
}
}

65
oc.json Normal file
View file

@ -0,0 +1,65 @@
{
"connectdialog": {
"title": "Connexion al servidor",
"address": "Adreça",
"port": "Pòrt",
"username": "Nom dutilizaire",
"password": "Senhal",
"tokens": "Getons",
"remove": "Suprimir",
"add": "Ajustar",
"cancel": "Anullar",
"connect": "Se connectar",
"error": {
"title": "Connexion impossibla",
"reason": {
"refused": "Lo servidor a refusat la connexion.",
"version": "Lo servidor utiliza una version incompatibla.",
"username": "Vòstre nom dutilizaire es estat regetat. Ensajatz benlèu un autre?",
"userpassword": "Lo senhal donat es incorrèct.\nLo nom dutilizaire quavètz causit requerís un senhal especial.",
"serverpassword": "Lo senhal donat es incorrèct.",
"username_in_use": "Lo nom dutilizaire donat es ja utilizat.",
"full": "Lo servidor es plen.",
"clientcert": "Lo servidor requerís que forniscatz un certificat client ques pas compatible amb aquesta web aplicacion.",
"server": "Lo servidor senhala:"
},
"retry": "Ensajar tornamai",
"cancel": "Anullar"
}
},
"joindialog": {
"title": "Conferéncia àudio Mumble",
"connect": "Participar a la conferéncia"
},
"usercontextmenu": {
"mute": "Copar lo son",
"deafen": "Sordina",
"priority_speaker": "Prioritat parlaire",
"local_mute": "Copar lo son localament",
"ignore_messages": "Ignorar los messatges",
"view_comment": "Veire lo comentari",
"change_comment": "Cambiar lo comentari",
"reset_comment": "Escafar lo comentari",
"view_avatar": "Veire lavatar",
"change_avatar": "Cambiar lavatar",
"reset_avatar": "Escafar Avatar",
"send_message": "Enviar un messatge",
"information": "Informacions",
"self_mute": "Copar mon son",
"self_deafen": "Me metre en sordina",
"add_friend": "Ajustar coma amic",
"remove_friend": "Tirar dels amics"
},
"channelcontextmenu": {
"channelcontextmenu.join": "Rejónher la sala",
"channelcontextmenu.add": "Ajustar",
"channelcontextmenu.edit": "Modificar",
"channelcontextmenu.remove": "Suprimir",
"channelcontextmenu.link": "Associar",
"channelcontextmenu.unlink": "Desassociar",
"channelcontextmenu.unlink_all": "Tot desassociar",
"channelcontextmenu.copy_mumble_url": "Copair lURL Mumble",
"channelcontextmenu.copy_mumble_web_url": "Copiar lURL Mumble-Web",
"channelcontextmenu.send_message": "Enviar messatge"
}
}

8895
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -4,7 +4,8 @@
"description": "An HTML5 Mumble client.", "description": "An HTML5 Mumble client.",
"scripts": { "scripts": {
"build": "webpack && [ -f dist/config.local.js ] || cp app/config.local.js dist/", "build": "webpack && [ -f dist/config.local.js ] || cp app/config.local.js dist/",
"prepublish": "rm -rf dist && npm run build", "watch": "webpack --watch",
"prepare": "rm -rf dist && npm run build",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"author": "Jonas Herzig <me@johni0702.de>", "author": "Jonas Herzig <me@johni0702.de>",
@ -15,37 +16,41 @@
"dist" "dist"
], ],
"devDependencies": { "devDependencies": {
"audio-buffer-utils": "^3.1.2", "@babel/core": "^7.9.0",
"@babel/plugin-transform-runtime": "^7.9.0",
"@babel/preset-env": "^7.9.0",
"@babel/runtime": "^7.9.2",
"audio-buffer-utils": "^5.1.2",
"audio-context": "^1.0.3", "audio-context": "^1.0.3",
"babel-core": "^6.18.2", "babel-loader": "^8.1.0",
"babel-loader": "^6.2.8", "brfs": "^2.0.2",
"babel-plugin-transform-runtime": "^6.15.0",
"babel-preset-es2015": "^6.14.0",
"babel-runtime": "^6.18.0",
"brfs": "^1.4.3",
"bytebuffer": "^5.0.1", "bytebuffer": "^5.0.1",
"css-loader": "^0.26.0", "css-loader": "^3.4.2",
"dompurify": "^0.8.9", "dompurify": "^2.0.8",
"drop-stream": "^1.0.0", "drop-stream": "^1.0.0",
"duplex-maker": "^1.0.0", "duplex-maker": "^1.0.0",
"extract-loader": "^0.1.0", "extract-loader": "^5.0.1",
"file-loader": "^0.9.0", "file-loader": "^4.3.0",
"html-loader": "^0.4.4", "fs": "0.0.1-security",
"json-loader": "^0.5.4", "html-loader": "^0.5.5",
"keyboardjs": "^2.3.4", "json-loader": "^0.5.7",
"knockout": "^3.4.0", "keyboardjs": "^2.5.1",
"knockout": "^3.5.1",
"lodash.assign": "^4.2.0", "lodash.assign": "^4.2.0",
"microphone-stream": "^3.0.5", "microphone-stream": "^5.0.1",
"node-sass": "^4.9.3", "mumble-client": "github:johni0702/mumble-client#ddf8424",
"raw-loader": "^0.5.1", "mumble-client-websocket": "^1.0.0",
"regexp-replace-loader": "0.0.1", "node-sass": "^4.13.1",
"sass-loader": "^4.1.1", "raw-loader": "^4.0.0",
"regexp-replace-loader": "1.0.1",
"sass-loader": "^8.0.2",
"stream-chunker": "^1.2.8", "stream-chunker": "^1.2.8",
"to-arraybuffer": "^1.0.1", "to-arraybuffer": "^1.0.1",
"transform-loader": "^0.2.3", "transform-loader": "^0.2.4",
"voice-activity-detection": "johni0702/voice-activity-detection#9f8bd90", "voice-activity-detection": "johni0702/voice-activity-detection#9f8bd90",
"webpack": "^1.13.3", "webpack": "^4.42.1",
"mumble-client-websocket": "^1.0.0", "webpack-cli": "^3.3.11",
"mumble-client": "github:johni0702/mumble-client#ddf8424" "worker-loader": "^2.0.0"
} },
"optionalDependencies": {}
} }

View file

@ -0,0 +1,183 @@
diff --git a/node_modules/mumble-client-codecs-browser/lib/decode-worker.js b/node_modules/mumble-client-codecs-browser/lib/decode-worker.js
index 3925f29..be9af92 100644
--- a/node_modules/mumble-client-codecs-browser/lib/decode-worker.js
+++ b/node_modules/mumble-client-codecs-browser/lib/decode-worker.js
@@ -1,10 +1,6 @@
'use strict';
-Object.defineProperty(exports, "__esModule", {
- value: true
-});
-exports.default = function (self) {
var opusDecoder, celt7Decoder;
self.addEventListener('message', function (e) {
var data = e.data;
@@ -55,10 +51,12 @@ exports.default = function (self) {
}, [_decoded.buffer]);
}
});
-};
+
var _libopus = require('libopus.js');
var _libcelt = require('libcelt7.js');
var MUMBLE_SAMPLE_RATE = 48000;
+
+export default null
\ No newline at end of file
diff --git a/node_modules/mumble-client-codecs-browser/lib/decoder-stream.js b/node_modules/mumble-client-codecs-browser/lib/decoder-stream.js
index 6cfda8b..28a9549 100644
--- a/node_modules/mumble-client-codecs-browser/lib/decoder-stream.js
+++ b/node_modules/mumble-client-codecs-browser/lib/decoder-stream.js
@@ -1,9 +1,5 @@
'use strict';
-Object.defineProperty(exports, "__esModule", {
- value: true
-});
-
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; }; }();
var _stream = require('stream');
@@ -12,17 +8,11 @@ var _reusePool = require('reuse-pool');
var _reusePool2 = _interopRequireDefault(_reusePool);
-var _webworkify = require('webworkify');
-
-var _webworkify2 = _interopRequireDefault(_webworkify);
-
var _toArraybuffer = require('to-arraybuffer');
var _toArraybuffer2 = _interopRequireDefault(_toArraybuffer);
-var _decodeWorker = require('./decode-worker');
-
-var _decodeWorker2 = _interopRequireDefault(_decodeWorker);
+import DecodeWorker from './decode-worker';
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
@@ -33,7 +23,7 @@ function _possibleConstructorReturn(self, call) { if (!self) { throw new Referen
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; }
var pool = (0, _reusePool2.default)(function () {
- return (0, _webworkify2.default)(_decodeWorker2.default);
+ return new DecodeWorker();
});
// Prepare first worker
pool.recycle(pool.get());
@@ -48,11 +38,6 @@ var DecoderStream = function (_Transform) {
_this._worker = pool.get();
_this._worker.onmessage = function (msg) {
- if (_this._worker.objectURL) {
- // The object URL can now be revoked as the worker has been loaded
- window.URL.revokeObjectURL(_this._worker.objectURL);
- _this._worker.objectURL = null;
- }
_this._onMessage(msg.data);
};
return _this;
@@ -112,4 +97,5 @@ var DecoderStream = function (_Transform) {
return DecoderStream;
}(_stream.Transform);
-exports.default = DecoderStream;
\ No newline at end of file
+//exports.default = DecoderStream;
+export default DecoderStream
\ No newline at end of file
diff --git a/node_modules/mumble-client-codecs-browser/lib/encode-worker.js b/node_modules/mumble-client-codecs-browser/lib/encode-worker.js
index f7187ab..c2ebaa3 100644
--- a/node_modules/mumble-client-codecs-browser/lib/encode-worker.js
+++ b/node_modules/mumble-client-codecs-browser/lib/encode-worker.js
@@ -1,10 +1,6 @@
'use strict';
-Object.defineProperty(exports, "__esModule", {
- value: true
-});
-exports.default = function (self) {
var opusEncoder, celt7Encoder;
var bitrate;
self.addEventListener('message', function (e) {
@@ -70,7 +66,7 @@ exports.default = function (self) {
}, [_buffer]);
}
});
-};
+
var _libopus = require('libopus.js');
@@ -83,3 +79,5 @@ var _toArraybuffer2 = _interopRequireDefault(_toArraybuffer);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var MUMBLE_SAMPLE_RATE = 48000;
+
+export default null
\ No newline at end of file
diff --git a/node_modules/mumble-client-codecs-browser/lib/encoder-stream.js b/node_modules/mumble-client-codecs-browser/lib/encoder-stream.js
index 021f131..eeb9189 100644
--- a/node_modules/mumble-client-codecs-browser/lib/encoder-stream.js
+++ b/node_modules/mumble-client-codecs-browser/lib/encoder-stream.js
@@ -1,9 +1,5 @@
'use strict';
-Object.defineProperty(exports, "__esModule", {
- value: true
-});
-
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; }; }();
var _stream = require('stream');
@@ -12,13 +8,7 @@ var _reusePool = require('reuse-pool');
var _reusePool2 = _interopRequireDefault(_reusePool);
-var _webworkify = require('webworkify');
-
-var _webworkify2 = _interopRequireDefault(_webworkify);
-
-var _encodeWorker = require('./encode-worker');
-
-var _encodeWorker2 = _interopRequireDefault(_encodeWorker);
+import EncodeWorker from './encode-worker'
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
@@ -29,7 +19,7 @@ function _possibleConstructorReturn(self, call) { if (!self) { throw new Referen
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; }
var pool = (0, _reusePool2.default)(function () {
- return (0, _webworkify2.default)(_encodeWorker2.default);
+ return new EncodeWorker();
});
// Prepare first worker
pool.recycle(pool.get());
@@ -46,11 +36,6 @@ var EncoderStream = function (_Transform) {
_this._worker = pool.get();
_this._worker.onmessage = function (msg) {
- if (_this._worker.objectURL) {
- // The object URL can now be revoked as the worker has been loaded
- window.URL.revokeObjectURL(_this._worker.objectURL);
- _this._worker.objectURL = null;
- }
_this._onMessage(msg.data);
};
return _this;
@@ -96,4 +81,5 @@ var EncoderStream = function (_Transform) {
return EncoderStream;
}(_stream.Transform);
-exports.default = EncoderStream;
\ No newline at end of file
+//exports.default = EncoderStream;
+export default EncoderStream
\ No newline at end of file

View file

@ -110,7 +110,7 @@ html, body {
height: 19px; height: 19px;
} }
.branch { .branch {
float: left; position: absolute;
padding-top: 3px; padding-top: 3px;
padding-bottom: 3px; padding-bottom: 3px;
background-color: $channels-bg-color; background-color: $channels-bg-color;
@ -129,7 +129,7 @@ html, body {
} }
.channel-tree, .channel-tree,
.user-tree { .user-tree {
float: left; position: absolute;
} }
.channel-tree::before, .channel-tree::before,
.user-tree::before { .user-tree::before {
@ -432,10 +432,10 @@ form {
height: 100%; height: 100%;
} }
.join-dialog { .join-dialog {
width: 100%; width: 300px;
height: 100%; height: 100px;
top: 0px; top: 50%;
left: 0px; left: 50%;
} }
.join-dialog .dialog-submit { .join-dialog .dialog-submit {
float: none; float: none;
@ -444,7 +444,7 @@ form {
top: calc(50% - 10px); top: calc(50% - 10px);
left: calc(50% - 100px); left: calc(50% - 100px);
} }
.connect-dialog input[type=text] { .connect-dialog input[type=text], select {
font-size: 15px; font-size: 15px;
border: 1px $dialog-input-border-color solid; border: 1px $dialog-input-border-color solid;
border-radius: 3px; border-radius: 3px;

View file

@ -1,8 +1,8 @@
var theme = 'MetroMumbleLight' var theme = '../themes/MetroMumbleLight'
var path = require('path');
var path = require('path')
module.exports = { module.exports = {
mode: 'development',
entry: { entry: {
index: [ index: [
'./app/index.js', './app/index.js',
@ -12,42 +12,47 @@ module.exports = {
theme: './app/theme.js', theme: './app/theme.js',
matrix: './app/matrix.js' matrix: './app/matrix.js'
}, },
devtool: "cheap-source-map",
output: { output: {
filename: '[name].js', path: path.join(__dirname, 'dist'),
path: './dist' chunkFilename: '[chunkhash].js',
filename: '[name].js'
}, },
module: { module: {
postLoaders: [ rules: [
{
include: /mumble-streams\/lib\/data.js/,
loader: 'transform-loader?brfs'
}
],
loaders: [
{ {
test: /\.js$/, test: /\.js$/,
exclude: /node_modules/, exclude: /node_modules/,
use: {
loader: 'babel-loader', loader: 'babel-loader',
query: { options: {
presets: ['es2015'], presets: ['@babel/preset-env'],
plugins: ['transform-runtime'] plugins: ['@babel/plugin-transform-runtime']
}
} }
}, },
{ {
test: /\.html$/, test: /\.html$/,
loaders: [ use: [
'file-loader?name=[name].[ext]', {
'extract-loader', loader: 'file-loader',
'html-loader?' + JSON.stringify({ options: { 'name': '[name].[ext]' }
},
{
loader: "extract-loader"
},
{
loader: 'html-loader',
options: {
attrs: ['img:src', 'link:href'], attrs: ['img:src', 'link:href'],
interpolate: 'require',
root: theme root: theme
}) }
}
] ]
}, },
{ {
test: /\.css$/, test: /\.css$/,
loaders: [ use: [
'file-loader', 'file-loader',
'extract-loader', 'extract-loader',
'css-loader' 'css-loader'
@ -55,7 +60,7 @@ module.exports = {
}, },
{ {
test: /\.scss$/, test: /\.scss$/,
loaders: [ use: [
'file-loader?name=[hash].css', 'file-loader?name=[hash].css',
'extract-loader', 'extract-loader',
'css-loader', 'css-loader',
@ -63,45 +68,42 @@ module.exports = {
] ]
}, },
{ {
type: 'javascript/auto',
test: /manifest\.json$|\.xml$/, test: /manifest\.json$|\.xml$/,
loaders: [ use: [
'file-loader', 'file-loader',
'extract-loader', 'extract-loader',
'regexp-replace-loader?' + JSON.stringify({ {
loader: 'regexp-replace-loader',
options: {
match: { match: {
pattern: "#require\\('([^']*)'\\)", pattern: "#require\\('([^']*)'\\)",
flags: 'g' flags: 'g'
}, },
replaceWith: '"+require("$1")+"' replaceWith: '"+require("$1")+"'
}), }
},
'raw-loader' 'raw-loader'
] ]
}, },
{ {
test: /\.json$/, test: /\.(svg|png|ico)$/,
exclude: /manifest\.json$/, use: [
loader: 'json-loader' 'file-loader'
]
}, },
{ {
test: /\.(svg|png|ico)$/, test: /worker\.js$/,
loader: 'file-loader' use: { loader: 'worker-loader' }
},
{
enforce: 'post',
test: /mumble-streams\/lib\/data.js/,
use: [
'transform-loader?brfs'
]
} }
] ]
}, },
resolve: { target: 'web'
alias: {
webworkify: 'webworkify-webpack'
},
root: [
path.resolve('./themes/')
]
},
includes: {
pattern: function (filepath) {
return {
re: /#require\((.+)\)/,
index: 1
}
}
}
} }