Runtime Services and Streaming#
Webserver (Linux)#
The webserver serves the Kataglyphis web frontend — a Flutter app with pages
for AI chat, blog posts, personal data, and an
open source license overview at /openSourceLicenses.
The maintained path is compose, which builds it with linux/ as the context
(linux/docker-compose.yml:3-5):
nerdctl compose -f linux/docker-compose.yml up -d webserver
By hand, the context must still be linux/ — the Dockerfile’s COPY sources are
context-relative ./webserver/nginx.conf and ./webserver/security-headers.conf
(linux/webserver/Dockerfile:27,30), and the repo root cannot stand in for it
because .dockerignore:18 excludes linux/webserver from the root context
outright:
The site itself is not in this repository — it is built in jotrockenmitlocken
and named as a build CONTEXT, so --build-context site=... is required and a
build without it fails by name
(../linux/webserver/README.md). The volume
mount below is the local-iteration path and is independent of that.
cd linux
nerdctl build -t kataglyphis-webserver:latest \
--build-context site=<path-to-jotrockenmitlocken>/build/web \
-f webserver/Dockerfile .
nerdctl run -d --name kataglyphis-webserver \
-p 8080:80 \
-v "$(pwd)/webserver/dist:/var/www/html" \
-v "$(pwd)/webserver/nginx.conf:/etc/nginx/nginx.conf:ro" \
kataglyphis-webserver:latest
linux/webserver/Dockerfile does not currently expose the fast Ubuntu mirror build flag.
Run with frontend display support#
nerdctl run --rm -it \
-e DISPLAY=$DISPLAY \
-e WAYLAND_DISPLAY=$WAYLAND_DISPLAY \
-e XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR \
-e PULSE_SERVER=$PULSE_SERVER \
-v /mnt/wslg:/mnt/wslg \
-v /tmp/.X11-unix:/tmp/.X11-unix \
-v $XDG_RUNTIME_DIR:$XDG_RUNTIME_DIR \
-v "$(pwd)":/workspace \
--workdir /workspace \
ghcr.io/kataglyphis/kataglyphis_beschleuniger:latest
Raspberry Pi Camera#
# list if camera is available
v4l2-ctl --list-devices
WebRTC Streaming#
The container includes a WebRTC signalling server (gst-webrtc-signalling-server) for real-time video streaming.
Firewall Configuration#
Allow port 8443 for the WebRTC signalling server:
sudo ufw allow 8443/tcp
Running the Signalling Server#
The beschleuniger container starts the signalling server automatically on port 8443:
nerdctl compose -f linux/docker-compose.yml up -d beschleuniger
Streaming from AccelerANTgine#
The AccelerANTgine project includes WebRTC streaming support via GStreamer’s webrtcsink.
The beschleuniger service mounts the checkout at /AccelerANTgine read-only, so
build on the host or in a writable copy; the preset writes build-release/ into the
source tree.
# Build the project (on the host with GStreamer, or in a writable copy in the container)
cd /AccelerANTgine
cmake --preset=linux-release-clang
cmake --build build-release
# Stream with test pattern
./build-release/bin/AccelerANTgine --webrtc --source test --server ws://localhost:8443
# Stream from libcamera (Raspberry Pi camera)
./build-release/bin/AccelerANTgine --webrtc --source libcamera --server ws://localhost:8443
# Stream from V4L2 USB camera
./build-release/bin/AccelerANTgine --webrtc --source v4l2 --device /dev/video0 --server ws://localhost:8443
CLI Options#
--webrtc Start WebRTC streaming
--config <file> Load settings from a JSON config file
--server <uri> Signalling server URI (default: ws://127.0.0.1:8443)
--source <type> Video source: libcamera, v4l2, test, file, uri (default: libcamera)
--device <path> V4L2 device path (default: /dev/video0)
--camera_id <name> libcamera camera name
--input_path <path> Local media file for --source file
--input_uri <uri> Media URI for --source uri
--width <pixels> Video width (default: 1280)
--height <pixels> Video height (default: 720)
--fps <rate> Framerate (default: 30)
--encoder <type> Encoder: h264-hw, h264-sw, vp8, vp9 (default: h264-hw)
--bitrate <kbps> Bitrate in kbps (default: 2000)
The size, rate, bitrate and server defaults come from the config (built-in, or the
--config file); a flag given on the command line overrides it. The flags are in
AccelerANTgine’s Src/cli_main.cpp.
Viewing the Stream#
The webserver’s nginx proxies /webrtc-ws to the beschleuniger service’s signalling
port (linux/webserver/nginx.conf). The viewer page, a GstWebRTC API client at
http://localhost/javascript/webrtc/index.html
shipped in the tracked linux/webserver/dist/, removed on 2026-09-15. The
jotrockenmitlocken site sources carry no viewer (checked 2026-09-25), so that URL
works only when a dist/ left over from before is volume-mounted as above.
Raw gst-launch-1.0 pipelines (debugging below the app)#
When --webrtc above fails, the useful next step is to reproduce the same
pipeline by hand. gst-launch-1.0 bisects the problem: if the raw pipeline
works, the fault is in the application wiring; if it does not, the fault is in
the plugins, the encoder, or the camera.
First establish the environment is sane at all:
gst-launch-1.0 --version
gst-inspect-1.0 # every plugin the runtime can see
gst-inspect-1.0 | grep -i h264 # ...or just the encoders
gst-launch-1.0 videotestsrc ! autovideosink
Prefix any pipeline with GST_DEBUG=3 (or 4) for negotiation failures — the
usual cause is a caps mismatch that the error message alone does not explain.
Raspberry Pi camera → WebRTC#
gst-launch-1.0 libcamerasrc ! \
video/x-raw,width=640,height=360,format=NV12,interlace-mode=progressive ! \
x264enc speed-preset=1 threads=1 byte-stream=true ! \
h264parse ! \
webrtcsink signaller::uri="ws://0.0.0.0:8444" name=ws meta="meta,name=gst-stream"
On a Pi 3 the USB/V4L2 path is the one that works:
gst-launch-1.0 v4l2src device=/dev/video0 ! \
video/x-raw,width=1280,height=720,framerate=15/1 ! \
webrtcsink signaller::uri="ws://0.0.0.0:8443" name=ws meta="meta,name=gst-stream"
Recording to a file instead of streaming#
Useful to prove the capture side independently of the network side:
gst-launch-1.0 v4l2src device=/dev/video50 ! \
video/x-raw,width=640,height=480,framerate=30/1 ! \
videoconvert ! \
openh264enc ! h264parse ! \
mp4mux ! filesink location=output.mp4
MJPEG source → WebRTC#
Cameras that only offer image/jpeg at the resolution you want need an explicit
decode step:
GST_DEBUG=3 gst-launch-1.0 -e \
v4l2src device=/dev/video20 ! \
image/jpeg,width=1280,height=720,framerate=5/1 ! \
jpegdec ! videoconvert ! \
video/x-raw,format=I420 ! \
openh264enc ! \
webrtcsink congestion-control=disabled \
signaller::uri="ws://0.0.0.0:8443" name=ws meta="meta,name=webfrontend-stream"
Hardware encoders that under-declare their caps#
Some vendor encoders (kyh264enc on Rockchip-class boards among them) negotiate
successfully but emit caps too vague for webrtcsink to build an SDP from. The
fix is an explicit capsfilter after the encoder — spelling out the stream
format, profile and level the downstream element needs:
GST_DEBUG=4 gst-launch-1.0 -v -e \
v4l2src device=/dev/video20 ! \
video/x-raw,format=YUY2,width=1280,height=720,framerate=5/1 ! \
videoconvert ! video/x-raw,format=NV12,width=1280,height=720,framerate=5/1 ! \
kyh264enc ! h264parse config-interval=1 ! \
capsfilter caps="video/x-h264,stream-format=(string)byte-stream,alignment=(string)au,profile=(string)main,level=(string)3.1,chroma-format=(string)4:2:0,bit-depth-luma=(uint)8,bit-depth-chroma=(uint)8,parsed=(boolean)true" ! \
queue ! \
webrtcsink congestion-control=disabled \
signaller::uri="ws://0.0.0.0:8443" name=ws meta="meta,name=webfrontend-stream"
Bisect this one with ... ! kyh264enc ! fakesink first — if that fails, the
encoder is the problem, not the sink.
Basler industrial camera (Pylon)#
gst-launch-1.0 -e \
pylonsrc ! \
video/x-raw,format=RGB,width=1920,height=1080,framerate=30/1 ! \
videoconvert ! videoscale ! \
video/x-raw,format=I420,width=1920,height=1080,framerate=30/1 ! \
queue ! \
vp8enc target-bitrate=6000000 deadline=1 cpu-used=6 threads=4 error-resilient=1 ! \
webrtcsink signaller::uri="ws://0.0.0.0:8443" name=ws meta="meta,name=webfrontend-stream"
Building GStreamer from source on a device#
The images build GStreamer through
linux/scripts/03-media/build/gstreamer/common/build-gstreamer-monorepo.sh.
This section is for the case that script does not cover: building on a target
board to get a plugin the packaged runtime lacks.
Build the version the packaged runtime ships, not a fixed number: the tag is
GSTREAMER_VERSION from linux/scripts/01-core/versions.env (1.29.2), which is
what setup-gstreamer.sh:607-638 checks out for the image. A build from an older
branch installs its own libgst*.so* over the prefix and lands you in the
stale-library case described under Removing a previous source
install.
git clone -b 1.29.2 https://gitlab.freedesktop.org/gstreamer/gstreamer.git
cd gstreamer
Install the build dependencies:
sudo apt install -y build-essential meson ninja-build git pkg-config \
libglib2.0-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \
libdrm-dev libx11-dev libxv-dev libxext-dev \
libasound2-dev libpulse-dev libv4l-dev libudev-dev libssl-dev \
libx264-dev libx265-dev \
libavcodec-dev libavformat-dev libavutil-dev libswscale-dev \
libogg-dev libvorbis-dev libopus-dev libtheora-dev \
libspeex-dev libmp3lame-dev libfaad-dev \
libjack-jackd2-dev liba52-0.7.4-dev yasm
Fetch the subprojects before configuring. This step is easy to miss and the resulting failure — a plugin silently absent from the build — does not point back at it:
meson subprojects download
meson subprojects update
Then:
meson setup build \
--prefix=/usr \
-Dtests=disabled \
-Dexamples=disabled \
-Drs=enabled \
-Dgpl=enabled \
-Dbuildtype=release
meson compile -C build
sudo meson install -C build
sudo ldconfig
The Rust plugins (-Drs=enabled, which is where webrtcsink lives) land in the
cargo target directory rather than the system plugin path, so point GStreamer at
both:
export GST_PLUGIN_PATH="$HOME/gst-plugins-rs/target/release:/usr/lib/$(gcc -dumpmachine)/gstreamer-1.0${GST_PLUGIN_PATH:+:$GST_PLUGIN_PATH}"
On a cross-built or multiarch rootfs the loader also needs the arch triplet
directory — /usr/lib/aarch64-linux-gnu on ARM64, /usr/lib/riscv64-linux-gnu
on RISC-V:
export LIBRARY_PATH="/usr/lib/$(gcc -dumpmachine):$LIBRARY_PATH"
export PKG_CONFIG_PATH=/usr/lib/pkgconfig:$PKG_CONFIG_PATH
Confirm the plugin is actually visible before debugging a pipeline that uses it:
gst-inspect-1.0 webrtcsink
Removing a previous source install#
meson install scatters files across the prefix, and ninja uninstall is often
unavailable by the time you need it. On a cross rootfs the stale copies are also
arch-specific, so they sit in the triplet directory rather than /usr/local/lib:
# preview first — the -print before -delete is deliberate
sudo find /usr/local/lib/riscv64-linux-gnu -type f -name 'libgst*.so*' -print
sudo find /usr/local/lib/riscv64-linux-gnu -type f -name 'libgst*.so*' -print -delete
sudo ldconfig
Swap the triplet for the target you built (aarch64-linux-gnu,
x86_64-linux-gnu). Leaving stale libgst*.so* behind is a common cause of a
pipeline that loads the wrong plugin version and fails with a caps error that
makes no sense against the source you are reading.
Prebuilt Android GStreamer#
The Android lane builds GStreamer from source with cerbero
(linux/scripts/03-media/build/gstreamer/android/build-android-from-source.sh,
cerbero cloned at the GSTREAMER_VERSION tag). The upstream universal tarball is
only the fallback build-gstreamer.sh takes when that script is missing. To use it
by hand, unpack it where Dockerfile.android expects to find it:
sudo mkdir -p /opt/android/gstreamer
sudo tar -xf gstreamer-1.0-android-universal-1.29.2.tar.xz -C /opt/android/gstreamer
Keep the version aligned with GSTREAMER_VERSION in
linux/scripts/01-core/versions.env: the fallback derives the tarball name
from it (linux/scripts/03-media/build/gstreamer/android/build-gstreamer.sh:38)
and verifies the download against GSTREAMER_ANDROID_UNIVERSAL_SHA256, which is
pinned for that exact tarball. A hand-unpacked tree of any other version surfaces
much later as a link error in the Android stage.