Initial commit
This commit is contained in:
commit
88a0749cd4
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal 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
46
docker-compose.yaml
Normal 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
|
||||
|
27
musescore-psock-build/Dockerfile
Normal file
27
musescore-psock-build/Dockerfile
Normal 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
|
50
novnc-audio-build/Dockerfile
Normal file
50
novnc-audio-build/Dockerfile
Normal 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
|
1
novnc-audio-build/client.conf
Normal file
1
novnc-audio-build/client.conf
Normal file
@ -0,0 +1 @@
|
||||
default-server=unix:/tmp/psock/pulseaudio.socket
|
5
novnc-audio-build/conf.d/audiostream.conf
Normal file
5
novnc-audio-build/conf.d/audiostream.conf
Normal 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
|
5
novnc-audio-build/conf.d/pulseaudio.conf
Normal file
5
novnc-audio-build/conf.d/pulseaudio.conf
Normal 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
|
5
novnc-audio-build/conf.d/websockify.conf
Normal file
5
novnc-audio-build/conf.d/websockify.conf
Normal 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
|
3
novnc-audio-build/default.pa
Normal file
3
novnc-audio-build/default.pa
Normal 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
|
2
novnc-audio-build/tokenfile
Normal file
2
novnc-audio-build/tokenfile
Normal file
@ -0,0 +1,2 @@
|
||||
vnc: localhost:5900
|
||||
pulse: localhost:5901
|
129
novnc-audio-build/webaudio.js
Normal file
129
novnc-audio-build/webaudio.js
Normal 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;
|
||||
};
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user