commit 88a0749cd443cba440462ed7367616acf9f1cbbc Author: Thomas Lovén Date: Wed Feb 1 14:29:47 2023 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..02f1ee5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +**/* +!.gitignore + +!docker-compose.yaml +!novnc-audio-build +!novnc-audio-build/** +!musescore-psock-build +!musescore-psock-build/* diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..7fc8168 --- /dev/null +++ b/docker-compose.yaml @@ -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 + diff --git a/musescore-psock-build/Dockerfile b/musescore-psock-build/Dockerfile new file mode 100644 index 0000000..ba530c0 --- /dev/null +++ b/musescore-psock-build/Dockerfile @@ -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 < { 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 \ No newline at end of file diff --git a/novnc-audio-build/client.conf b/novnc-audio-build/client.conf new file mode 100644 index 0000000..dc07062 --- /dev/null +++ b/novnc-audio-build/client.conf @@ -0,0 +1 @@ +default-server=unix:/tmp/psock/pulseaudio.socket \ No newline at end of file diff --git a/novnc-audio-build/conf.d/audiostream.conf b/novnc-audio-build/conf.d/audiostream.conf new file mode 100644 index 0000000..e6fe5ab --- /dev/null +++ b/novnc-audio-build/conf.d/audiostream.conf @@ -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 \ No newline at end of file diff --git a/novnc-audio-build/conf.d/pulseaudio.conf b/novnc-audio-build/conf.d/pulseaudio.conf new file mode 100644 index 0000000..049c68a --- /dev/null +++ b/novnc-audio-build/conf.d/pulseaudio.conf @@ -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 \ No newline at end of file diff --git a/novnc-audio-build/conf.d/websockify.conf b/novnc-audio-build/conf.d/websockify.conf new file mode 100644 index 0000000..c833182 --- /dev/null +++ b/novnc-audio-build/conf.d/websockify.conf @@ -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 \ No newline at end of file diff --git a/novnc-audio-build/default.pa b/novnc-audio-build/default.pa new file mode 100644 index 0000000..f3e2994 --- /dev/null +++ b/novnc-audio-build/default.pa @@ -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 \ No newline at end of file diff --git a/novnc-audio-build/tokenfile b/novnc-audio-build/tokenfile new file mode 100644 index 0000000..65f4242 --- /dev/null +++ b/novnc-audio-build/tokenfile @@ -0,0 +1,2 @@ +vnc: localhost:5900 +pulse: localhost:5901 \ No newline at end of file diff --git a/novnc-audio-build/webaudio.js b/novnc-audio-build/webaudio.js new file mode 100644 index 0000000..4e8db20 --- /dev/null +++ b/novnc-audio-build/webaudio.js @@ -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; + }; +} \ No newline at end of file