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#

rpi-cam sources

# 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.