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