From 51e965de8ef59b8079a7927f2418cda8a0526cf0 Mon Sep 17 00:00:00 2001 From: "Josh.5" Date: Tue, 11 Jan 2022 01:03:30 +1300 Subject: [PATCH] initial commit --- .dockerignore | 1 + .gitignore | 1 + Dockerfile | 363 +++++++++++++++ README.md | 43 +- devops/run_server.sh | 66 +++ images/steam-icon.png | Bin 0 -> 47255 bytes overlay/entrypoint.sh | 47 ++ overlay/etc/home_directory_template/.bashrc | 9 + .../.cache/log/.placeholder | 0 .../.config/autostart/Steam.desktop | 14 + .../xfce-perchannel-xml/xfce4-desktop.xml | 50 +++ .../xfce-perchannel-xml/xfce4-panel.xml | 62 +++ .../xfconf/xfce-perchannel-xml/xsettings.xml | 44 ++ .../.local/share/applications/x11vnc.desktop | 12 + overlay/etc/supervisor/conf.d/services.conf | 130 ++++++ overlay/opt/noVNC/audio.patch | 416 ++++++++++++++++++ overlay/scripts/10-setup_user.sh | 31 ++ overlay/scripts/20-configre_sshd.sh | 10 + overlay/scripts/30-configure_system_paths.sh | 32 ++ overlay/scripts/40-setup_locale.sh | 19 + overlay/scripts/50-configure_audio.sh | 20 + overlay/scripts/80-configure_nvidia_driver.sh | 64 +++ overlay/scripts/90-configure_xorg.sh | 57 +++ 23 files changed, 1489 insertions(+), 2 deletions(-) create mode 100755 .dockerignore create mode 100755 .gitignore create mode 100644 Dockerfile create mode 100755 devops/run_server.sh create mode 100644 images/steam-icon.png create mode 100644 overlay/entrypoint.sh create mode 100644 overlay/etc/home_directory_template/.bashrc create mode 100644 overlay/etc/home_directory_template/.cache/log/.placeholder create mode 100644 overlay/etc/home_directory_template/.config/autostart/Steam.desktop create mode 100644 overlay/etc/home_directory_template/.config/xfce4/xfconf/xfce-perchannel-xml/xfce4-desktop.xml create mode 100644 overlay/etc/home_directory_template/.config/xfce4/xfconf/xfce-perchannel-xml/xfce4-panel.xml create mode 100644 overlay/etc/home_directory_template/.config/xfce4/xfconf/xfce-perchannel-xml/xsettings.xml create mode 100644 overlay/etc/home_directory_template/.local/share/applications/x11vnc.desktop create mode 100644 overlay/etc/supervisor/conf.d/services.conf create mode 100644 overlay/opt/noVNC/audio.patch create mode 100644 overlay/scripts/10-setup_user.sh create mode 100644 overlay/scripts/20-configre_sshd.sh create mode 100644 overlay/scripts/30-configure_system_paths.sh create mode 100644 overlay/scripts/40-setup_locale.sh create mode 100644 overlay/scripts/50-configure_audio.sh create mode 100644 overlay/scripts/80-configure_nvidia_driver.sh create mode 100644 overlay/scripts/90-configure_xorg.sh diff --git a/.dockerignore b/.dockerignore new file mode 100755 index 0000000..04204c7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +config diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..04204c7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +config diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..18646ce --- /dev/null +++ b/Dockerfile @@ -0,0 +1,363 @@ +FROM debian:bullseye-slim + +# Update package repos +ARG DEBIAN_FRONTEND=noninteractive +RUN \ + echo "**** Update apt database ****" \ + && sed -i '/^.*main/ s/$/ contrib non-free/' /etc/apt/sources.list \ + && \ + echo + +# Update locale +RUN \ + echo "**** Update apt database ****" \ + && apt-get update \ + && \ + echo "**** Install and configure locals ****" \ + && apt-get -y install --no-install-recommends \ + locales \ + procps \ + && echo 'en_US.UTF-8 UTF-8' > /etc/locale.gen \ + && locale-gen \ + && \ + echo "**** Section cleanup ****" \ + && apt-get clean autoclean -y \ + && apt-get autoremove -y \ + && rm -rf \ + /var/lib/apt/lists/* \ + /var/tmp/* \ + /tmp/* \ + && \ + echo +ENV \ + LANG=en_US.UTF-8 \ + LANGUAGE=en_US:en \ + LC_ALL=en_US.UTF-8 + +# Re-install certificates +RUN \ + echo "**** Update apt database ****" \ + && apt-get update \ + && \ + echo "**** Install certificates ****" \ + && apt-get -y install --reinstall \ + ca-certificates \ + && \ + echo "**** Section cleanup ****" \ + && apt-get clean autoclean -y \ + && apt-get autoremove -y \ + && rm -rf \ + /var/lib/apt/lists/* \ + /var/tmp/* \ + /tmp/* \ + && \ + echo + +# Install core packages +RUN \ + echo "**** Update apt database ****" \ + && apt-get update \ + && \ + echo "**** Install tools ****" \ + && apt-get -y install \ + bash \ + bash-completion \ + gcc \ + git \ + kmod \ + less \ + make \ + nano \ + sudo \ + vim \ + wget \ + && \ + echo "**** Section cleanup ****" \ + && apt-get clean autoclean -y \ + && apt-get autoremove -y \ + && rm -rf \ + /var/lib/apt/lists/* \ + /var/tmp/* \ + /tmp/* \ + && \ + echo + +# Install desktop requirements +RUN \ + echo "**** Update apt database ****" \ + && apt-get update \ + && \ + echo "**** Install desktop requirements ****" \ + && apt-get -y install --no-install-recommends \ + dbus-x11 \ + libxcomposite-dev \ + libxcursor1 \ + man-db \ + mlocate \ + net-tools \ + pciutils \ + pkg-config \ + python3 \ + python3-numpy \ + python3-setuptools \ + rsync \ + x11vnc \ + xauth \ + xorg \ + xserver-xorg-legacy \ + xvfb \ + && \ + echo "**** Section cleanup ****" \ + && apt-get clean autoclean -y \ + && apt-get autoremove -y \ + && rm -rf \ + /var/lib/apt/lists/* \ + /var/tmp/* \ + /tmp/* \ + && \ + echo + +# Install supervisor +RUN \ + echo "**** Update apt database ****" \ + && apt-get update \ + && \ + echo "**** Install supervisor ****" \ + && apt-get -y install \ + supervisor \ + && \ + echo "**** Section cleanup ****" \ + && apt-get clean autoclean -y \ + && apt-get autoremove -y \ + && rm -rf \ + /var/lib/apt/lists/* \ + /var/tmp/* \ + /tmp/* \ + && \ + echo + +# Install openssh server +RUN \ + echo "**** Update apt database ****" \ + && apt-get update \ + && \ + echo "**** Install openssh server ****" \ + && apt-get -y install \ + openssh-server \ + && echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config \ + && \ + echo "**** Section cleanup ****" \ + && apt-get clean autoclean -y \ + && apt-get autoremove -y \ + && rm -rf \ + /var/lib/apt/lists/* \ + /var/tmp/* \ + /tmp/* \ + && \ + echo + +# Install noVNC +ARG NOVNC_VERSION=1.2.0 +RUN \ + echo "**** Fetch noVNC ****" \ + && cd /tmp \ + && wget -O /tmp/novnc.tar.gz https://github.com/novnc/noVNC/archive/v${NOVNC_VERSION}.tar.gz \ + && \ + echo "**** Extract noVNC ****" \ + && cd /tmp \ + && tar -xvf /tmp/novnc.tar.gz \ + && \ + echo "**** Configure noVNC ****" \ + && cd /tmp/noVNC-${NOVNC_VERSION} \ + && sed -i 's/credentials: { password: password } });/credentials: { password: password },\n wsProtocols: ["'"binary"'"] });/g' app/ui.js \ + && mkdir -p /opt \ + && rm -rf /opt/noVNC \ + && cd /opt \ + && mv -f /tmp/noVNC-${NOVNC_VERSION} /opt/noVNC \ + && cd /opt/noVNC \ + && ln -s vnc.html index.html \ + && chmod -R 755 /opt/noVNC \ + && \ + echo "**** Modify noVNC title ****" \ + && sed -i '/ document.title =/c\ document.title = "Steam Headless - noVNC";' \ + /opt/noVNC/app/ui.js \ + && \ + echo "**** Section cleanup ****" \ + && rm -rf \ + /tmp/noVNC* \ + /tmp/novnc.tar.gz + +# Install Websockify +ARG WEBSOCKETIFY_VERSION=0.10.0 +RUN \ + echo "**** Fetch Websockify ****" \ + && cd /tmp \ + && wget -O /tmp/websockify.tar.gz https://github.com/novnc/websockify/archive/v${WEBSOCKETIFY_VERSION}.tar.gz \ + && \ + echo "**** Extract Websockify ****" \ + && cd /tmp \ + && tar -xvf /tmp/websockify.tar.gz \ + && \ + echo "**** Install Websockify to main ****" \ + && cd /tmp/websockify-${WEBSOCKETIFY_VERSION} \ + && python3 ./setup.py install \ + && \ + echo "**** Install Websockify to noVNC path ****" \ + && cd /tmp \ + && mv -v /tmp/websockify-${WEBSOCKETIFY_VERSION} /opt/noVNC/utils/websockify \ + && \ + echo "**** Section cleanup ****" \ + && rm -rf \ + /tmp/websockify-* \ + /tmp/websockify.tar.gz + +# Install desktop environment +RUN \ + echo "**** Update apt database ****" \ + && apt-get update \ + && \ + echo "**** Install desktop environment ****" \ + && apt-get -y install \ + xfce4 \ + xfce4-terminal \ + msttcorefonts \ + fonts-vlgothic \ + gedit \ + && \ + echo "**** Section cleanup ****" \ + && apt-get clean autoclean -y \ + && apt-get autoremove -y \ + && rm -rf \ + /var/lib/apt/lists/* \ + /var/tmp/* \ + /tmp/* \ + && \ + echo + +# Install firefox +RUN \ + echo "**** Update apt database ****" \ + && apt-get update \ + && \ + echo "**** Install firefox ****" \ + && apt-get -y install \ + firefox-esr \ + && \ + echo "**** Section cleanup ****" \ + && apt-get clean autoclean -y \ + && apt-get autoremove -y \ + && rm -rf \ + /var/lib/apt/lists/* \ + /var/tmp/* \ + /tmp/* \ + && \ + echo + +# Install Steam +RUN \ + echo "**** Install steam ****" \ + && dpkg --add-architecture i386 \ + && apt-get update \ + && echo steam steam/question select "I AGREE" | debconf-set-selections \ + && echo steam steam/license note '' | debconf-set-selections \ + && apt-get -y install \ + steam \ + steam-devices \ + vulkan-tools \ + mesa-utils \ + mesa-vulkan-drivers \ + libglx-mesa0:i386 \ + mesa-vulkan-drivers:i386 \ + libgl1-mesa-dri:i386 \ + && \ + echo "**** Section cleanup ****" \ + && apt-get clean autoclean -y \ + && apt-get autoremove -y \ + && rm -rf \ + /var/lib/apt/lists/* \ + /var/tmp/* \ + /tmp/* \ + && \ + echo + +# Setup browser audio streaming deps +RUN \ + echo "**** Update apt database ****" \ + && apt-get update \ + && \ + echo "**** Install audio streaming deps ****" \ + && apt-get -y install --no-install-recommends \ + bzip2 \ + gstreamer1.0-alsa \ + gstreamer1.0-gl \ + gstreamer1.0-gtk3 \ + gstreamer1.0-libav \ + gstreamer1.0-plugins-base \ + gstreamer1.0-plugins-good \ + gstreamer1.0-pulseaudio \ + gstreamer1.0-qt5 \ + gstreamer1.0-tools \ + gstreamer1.0-x \ + libglu1-mesa \ + libgstreamer1.0-0 \ + libgtk2.0-0 \ + libncursesw5 \ + libopenal1 \ + libsdl-image1.2 \ + libsdl-ttf2.0-0 \ + libsdl1.2debian \ + libsndfile1 \ + novnc \ + pulseaudio \ + ucspi-tcp \ + && \ + echo "**** Section cleanup ****" \ + && apt-get clean autoclean -y \ + && apt-get autoremove -y \ + && rm -rf \ + /var/lib/apt/lists/* \ + /var/tmp/* \ + /tmp/* \ + && \ + echo + +# Configure default user and set env +ENV \ + USER="default" \ + USER_PASSWORD="password" \ + USER_HOME="/home/default" \ + TZ="Pacific/Auckland" \ + USER_LOCALES="en_US.UTF-8 UTF-8" +RUN \ + echo "**** Configure default user '${USER}' ****" \ + && mkdir -p \ + ${USER_HOME} \ + /games \ + && useradd -d ${USER_HOME} -s /bin/bash ${USER} \ + && chown -R ${USER} \ + ${USER_HOME} \ + /games \ + && echo "${USER} ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers + +# Add FS overlay +COPY overlay / + +# Set display environment variables +ENV \ + DISPLAY_CDEPTH="24" \ + DISPLAY_DPI="96" \ + DISPLAY_REFRESH="60" \ + DISPLAY_SIZEH="900" \ + DISPLAY_SIZEW="1600" \ + DISPLAY_VIDEO_PORT="DFP" \ + DISPLAY=":0" \ + NVIDIA_DRIVER_CAPABILITIES="all" \ + NVIDIA_VISIBLE_DEVICES="all" + +# Be sure that the noVNC port is exposed +EXPOSE 8083 +EXPOSE 32123 + +# Set entrypoint +RUN chmod +x /entrypoint.sh +ENTRYPOINT ["/entrypoint.sh"] diff --git a/README.md b/README.md index 71f5035..16b7856 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,41 @@ -# docker-steam-headless -Headless Steam supporting NVIDIA GPU and accessible via NoVNC +# Headless Steam Service + +Play your games in the browser with audio. Connect another device and use it with Steam Remote Play. + +## Features: +- Full video/audio noVNC web access to a Xfce4 Desktop +- NVIDIA GPU support +- Root access +- SSH server for remote terminal access + +--- +## Notes: + +### ADDITIONAL SOFTWARE: +If you wish to install additional applications, you can generate a +script inside the `~/init.d` directory ending with ".sh". This will be executed on the container startup. + +### STORAGE PATHS: +Everything that you wish to save in this container should be stored in the home directory or a docker container mount that you have specified. All files that are store outside your home directory are not persistent and will be wiped if there is an update of the container or you change something in the template. + +### GAMES LIBRARY: +It is recommended that you mount your games library to `/games` and configure Steam to add that path. + +### AUTO START APPLICATIONS: +In this container, Steam is configured to automatically start. If you wish to add additional services to automatically start, add them under **Applications > Settings > Session and Startup** in the WebUI. + +### NETWORK MODE: +If you want to use the container as a Steam Remote Play (previously "In Home Streaming") host device you should create a custom network and assign this container it's own IP, if you don't do this the traffic will be routed through the internet since Steam thinks you are on a different network. + +--- +## Running: + +For a development environment, I have created a script in the devops directory. + + +--- +## TODO: +- Configure xorg.conf with no NVIDIA GPU +- Lock SSH access to user only (remove root access) +- Require user to enter password for sudo +- Document how to run this container diff --git a/devops/run_server.sh b/devops/run_server.sh new file mode 100755 index 0000000..1d0fda2 --- /dev/null +++ b/devops/run_server.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +### +# File: run.sh +# Project: docker-steamos +# File Created: Saturday, 8th January 2022 2:34:23 pm +# Author: Josh.5 (jsunnex@gmail.com) +# ----- +# Last Modified: Monday, 10th January 2022 11:04:51 pm +# Modified By: Josh.5 (jsunnex@gmail.com) +### + +script_path=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ); +project_base_path=$(realpath ${script_path}/..); + + +if [[ ${1} == "stop" ]]; then + docker stop steam-headless + docker rm steam-headless + exit $? +elif [[ ${1} == "tail" ]]; then + docker logs -f steam-headless + exit $? +elif [[ ${1} == "user" ]]; then + docker exec -ti --user default steam-headless bash + exit $? +elif [[ ${1} == "root" ]]; then + docker exec -ti --user 0 steam-headless bash + exit $? +fi + + +docker stop steam-headless +docker rm steam-headless +sleep 1 + + +docker run -d --name='steam-headless' \ + --privileged=true \ + --net='br0' --ip='192.168.1.208' \ + --cpuset-cpus='3,9,4,10,5,11' \ + -e PUID="99" \ + -e PGID="100" \ + -e UMASK='000' \ + -e USER_PASSWORD="password" \ + -e USER="default" \ + -e HOME="/home/test" \ + -e USER_HOME="/home/default" \ + -e TZ="Pacific/Auckland" \ + -e USER_LOCALES="en_US.UTF-8 UTF-8" \ + -e DISPLAY_CDEPTH="24" \ + -e DISPLAY_DPI="96" \ + -e DISPLAY_REFRESH="60" \ + -e DISPLAY_SIZEH="720" \ + -e DISPLAY_SIZEW="1280" \ + -e DISPLAY_VIDEO_PORT="DFP" \ + -e DISPLAY=":0" \ + -e NVIDIA_DRIVER_CAPABILITIES="all" \ + -e NVIDIA_VISIBLE_DEVICES="all" \ + -v "${project_base_path}/config/home/default":'/home/default':'rw' \ + --hostname='steam-headless' \ + --shm-size=2G \ + --runtime=nvidia \ + josh5/steam-headless:latest + + +docker logs -f steam-headless diff --git a/images/steam-icon.png b/images/steam-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b7d71a57ecaba1a199a14308759cf20e932ebdba GIT binary patch literal 47255 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/overlay/etc/home_directory_template/.config/xfce4/xfconf/xfce-perchannel-xml/xfce4-panel.xml b/overlay/etc/home_directory_template/.config/xfce4/xfconf/xfce-perchannel-xml/xfce4-panel.xml new file mode 100644 index 0000000..d0a0d9f --- /dev/null +++ b/overlay/etc/home_directory_template/.config/xfce4/xfconf/xfce-perchannel-xml/xfce4-panel.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/overlay/etc/home_directory_template/.config/xfce4/xfconf/xfce-perchannel-xml/xsettings.xml b/overlay/etc/home_directory_template/.config/xfce4/xfconf/xfce-perchannel-xml/xsettings.xml new file mode 100644 index 0000000..cd0228c --- /dev/null +++ b/overlay/etc/home_directory_template/.config/xfce4/xfconf/xfce-perchannel-xml/xsettings.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/overlay/etc/home_directory_template/.local/share/applications/x11vnc.desktop b/overlay/etc/home_directory_template/.local/share/applications/x11vnc.desktop new file mode 100644 index 0000000..6e110dc --- /dev/null +++ b/overlay/etc/home_directory_template/.local/share/applications/x11vnc.desktop @@ -0,0 +1,12 @@ +# Hide application from menu +[Desktop Entry] +Name=X11VNC Server +Comment=Share this desktop by VNC +Exec=x11vnc -gui tray=setpass -rfbport PROMPT -bg -o %%HOME/.x11vnc.log.%%VNCDISPLAY +Icon=computer +Terminal=false +Type=Application +StartupNotify=false +#StartupWMClass=x11vnc_port_prompt +Categories=Network;RemoteAccess; +Hidden=true diff --git a/overlay/etc/supervisor/conf.d/services.conf b/overlay/etc/supervisor/conf.d/services.conf new file mode 100644 index 0000000..a82f082 --- /dev/null +++ b/overlay/etc/supervisor/conf.d/services.conf @@ -0,0 +1,130 @@ +[supervisord] +user=root +nodaemon=true + +[program:ssh] +autostart=true +priority=10 +directory=/ +command=/usr/sbin/sshd -D +user=root +autorestart=true +stopsignal=QUIT + +[program:dbus] +autostart=true +priority=10 +directory=/ +command=dbus-daemon --config-file=/usr/share/dbus-1/system.conf --nofork --nopidfile +user=%(ENV_USER)s +environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s" +autorestart=true +stopsignal=QUIT + +[program:pulseaudio] +autostart=true +priority=10 +directory=/ +command=/usr/bin/pulseaudio -vvvv --disallow-module-loading --disallow-exit --exit-idle-time=-1 +user=%(ENV_USER)s +environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s" +autorestart=true +stopsignal=QUIT +stdout_logfile=/home/%(ENV_USER)s/.cache/log/pulseaudio.log +stderr_logfile=/home/%(ENV_USER)s/.cache/log/pulseaudio.err + +[program:audiostream] +autostart=true +priority=10 +command=tcpserver localhost 5901 gst-launch-1.0 -q pulsesrc server=/tmp/pulseaudio.socket ! audio/x-raw, channels=2, rate=24000 ! cutter ! opusenc ! webmmux ! fdsink fd=1 +autorestart=true +stopsignal=QUIT +stdout_logfile=/home/%(ENV_USER)s/.cache/log/audiostream.log +stderr_logfile=/home/%(ENV_USER)s/.cache/log/audiostream.err + +[program:audiowebsock] +autostart=true +priority=10 +command=/usr/local/bin/websockify 32123 localhost:5901 +autorestart=true +stopsignal=QUIT +stdout_logfile=/home/%(ENV_USER)s/.cache/log/audiowebsock.log +stderr_logfile=/home/%(ENV_USER)s/.cache/log/audiowebsock.err + +[program:xorg] +autostart=true +priority=20 +directory=/ +command=/usr/bin/Xorg vt7 -novtswitch -sharevts -dpi "%(ENV_DISPLAY_DPI)s" +extension "MIT-SHM" %(ENV_DISPLAY)s +#command=/usr/bin/Xorg -novtswitch -sharevts -dpi "%(ENV_DISPLAY_DPI)s" +extension "MIT-SHM" "%(ENV_DISPLAY)s" +user=%(ENV_USER)s +environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s",XDG_RUNTIME_DIR="/tmp/xdg",RUNLEVEL="3" +autorestart=true +stopsignal=QUIT +stdout_logfile=/home/%(ENV_USER)s/.cache/log/xorg.log +stderr_logfile=/home/%(ENV_USER)s/.cache/log/xorg.err + +# Dont use xvfb as it does not work with nvidia X11 configuration +# [program:xvfb] +# autostart=false +# priority=20 +# directory=/ +# command=/usr/bin/Xvfb %(ENV_DISPLAY)s -screen 0 1920x1080x24 +# user=%(ENV_USER)s +# environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s" +# autorestart=true +# stopsignal=QUIT +# stdout_logfile=/home/%(ENV_USER)s/.cache/log/xvfb.log +# stderr_logfile=/home/%(ENV_USER)s/.cache/log/xvfb.err + +[program:x11vnc] +autostart=true +priority=30 +directory=/ +#command=/usr/bin/x11vnc -display %(ENV_DISPLAY)s -xkb +command=/usr/bin/x11vnc -display %(ENV_DISPLAY)s -rfbport 5900 -shared -forever +user=%(ENV_USER)s +autorestart=true +stopsignal=QUIT +stdout_logfile=/home/%(ENV_USER)s/.cache/log/x11vnc.log +stderr_logfile=/home/%(ENV_USER)s/.cache/log/x11vnc.err + +[program:de] +autostart=true +priority=50 +directory=/home/%(ENV_USER)s +command=/usr/bin/startxfce4 +#command=/usr/bin/mate-session +#command=/usr/bin/startlxqt +#command=/usr/bin/dbus-launch startxfce4 +#command=/usr/bin/dbus-launch xfce4-session +#command=/usr/bin/dbus-launch startplasma-x11 +user=%(ENV_USER)s +autorestart=true +stopsignal=QUIT +environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s" +stdout_logfile=/home/%(ENV_USER)s/.cache/log/de.log +stderr_logfile=/home/%(ENV_USER)s/.cache/log/de.err + +## Experimental launch directly to steam big picture... (this is cool, but not ideal for headless) +# [program:steam] +# autostart=true +# priority=50 +# directory=/home/%(ENV_USER)s +# command=/usr/bin/steam -bigpicture +# user=%(ENV_USER)s +# autorestart=true +# stopsignal=QUIT +# environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s" +# stdout_logfile=/home/%(ENV_USER)s/.cache/log/steam.log +# stderr_logfile=/home/%(ENV_USER)s/.cache/log/steam.err + +[program:novnc] +autostart=true +priority=100 +command=/opt/noVNC/utils/launch.sh --vnc localhost:5900 --listen 8083 +#command=/usr/share/novnc/utils/launch.sh --vnc localhost:5900 --listen 8083 +#command=/usr/local/bin/websockify -D --web=/usr/share/novnc/ --cert=/etc/ssl/novnc.pem 8083 localhost:5900 +user=%(ENV_USER)s +environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s" +autorestart=true diff --git a/overlay/opt/noVNC/audio.patch b/overlay/opt/noVNC/audio.patch new file mode 100644 index 0000000..8f31b12 --- /dev/null +++ b/overlay/opt/noVNC/audio.patch @@ -0,0 +1,416 @@ +diff --git a/app/styles/base.css b/app/styles/base.css +index adad415..dfefa6a 100644 +--- a/app/styles/base.css ++++ b/app/styles/base.css +@@ -683,7 +683,7 @@ select:active { + #noVNC_setting_port { + width: 80px; + } +-#noVNC_setting_path { ++#noVNC_setting_path #noVNC_setting_apath { + width: 100px; + } + +diff --git a/app/ui.js b/app/ui.js +index cb6a9fd..4d599e1 100644 +--- a/app/ui.js ++++ b/app/ui.js +@@ -15,6 +15,7 @@ import KeyTable from "../core/input/keysym.js"; + import keysyms from "../core/input/keysymdef.js"; + import Keyboard from "../core/input/keyboard.js"; + import RFB from "../core/rfb.js"; ++import WebAudio from "../core/webaudio.js"; + import * as WebUtil from "./webutil.js"; + + const PAGE_TITLE = "noVNC"; +@@ -40,6 +41,7 @@ const UI = { + inhibitReconnect: true, + reconnectCallback: null, + reconnectPassword: null, ++ webaudio: null, + + prime() { + return WebUtil.initSettings().then(() => { +@@ -171,6 +173,7 @@ const UI = { + UI.initSetting('compression', 2); + UI.initSetting('shared', true); + UI.initSetting('view_only', false); ++ UI.initSetting('audio', true); + UI.initSetting('show_dot', false); + UI.initSetting('path', 'websockify'); + UI.initSetting('repeaterID', ''); +@@ -200,6 +203,20 @@ const UI = { + } + }, + ++ toggleAudio() { ++ console.log('here'); ++ const audio = UI.getSetting('audio'); ++ if (audio) { ++ UI.webaudio.start(); ++ } else { ++ if(UI.webaudio !== null) { ++ if (UI.webaudio.connected) { ++ UI.webaudio.stop(); ++ } ++ } ++ } ++ }, ++ + /* ------^------- + * /INIT + * ============== +@@ -356,6 +373,8 @@ const UI = { + UI.addSettingChangeHandler('shared'); + UI.addSettingChangeHandler('view_only'); + UI.addSettingChangeHandler('view_only', UI.updateViewOnly); ++ UI.addSettingChangeHandler('audio'); ++ UI.addSettingChangeHandler('audio', UI.updateEnableAudio); + UI.addSettingChangeHandler('show_dot'); + UI.addSettingChangeHandler('show_dot', UI.updateShowDotCursor); + UI.addSettingChangeHandler('host'); +@@ -841,6 +860,7 @@ const UI = { + UI.updateSetting('compression'); + UI.updateSetting('shared'); + UI.updateSetting('view_only'); ++ UI.updateSetting('audio'); + UI.updateSetting('path'); + UI.updateSetting('repeaterID'); + UI.updateSetting('logging'); +@@ -1041,6 +1061,7 @@ const UI = { + UI.rfb.resizeSession = UI.getSetting('resize') === 'remote'; + UI.rfb.qualityLevel = parseInt(UI.getSetting('quality')); + UI.rfb.compressionLevel = parseInt(UI.getSetting('compression')); ++ UI.rfb.enableAudio = UI.getSetting('audio'); + UI.rfb.showDotCursor = UI.getSetting('show_dot'); + + UI.updateViewOnly(); // requires UI.rfb +@@ -1056,6 +1077,10 @@ const UI = { + + UI.updateVisualState('disconnecting'); + ++ if(UI.webaudio !== null && UI.webaudio.socket !== null) { ++ UI.webaudio.socket.close(); ++ } ++ + // Don't display the connection settings until we're actually disconnected + }, + +@@ -1097,6 +1122,19 @@ const UI = { + + // Do this last because it can only be used on rendered elements + UI.rfb.focus(); ++ ++ let audio_url; ++ let host = window.location.hostname; ++ let port = 32123; ++ if (window.location.protocol === "https:") { ++ audio_url = 'wss'; ++ } else { ++ audio_url = 'ws'; ++ } ++ audio_url += '://' + host + ':' + port; ++ ++ UI.webaudio = new WebAudio(audio_url); ++ UI.toggleAudio(); + }, + + disconnectFinished(e) { +@@ -1647,6 +1685,12 @@ const UI = { + } + }, + ++ updateEnableAudio() { ++ if (!UI.rfb) return; ++ UI.rfb.enableAudio = UI.getSetting('audio'); ++ UI.toggleAudio(); ++ }, ++ + updateShowDotCursor() { + if (!UI.rfb) return; + UI.rfb.showDotCursor = UI.getSetting('show_dot'); +diff --git a/core/webaudio.js b/core/webaudio.js +new file mode 100644 +index 0000000..28c71ac +--- /dev/null ++++ b/core/webaudio.js +@@ -0,0 +1,142 @@ ++export default class WebAudio { ++ constructor(url) { ++ this.url = url ++ ++ 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 ++ this.updateCheck = null; ++ this.syncCheck = null; ++ this.reduceCheck = null; ++ this.ConnCheck = null; ++ ++ } ++ ++ //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; ++ ++ if (this.updateCheck === null) this.updateCheck = setInterval(() => this.updateQueue(), this.updateBufferEvery); ++ if (this.syncCheck === null) this.syncCheck = setInterval(() => this.syncInterval(), this.syncLagInterval); ++ if (this.reduceCheck === null) this.reduceCheck = setInterval(() => this.reduceBuffer(), this.reduceBufferInterval); ++ if (this.ConnCheck === null) this.ConnCheck = setInterval(() => this.tryLastPacket(), this.connectionCheckInterval); ++ ++ 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(); ++ } ++ ++ stop() { ++ // Clear all interval timers ++ clearInterval(this.updateCheck); ++ clearInterval(this.syncCheck); ++ clearInterval(this.reduceCheck); ++ clearInterval(this.ConnCheck); ++ // Close the socket ++ this.socket.close(); ++ this.connected = false; ++ // Reset timers to null ++ this.updateCheck = null; ++ this.syncCheck = null; ++ this.reduceCheck = null; ++ this.ConnCheck = null; ++ } ++ ++ 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; ++ }; ++} +diff --git a/vnc.html b/vnc.html +index c678c2a..d1652a0 100644 +--- a/vnc.html ++++ b/vnc.html +@@ -170,6 +170,10 @@ + + +

  • ++
  • ++ ++
  • ++

  • +
  • + +
  • +diff --git a/vnc_lite.html b/vnc_lite.html +index 8e2f5cb..d3cf6ab 100644 +--- a/vnc_lite.html ++++ b/vnc_lite.html +@@ -41,6 +41,15 @@ + #status { + text-align: center; + } ++ #toggleAudioButton { ++ position: fixed; ++ top: 0px; ++ left: 0px; ++ border: 1px outset; ++ padding: 5px 5px 4px 5px; ++ cursor: pointer; ++ display: none; ++ } + #sendCtrlAltDelButton { + position: fixed; + top: 0px; +@@ -60,18 +69,38 @@ +