Add Docker Compose stack and Pi notes

LiveKit on host networking so WebRTC can advertise a reachable address,
plus the server and website.

Claude-Session: https://claude.ai/code/session_01SS9F92jb51bMCRCKem6QtD
This commit is contained in:
2026-08-09 17:09:25 -04:00
parent a0cbd88fa4
commit 735545512d
5 changed files with 187 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
LIVEKIT_API_KEY=devkey
LIVEKIT_API_SECRET=devsecretdevsecretdevsecretdevsecret
PUBLIC_LIVEKIT_URL=ws://localhost:7880
PUBLIC_API_URL=http://localhost:8080
SESSION_SECRET=change-me-to-a-long-random-string
SESSION_TTL_HOURS=12
BOOTSTRAP_ADMIN_EMAIL=admin@pistation.local
BOOTSTRAP_ADMIN_PASSWORD=changeme
PIN_ROTATION_SECONDS=45
PIN_GRACE_SECONDS=15
CORS_ORIGINS=http://localhost:3000,http://localhost:5173
+58
View File
@@ -0,0 +1,58 @@
name: pistation
services:
livekit:
image: livekit/livekit-server:latest
restart: unless-stopped
command: --config /etc/livekit.yaml
environment:
LIVEKIT_KEYS: "${LIVEKIT_API_KEY}: ${LIVEKIT_API_SECRET}"
volumes:
- ./livekit.yaml:/etc/livekit.yaml:ro
network_mode: host
server:
build:
context: ..
dockerfile: server/Dockerfile
restart: unless-stopped
depends_on:
- livekit
environment:
BIND_ADDRESS: 0.0.0.0:8080
DATABASE_URL: sqlite:///data/pistation.db?mode=rwc
MEDIA_DIR: /data/media
PACKAGE_DIR: /data/packages
PUBLIC_API_URL: ${PUBLIC_API_URL}
LIVEKIT_URL: ${PUBLIC_LIVEKIT_URL}
LIVEKIT_API_KEY: ${LIVEKIT_API_KEY}
LIVEKIT_API_SECRET: ${LIVEKIT_API_SECRET}
SESSION_SECRET: ${SESSION_SECRET}
SESSION_TTL_HOURS: ${SESSION_TTL_HOURS}
BOOTSTRAP_ADMIN_EMAIL: ${BOOTSTRAP_ADMIN_EMAIL}
BOOTSTRAP_ADMIN_PASSWORD: ${BOOTSTRAP_ADMIN_PASSWORD}
PIN_ROTATION_SECONDS: ${PIN_ROTATION_SECONDS}
PIN_GRACE_SECONDS: ${PIN_GRACE_SECONDS}
CORS_ORIGINS: ${CORS_ORIGINS}
volumes:
- server-data:/data
ports:
- "8080:8080"
web:
build:
context: ..
dockerfile: apps/web-client/Dockerfile
restart: unless-stopped
depends_on:
- server
environment:
PORT: 3000
HOST: 0.0.0.0
PUBLIC_API_URL: ${PUBLIC_API_URL}
PUBLIC_LIVEKIT_URL: ${PUBLIC_LIVEKIT_URL}
ports:
- "3000:3000"
volumes:
server-data:
+22
View File
@@ -0,0 +1,22 @@
port: 7880
bind_addresses:
- 0.0.0.0
rtc:
tcp_port: 7881
port_range_start: 50000
port_range_end: 50100
use_external_ip: false
turn:
enabled: true
udp_port: 3478
domain: localhost
room:
auto_create: true
empty_timeout: 300
max_participants: 50
logging:
level: info
+5
View File
@@ -0,0 +1,5 @@
{
"serverUrl": "http://pistation.local:8080",
"enrollmentToken": "paste-the-token-from-the-admin-panel",
"joinUrl": "pistation.local:3000"
}
+86
View File
@@ -0,0 +1,86 @@
# Spike: video performance on the Pi Zero 2W
The original question was whether WebKitGTK could reach hardware H.264 decode. That turned
out to be moot: WebKitGTK ships without WebRTC at all, so the kiosk cannot receive media in
the webview.
The kiosk now joins the room from Rust with the native LiveKit SDK. Decoding happens there,
each frame is scaled and JPEG encoded, and the result is pushed to the webview to be drawn
on a canvas. The question this spike answers has therefore changed: can a Zero 2W decode the
incoming stream **and** re-encode it fast enough to look live.
The decode half is still worth measuring with the commands below, because libwebrtc uses the
same V4L2 path. The encode half is measured from the app itself, by lowering
`PISTATION_VIDEO_MAX_WIDTH`, `PISTATION_VIDEO_FPS` and `PISTATION_VIDEO_QUALITY` until the
picture keeps up.
## Baseline
The Zero 2W has a VideoCore IV class block exposed through V4L2 stateful decode at
`/dev/video10`, reached through GStreamer's `v4l2h264dec`.
## Steps
1. Confirm the kernel exposes the decoder.
```
ls -l /dev/video10 /dev/video11 /dev/video12
v4l2-ctl -d /dev/video10 --all | head -40
```
`/dev/video10` must report `Codec Decoder` capabilities.
2. Confirm GStreamer picks the hardware element over the software one.
```
gst-inspect-1.0 v4l2h264dec
gst-launch-1.0 videotestsrc num-buffers=600 ! video/x-raw,width=1280,height=720 \
! x264enc tune=zerolatency ! h264parse ! v4l2h264dec ! fpsdisplaysink text-overlay=false
```
Record the reported average FPS. Under 25 means the decode path is not viable.
3. Run the same clip through the software decoder for comparison.
```
gst-launch-1.0 videotestsrc num-buffers=600 ! video/x-raw,width=1280,height=720 \
! x264enc tune=zerolatency ! h264parse ! avdec_h264 ! fpsdisplaysink text-overlay=false
```
If step 2 and step 3 report similar numbers, WebKit is almost certainly using
software decode and the hardware element is not being selected.
4. Measure the real path. Start the kiosk, join from a laptop, share a 1280x720
screen, and sample CPU while the stream is live.
```
systemctl start pistation-kiosk
top -b -n 30 -d 1 | grep -E "WebKitWebProcess|pistation-kiosk"
```
Sustained WebKitWebProcess CPU above roughly 85 percent of one core means the
decode is running in software.
5. Force the issue if step 4 is inconclusive.
```
GST_DEBUG=v4l2*:5 systemctl start pistation-kiosk
journalctl -u pistation-kiosk | grep -i v4l2h264dec
```
## Recording the result
| Measurement | Value | Pass threshold |
| --- | --- | --- |
| `v4l2h264dec` 720p30 FPS | | 25 or higher |
| WebKitWebProcess CPU, 720p live share | | under 85 percent of one core |
| Frames dropped over 5 minutes | | under 1 percent |
| Annotation input to on-screen latency | | under 250 ms |
## If the spike fails
1. Try 960x540 at 20 fps by capping the publisher in `apps/web-client` before
declaring failure. The Zero 2W often passes at that resolution.
2. If it still fails, build `services/compositor-bot`. GStreamer composites the
screen share and the annotation canvas server side and sends the Pi one stream,
which drops the Pi's job to plain video playback.