Initial commit

This commit is contained in:
Thomas Lovén 2024-12-09 14:55:52 +01:00
commit 5d2dd6c946
8 changed files with 304 additions and 0 deletions

69
Dockerfile Normal file
View File

@ -0,0 +1,69 @@
FROM ubuntu
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive \
apt-get install -y --no-install-recommends\
bash \
sudo \
git \
procps
RUN mkdir -p /opt/noVNC
RUN adduser --home /home/novnc --shell /bin/bash --system --disabled-password novnc \
&& echo "novnc ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
# X11 and xfce
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive \
apt-get install -y --no-install-recommends\
xvfb xauth dbus-x11 xfce4 xfce4-terminal \
x11-xserver-utils
# VNC
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive \
apt-get install -y --no-install-recommends\
python3 python3-pip \
tigervnc-standalone-server tigervnc-common \
openssl \
&& pip3 install numpy
# NoVNC
RUN git clone --single-branch https://github.com/novnc/noVNC.git /opt/noVNC \
&& git clone --single-branch https://github.com/novnc/websockify.git /opt/noVNC/utils/websockify \
&& ln -s /opt/noVNC/vnc.html /opt/noVNC/index.html
RUN openssl req -batch -new -x509 -days 365 -nodes -out self.pem -keyout /opt/noVNC/utils/websockify/self.pem
# Audio
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive \
apt-get install -y --no-install-recommends\
pulseaudio \
pavucontrol \
ucspi-tcp \
gstreamer1.0-plugins-good \
gstreamer1.0-pulseaudio \
gstreamer1.0-tools
COPY pulse/ /etc/pulse
COPY novnc /opt/noVNC/
RUN sed -i "/import RFB/a \
import '../webaudio.js'" \
/opt/noVNC/app/ui.js
# Extra applications
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive \
apt-get install -y --no-install-recommends\
firefox neovim
COPY entrypoint.sh /opt/noVNC/entrypoint.sh
ENTRYPOINT ["/opt/noVNC/entrypoint.sh"]
EXPOSE 8080
USER novnc
WORKDIR /home/novnc

24
README.md Normal file
View File

@ -0,0 +1,24 @@
# NOVNC-BASE
A desktop environment with sound in docker
Can be used as a base file for application specific containers.
E.g:
```dockerfile
FROM thomasloven/novnc-base
RUN sudo apt-get update \
&& DEBIAN_FRONTEND=noninteractive \
sudo apt-get install -y blender
CMD ["blender"]
```
### Bonus functionality - dotfiles installation.
If the environment variable `DOTFILES_REPO` is set, the container will `git
clone` that into `~/dotfiles` and then run `~/dotfiles/install.sh` if it
exists.

57
entrypoint.sh Executable file
View File

@ -0,0 +1,57 @@
#! /bin/bash
shopt -s nullglob
kill_pid() {
if [ -f $1 ]; then
cat $1 | xargs kill > /dev/null 2>&1
rm -f $1
fi
}
# Perform cleanup and remove X1 lock files if the container was restarted
rm -f /tmp/.X1-lock /tmp/.X11-unix/X1
kill_pid ~/.vnc-pid
kill_pid ~/.pa-pid
kill_pid ~/.tcp-pid
kill_pid ~/.ws-pid
# Clone and install dotfiles if DOTFILES_REPO is defined
if [ -n "$DOTFILES_REPO" ]; then
if [ ! -d ~/dotfiles ]; then
git clone $DOTFILES_REPO ~/dotfiles
if [ -f ~/dotfiles/install.sh ]; then
/bin/bash ~/dotfiles/install.sh
fi
fi
fi
# Launch VNC server - view :1 defaults to port 5901
vncserver :1 -SecurityTypes None -localhost no --I-KNOW-THIS-IS-INSECURE &
echo "$!" > ~/.vnc-pid
# Launch pulseaudio server
# /etc/pulse/client.conf and /etc/pulse/default.pa are setup to make a default
# audio sink which outputs to a socket at /tmp/pulseaudio.socket
DISPLAY=:0.0 pulseaudio --disallow-module-loading --disallow-exit --exit-idle-time=-1&
echo "$!" > ~/.pa-pid
# Use gstreamer to stream the pulseaudio source /tmp/pulseaudio.socket to stdout (fd=1)
# the tcpserver from ucspi-tcp pipes this to tcp port 6901
tcpserver localhost 6901 gst-launch-1.0 -q pulsesrc server=/tmp/pulseaudio.socket ! audio/x-raw, channels=2, rate=12000 ! cutter ! opusenc ! webmmux ! fdsink fd=1 &
echo "$!" > ~/.tcp-pid
# Websockify does three things:
# - publishes /opt/noVNC to http port 8080
# - proxies vnc port 5901 to 8080/websockify?token=vnc
# - proxies pulseaudio port 6901 to 8080/websockify?token=pulse
# The latter two are defined through the tokenfile
/opt/noVNC/utils/websockify/websockify.py --web /opt/noVNC 8080 --token-plugin=TokenFile --token-source=/opt/noVNC/tokenfile &
echo "$!" > ~/.ws-pid
if [ -n "$@" ]; then
DISPLAY=:1.0 exec "$@" &
fi
wait

6
novnc/defaults.json Normal file
View File

@ -0,0 +1,6 @@
{
"autoconnect": true,
"reconnect": true,
"path": "websockify?token=vnc",
"resize": "remote"
}

2
novnc/tokenfile Normal file
View File

@ -0,0 +1,2 @@
vnc: localhost:5901
pulse: localhost:6901

142
novnc/webaudio.js Normal file
View File

@ -0,0 +1,142 @@
class WebAudio {
constructor(url=null) {
if (url !== null)
this.url = url
else {
if (window.location.protocol === "https:")
this.url = "wss"
else
this.url = "ws"
this.url += "://" + window.location.host + "/websockify?token=pulse"
}
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
setInterval(() => this.updateQueue(), this.updateBufferEvery);
setInterval(() => this.syncInterval(), this.syncLagInterval);
setInterval(() => this.reduceBuffer(), this.reduceBufferInterval);
setInterval(() => this.tryLastPacket(), this.connectionCheckInterval);
}
//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;
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();
}
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;
};
}
const wa = new WebAudio(null);
// window.wa = wa; // For debugging accesibility
// Audio playback requires user interaction before being allowed by browsers
const connect = () => {
if (!wa.connected)
wa.start();
};
addEventListener("keydown", connect);
addEventListener("pointerdown", connect);
addEventListener("touchstart", connect);

1
pulse/client.conf Normal file
View File

@ -0,0 +1 @@
default-server=unix:/tmp/pulseaudio.socket

3
pulse/default.pa Normal file
View File

@ -0,0 +1,3 @@
#!/usr/bin/pulseaudio -nF
load-module module-native-protocol-unix socket=/tmp/pulseaudio.socket auth-anonymous=1
load-module module-always-sink