Initial commit
This commit is contained in:
commit
5d2dd6c946
69
Dockerfile
Normal file
69
Dockerfile
Normal 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
24
README.md
Normal 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
57
entrypoint.sh
Executable 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
6
novnc/defaults.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"autoconnect": true,
|
||||
"reconnect": true,
|
||||
"path": "websockify?token=vnc",
|
||||
"resize": "remote"
|
||||
}
|
2
novnc/tokenfile
Normal file
2
novnc/tokenfile
Normal file
@ -0,0 +1,2 @@
|
||||
vnc: localhost:5901
|
||||
pulse: localhost:6901
|
142
novnc/webaudio.js
Normal file
142
novnc/webaudio.js
Normal 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
1
pulse/client.conf
Normal file
@ -0,0 +1 @@
|
||||
default-server=unix:/tmp/pulseaudio.socket
|
3
pulse/default.pa
Normal file
3
pulse/default.pa
Normal 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
|
Loading…
x
Reference in New Issue
Block a user