Files
docker-steam-headless/overlay/opt/noVNC/audio.patch
Josh.5 6340f4113e Add a Nginx reverse proxy to combine all the used ports into one single port
This will allow this web UI to sit nicely behind a reverse proxy.

Dynamically set the ports used for the
- VNC service
- noVNC service
- Pulseaudio socket
- VNC audio transport websocket

Remove all mentions of the above used ports in the Dockerfile.
2022-10-04 09:41:14 +13:00

417 lines
14 KiB
Diff

diff --git a/app/styles/base.css b/app/styles/base.css
index adad415..dfefa6a 100644
--- a/app/styles/base.css
+++ b/app/styles/base.css
@@ -683,7 +683,7 @@ select:active {
#noVNC_setting_port {
width: 80px;
}
-#noVNC_setting_path {
+#noVNC_setting_path #noVNC_setting_apath {
width: 100px;
}
diff --git a/app/ui.js b/app/ui.js
index cb6a9fd..4d599e1 100644
--- a/app/ui.js
+++ b/app/ui.js
@@ -15,6 +15,7 @@ import KeyTable from "../core/input/keysym.js";
import keysyms from "../core/input/keysymdef.js";
import Keyboard from "../core/input/keyboard.js";
import RFB from "../core/rfb.js";
+import WebAudio from "../core/webaudio.js";
import * as WebUtil from "./webutil.js";
const PAGE_TITLE = "noVNC";
@@ -40,6 +41,7 @@ const UI = {
inhibitReconnect: true,
reconnectCallback: null,
reconnectPassword: null,
+ webaudio: null,
prime() {
return WebUtil.initSettings().then(() => {
@@ -171,6 +173,7 @@ const UI = {
UI.initSetting('compression', 2);
UI.initSetting('shared', true);
UI.initSetting('view_only', false);
+ UI.initSetting('audio', true);
UI.initSetting('show_dot', false);
UI.initSetting('path', 'websockify');
UI.initSetting('repeaterID', '');
@@ -200,6 +203,20 @@ const UI = {
}
},
+ toggleAudio() {
+ console.log('here');
+ const audio = UI.getSetting('audio');
+ if (audio) {
+ UI.webaudio.start();
+ } else {
+ if(UI.webaudio !== null) {
+ if (UI.webaudio.connected) {
+ UI.webaudio.stop();
+ }
+ }
+ }
+ },
+
/* ------^-------
* /INIT
* ==============
@@ -356,6 +373,8 @@ const UI = {
UI.addSettingChangeHandler('shared');
UI.addSettingChangeHandler('view_only');
UI.addSettingChangeHandler('view_only', UI.updateViewOnly);
+ UI.addSettingChangeHandler('audio');
+ UI.addSettingChangeHandler('audio', UI.updateEnableAudio);
UI.addSettingChangeHandler('show_dot');
UI.addSettingChangeHandler('show_dot', UI.updateShowDotCursor);
UI.addSettingChangeHandler('host');
@@ -841,6 +860,7 @@ const UI = {
UI.updateSetting('compression');
UI.updateSetting('shared');
UI.updateSetting('view_only');
+ UI.updateSetting('audio');
UI.updateSetting('path');
UI.updateSetting('repeaterID');
UI.updateSetting('logging');
@@ -1041,6 +1061,7 @@ const UI = {
UI.rfb.resizeSession = UI.getSetting('resize') === 'remote';
UI.rfb.qualityLevel = parseInt(UI.getSetting('quality'));
UI.rfb.compressionLevel = parseInt(UI.getSetting('compression'));
+ UI.rfb.enableAudio = UI.getSetting('audio');
UI.rfb.showDotCursor = UI.getSetting('show_dot');
UI.updateViewOnly(); // requires UI.rfb
@@ -1056,6 +1077,10 @@ const UI = {
UI.updateVisualState('disconnecting');
+ if(UI.webaudio !== null && UI.webaudio.socket !== null) {
+ UI.webaudio.socket.close();
+ }
+
// Don't display the connection settings until we're actually disconnected
},
@@ -1097,6 +1122,19 @@ const UI = {
// Do this last because it can only be used on rendered elements
UI.rfb.focus();
+
+ let audio_url;
+ // let host = window.location.hostname;
+ // let port = '<PORT_AUDIO_WEBSOCKET>';
+ if (window.location.protocol === "https:") {
+ audio_url = 'wss';
+ } else {
+ audio_url = 'ws';
+ }
+ audio_url += '://' + window.location.host + '/audiowebsock';
+
+ UI.webaudio = new WebAudio(audio_url);
+ UI.toggleAudio();
},
disconnectFinished(e) {
@@ -1647,6 +1685,12 @@ const UI = {
}
},
+ updateEnableAudio() {
+ if (!UI.rfb) return;
+ UI.rfb.enableAudio = UI.getSetting('audio');
+ UI.toggleAudio();
+ },
+
updateShowDotCursor() {
if (!UI.rfb) return;
UI.rfb.showDotCursor = UI.getSetting('show_dot');
diff --git a/core/webaudio.js b/core/webaudio.js
new file mode 100644
index 0000000..28c71ac
--- /dev/null
+++ b/core/webaudio.js
@@ -0,0 +1,142 @@
+export default class WebAudio {
+ constructor(url) {
+ this.url = url
+
+ this.connected = false;
+
+ //constants for audio behavoir
+ this.maximumAudioLag = 1.5; //amount of seconds we can potentially be behind the server audio stream
+ this.syncLagInterval = 5000; //check every x milliseconds if we are behind the server audio stream
+ this.updateBufferEvery = 20; //add recieved data to the player buffer every x milliseconds
+ this.reduceBufferInterval = 500; //trim the output audio stream buffer every x milliseconds so we don't overflow
+ this.maximumSecondsOfBuffering = 1; //maximum amount of data to store in the play buffer
+ this.connectionCheckInterval = 500; //check the connection every x milliseconds
+
+ //register all our background timers. these need to be created only once - and will run independent of the object's streams/properties
+ this.updateCheck = null;
+ this.syncCheck = null;
+ this.reduceCheck = null;
+ this.ConnCheck = null;
+
+ }
+
+ //registers all the event handlers for when this stream is closed - or when data arrives.
+ registerHandlers() {
+ this.mediaSource.addEventListener('sourceended', e => this.socketDisconnected(e))
+ this.mediaSource.addEventListener('sourceclose', e => this.socketDisconnected(e))
+ this.mediaSource.addEventListener('error', e => this.socketDisconnected(e))
+ this.buffer.addEventListener('error', e => this.socketDisconnected(e))
+ this.buffer.addEventListener('abort', e => this.socketDisconnected(e))
+ }
+
+ //starts the web audio stream. only call this method on button click.
+ start() {
+ if (!!this.connected) return;
+ if (!!this.audio) this.audio.remove();
+ this.queue = null;
+
+ if (this.updateCheck === null) this.updateCheck = setInterval(() => this.updateQueue(), this.updateBufferEvery);
+ if (this.syncCheck === null) this.syncCheck = setInterval(() => this.syncInterval(), this.syncLagInterval);
+ if (this.reduceCheck === null) this.reduceCheck = setInterval(() => this.reduceBuffer(), this.reduceBufferInterval);
+ if (this.ConnCheck === null) this.ConnCheck = setInterval(() => this.tryLastPacket(), this.connectionCheckInterval);
+
+ this.mediaSource = new MediaSource()
+ this.mediaSource.addEventListener('sourceopen', e => this.onSourceOpen())
+ //first we need a media source - and an audio object that contains it.
+ this.audio = document.createElement('audio');
+ this.audio.src = window.URL.createObjectURL(this.mediaSource);
+
+ //start our stream - we can only do this on user input
+ this.audio.play();
+ }
+
+ stop() {
+ // Clear all interval timers
+ clearInterval(this.updateCheck);
+ clearInterval(this.syncCheck);
+ clearInterval(this.reduceCheck);
+ clearInterval(this.ConnCheck);
+ // Close the socket
+ this.socket.close();
+ this.connected = false;
+ // Reset timers to null
+ this.updateCheck = null;
+ this.syncCheck = null;
+ this.reduceCheck = null;
+ this.ConnCheck = null;
+ }
+
+ wsConnect() {
+ if (!!this.socket) this.socket.close();
+
+ this.socket = new WebSocket(this.url, ['binary', 'base64'])
+ this.socket.binaryType = 'arraybuffer'
+ this.socket.addEventListener('message', e => this.websocketDataArrived(e), false);
+ }
+
+ //this is called when the media source contains data
+ onSourceOpen(e) {
+ this.buffer = this.mediaSource.addSourceBuffer('audio/webm; codecs="opus"')
+ this.registerHandlers();
+ this.wsConnect();
+ }
+
+ //whenever data arrives in our websocket this is called.
+ websocketDataArrived(e) {
+ this.lastPacket = Date.now();
+ this.connected = true;
+ this.queue = this.queue == null ? e.data : this.concat(this.queue, e.data);
+ }
+
+ //whenever a disconnect happens this is called.
+ socketDisconnected(e) {
+ console.log(e);
+ this.connected = false;
+ }
+
+ tryLastPacket() {
+ if (this.lastPacket == null) return;
+ if ((Date.now() - this.lastPacket) > 1000) {
+ this.socketDisconnected('timeout');
+ }
+ }
+
+ //this updates the buffer with the data from our queue
+ updateQueue() {
+ if (!(!!this.queue && !!this.buffer && !this.buffer.updating)) {
+ return;
+ }
+
+ this.buffer.appendBuffer(this.queue);
+ this.queue = null;
+ }
+
+ //reduces the stream buffer to the minimal size that we need for streaming
+ reduceBuffer() {
+ if (!(this.buffer && !this.buffer.updating && !!this.audio && !!this.audio.currentTime && this.audio.currentTime > 1)) {
+ return;
+ }
+
+ this.buffer.remove(0, this.audio.currentTime - 1);
+ }
+
+ //synchronizes the current time of the stream with the server
+ syncInterval() {
+ if (!(this.audio && this.audio.currentTime && this.audio.currentTime > 1 && this.buffer && this.buffer.buffered && this.buffer.buffered.length > 1)) {
+ return;
+ }
+
+ var currentTime = this.audio.currentTime;
+ var targetTime = this.buffer.buffered.end(this.buffer.buffered.length - 1);
+
+ if (targetTime > (currentTime + this.maximumAudioLag)) this.audio.fastSeek(targetTime);
+ }
+
+ //joins two data arrays - helper function
+ concat(buffer1, buffer2) {
+ var tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
+ tmp.set(new Uint8Array(buffer1), 0);
+ tmp.set(new Uint8Array(buffer2), buffer1.byteLength);
+ return tmp.buffer;
+ };
+}
diff --git a/vnc.html b/vnc.html
index c678c2a..d1652a0 100644
--- a/vnc.html
+++ b/vnc.html
@@ -170,6 +170,10 @@
<label><input id="noVNC_setting_view_only" type="checkbox"> View Only</label>
</li>
<li><hr></li>
+ <li>
+ <label><input id="noVNC_setting_audio" type="checkbox"> Enable Audio</label>
+ </li>
+ <li><hr></li>
<li>
<label><input id="noVNC_setting_view_clip" type="checkbox"> Clip to Window</label>
</li>
diff --git a/vnc_lite.html b/vnc_lite.html
index 8e2f5cb..d3cf6ab 100644
--- a/vnc_lite.html
+++ b/vnc_lite.html
@@ -41,6 +41,15 @@
#status {
text-align: center;
}
+ #toggleAudioButton {
+ position: fixed;
+ top: 0px;
+ left: 0px;
+ border: 1px outset;
+ padding: 5px 5px 4px 5px;
+ cursor: pointer;
+ display: none;
+ }
#sendCtrlAltDelButton {
position: fixed;
top: 0px;
@@ -60,18 +69,38 @@
<script type="module" crossorigin="anonymous">
// RFB holds the API to connect and communicate with a VNC server
import RFB from './core/rfb.js';
+ import WebAudio from './core/webaudio.js';
let rfb;
let desktopName;
+ let wa;
// When this function is called we have
// successfully connected to a server
function connectedToServer(e) {
+ let audio_url;
+ // let host = window.location.hostname;
+ // let port = '<PORT_AUDIO_WEBSOCKET>';
+ if (window.location.protocol === "https:") {
+ audio_url = 'wss';
+ } else {
+ audio_url = 'ws';
+ }
+ audio_url += '://' + window.location.host + '/audiowebsock';
+
+ wa = new WebAudio(audio_url);
+ document.getElementById('toggleAudioButton').style.display = "block";
+
status("Connected to " + desktopName);
}
// This function is called when we are disconnected
function disconnectedFromServer(e) {
+ if(wa !== null) {
+ wa.stop();
+ wa = null;
+ document.getElementById('toggleAudioButton').style.display = "none";
+ }
if (e.detail.clean) {
status("Disconnected");
} else {
@@ -100,6 +129,19 @@
return false;
}
+ // Enable the audio websocket
+ function toggleAudio() {
+ if (wa.connected) {
+ console.log('Stopping audio...')
+ wa.stop();
+ document.getElementById('toggleAudioButton').innerText = "Enable Audio";
+ } else {
+ console.log('Starting audio websocket on: ' + audio_url)
+ wa.start();
+ document.getElementById('toggleAudioButton').innerText = "Disable Audio";
+ }
+ }
+
// Show a status text in the top bar
function status(text) {
document.getElementById('status').textContent = text;
@@ -135,12 +177,16 @@
document.getElementById('sendCtrlAltDelButton')
.onclick = sendCtrlAltDel;
+ document.getElementById('toggleAudioButton')
+ .onclick = toggleAudio;
+
// Read parameters specified in the URL query string
// By default, use the host and port of server that served this file
const host = readQueryVariable('host', window.location.hostname);
let port = readQueryVariable('port', window.location.port);
const password = readQueryVariable('password');
const path = readQueryVariable('path', 'websockify');
+ const audio_path = readQueryVariable('audio_path', '');
// | | | | | |
// | | | Connect | | |
@@ -150,6 +196,7 @@
// Build the websocket URL used to connect
let url;
+ let audio_url;
if (window.location.protocol === "https:") {
url = 'wss';
} else {
@@ -159,6 +206,9 @@
if(port) {
url += ':' + port;
}
+ if(audio_path !== '') {
+ audio_url = url + '/' + audio_path;
+ }
url += '/' + path;
// Creating a new RFB object will start a new connection
@@ -179,6 +229,7 @@
<body>
<div id="top_bar">
+ <div id="toggleAudioButton">Enable Audio</div>
<div id="status">Loading</div>
<div id="sendCtrlAltDelButton">Send CtrlAltDel</div>
</div>