Initial commit

This commit is contained in:
Thomas Lovén 2023-02-01 14:29:47 +01:00
commit 88a0749cd4
11 changed files with 281 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
**/*
!.gitignore
!docker-compose.yaml
!novnc-audio-build
!novnc-audio-build/**
!musescore-psock-build
!musescore-psock-build/*

46
docker-compose.yaml Normal file
View File

@ -0,0 +1,46 @@
version: "3.0"
networks:
web:
external: true
volumes:
pulse_socket:
services:
#DOCKER_BUILDKIT=1 COMPOSE_DOCKER_CLI_BUILD=1 docker-compose build musescore
musescore:
image: thomasloven/musescore-psock
build: ./musescore-psock-build/
depends_on:
- novnc
volumes:
- pulse_socket:/tmp/psock
- ./mscore:/root/Documents/MuseScore4
environment:
DISPLAY: novnc:0.0
command:
- /bin/sh
- -c
- /bin/sleep 3 && mscore
novnc:
image: thomasloven/novnc-audio
build: ./novnc-audio-build/
networks:
web:
default:
volumes:
- pulse_socket:/tmp/psock
environment:
RUN_XTERM: "yes"
RUN_FLUXBOX: "yes"
DISPLAY_WIDTH: 1920
DISPLAY_HEIGHT: 1080
labels:
traefik.enable: true
traefik.docker.network: web
traefik.http.routers.musescore.rule: Host(`musescore.${BASE_DOMAIN}`)
traefik.http.routers.musescore.tls.certResolver: le
traefik.http.routers.musescore.middlewares: auth@file

View File

@ -0,0 +1,27 @@
# syntax=docker/dockerfile:1.4
FROM archlinux
RUN pacman -Sy \
&& pacman --noconfirm -S xterm alsa-utils alsa-plugins libpulse
ENV PULSE_SERVER unix:/tmp/psock/pulseaudio.socket
COPY <<EOF /root/.asoundrc
pcm.!default {
type pulse
fallback "sysdefault"
hint {
show on
description "Default ALSA output (currently PulseAudio Sound Server)"
}
}
ctl.!default {
type pulse
fallback "sysdefault"
}
EOF
RUN pacman -Sy\
&& pacman --noconfirm -S community/musescore
CMD mscore

View File

@ -0,0 +1,50 @@
# Inspired by https://github.com/vexingcodes/dwarf-fortress-docker
FROM theasp/novnc:latest
RUN apt-get update --allow-releaseinfo-change \
&& DEBIAN_FRONTEND=noninteractive apt-get install --yes --no-install-recommends \
gstreamer1.0-plugins-good \
gstreamer1.0-pulseaudio \
gstreamer1.0-tools \
\
libglu1-mesa \
libgtk2.0-0 \
libncursesw5 \
libopenal1 \
libsdl-image1.2 \
libsdl-ttf2.0-0 \
libsdl1.2debian \
\
libsndfile1 \
pulseaudio \
ucspi-tcp \
dbus-x11 \
&& rm -rf /var/lib/apt/lists/*
COPY conf.d /app/conf.d2
RUN mv /app/conf.d2/* /app/conf.d
COPY tokenfile /app/tokenfile
COPY default.pa client.conf /etc/pulse/
COPY webaudio.js /usr/share/novnc/core/
RUN ln -s /usr/share/novnc/vnc_lite.html /usr/share/novnc/index.html \
&& sed -i 's/display:flex/display:none/' /usr/share/novnc/app/styles/lite.css \
&& sed -i "/import RFB/a \
import WebAudio from './core/webaudio.js'" \
/usr/share/novnc/vnc_lite.html \
&& sed -i "/function connected(e)/a \
var wa = new WebAudio(null); \
document.getElementsByTagName('canvas')[0].addEventListener('keydown', e => { wa.start(); });" \
/usr/share/novnc/vnc_lite.html \
&& sed -i "s/'path', 'websockify');/'path', 'websockify?token=vnc');/" \
/usr/share/novnc/vnc_lite.html
RUN groupadd pa \
&& useradd --create-home --gid pa pa \
&& mkdir -p /tmp/psock \
&& chown -R pa:pa /tmp/psock
VOLUME /tmp/psock

View File

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

View File

@ -0,0 +1,5 @@
[program:audiostream]
command=tcpserver localhost 5901 gst-launch-1.0 -q pulsesrc server=/tmp/psock/pulseaudio.socket !audio/x-raw, channels=2, rate=24000 ! cutter ! opusenc ! webmmux ! fdsink fd=1
autorestart=true
stdout_logfile=/var/log/audiostream.log
redirect_stderr=true

View File

@ -0,0 +1,5 @@
[program:pulseaudio]
command=su --command="/usr/bin/pulseaudio --disallow-module-loading -vvvv --disallow-exit --exit-idle-time=-1" pa
autorestart=true
stdout_logfile=/var/log/pulseaudio.log
redirect_stderr=true

View File

@ -0,0 +1,5 @@
[program:websockify]
command=websockify --web /usr/share/novnc 8080 --token-plugin=TokenFile --token-source=/app/tokenfile
autorestart=true
stdout_logfile=/var/log/websockify.log
redirect_stderr=true

View File

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

View File

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

View File

@ -0,0 +1,129 @@
export default 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.hostname + "/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;
};
}