PiStation
Turn any TV or monitor into a screen the whole room can use.
A Raspberry Pi drives a TV or monitor and shows a six digit code. Anyone on the network types that code into the website to take the screen. They can share a screen or a phone camera, draw over what is being shown, or open a shared whiteboard. When nobody is presenting, the display becomes a dashboard.
No accounts. No cloud. Nothing leaves the network it runs on.
What it does
- Screen sharing from any browser, no install and no cable. Anyone in the room can take a turn, one at a time.
- Camera sharing from phones and laptops. Point a phone at a whiteboard or a workbench and it goes on the big screen, with front and rear cameras and a flip button. A screen share always takes priority over a camera.
- Live annotation over whatever is on screen, with an adjustable pen, arrows, shapes and a highlighter. Everyone's cursor carries their name.
- Shared whiteboard powered by Excalidraw, mirrored onto the display.
- Idle dashboard with clock, weather, agenda and rotating wallpapers, plus a builder for your own widgets.
- Night mode that drops the screen to just the time and the join code outside working hours.
- Rotating codes, changed every minute, so an old photo of the screen is worthless. Sessions survive the rotation.
- Health reporting from every kiosk: CPU, memory, Wi-Fi signal, temperature and uptime.
Self hosting
Everything ships as a Docker Compose stack: a LiveKit media server, a Rust backend and the website.
git clone <your-repo> pistation
cd pistation
cp infra/.env.example infra/.env
Edit infra/.env and set at least LIVEKIT_API_SECRET, SESSION_SECRET and
BOOTSTRAP_ADMIN_PASSWORD. Then:
docker compose -f infra/docker-compose.yml up -d
| Service | Port |
|---|---|
| Website | 3000 |
| API | 8080 |
| LiveKit | 7880 |
Sign in at /admin with the bootstrap admin details from your .env.
If you are serving anything other than localhost, set PUBLIC_API_URL and
PUBLIC_LIVEKIT_URL in infra/.env to addresses your devices can actually reach, and change
turn.domain in infra/livekit.yaml to the same host. LiveKit reads that file literally, so it
does not pick up environment variables.
The LiveKit container uses host networking, because WebRTC needs to advertise an address your devices can reach and a container on a bridge network has none. That is Linux only; on macOS or Windows, run LiveKit outside Docker.
Setting up a screen
Works on the Raspberry Pi Zero 2 W, Pi 4 and Pi 5, and needs the 64 bit Raspberry Pi OS. The media stack has no 32 bit ARM build, so the installer stops with an explanation rather than failing halfway if it finds a 32 bit system.
Upload a kiosk build once on the admin home page, then create a kiosk. The admin panel hands you a single command to paste into an SSH session on a fresh Raspberry Pi OS Lite install. It must be run with sudo, since it installs packages, writes to the boot partition and adds a service:
curl -fsSL http://your-server:8080/install.sh | sudo bash -s -- --key <enrollment-token>
That installs the kiosk, writes its configuration, enlarges swap, tunes the boot config and reboots straight into the display. It is safe to re-run, which is also how you repoint a kiosk at a new server.
The installer reads the board model and picks video settings and swap to match:
| Board | Video | Swap |
|---|---|---|
| Pi Zero 2 W | 960px at 12fps, quality 70 | 2 GB |
| Pi 4 | 1600px at 30fps, quality 80 | 1 GB |
| Pi 5 | 1920px at 60fps, quality 85 | 512 MB |
Raspberry Pi OS ships with 100 MB of swap, which a 512 MB Zero 2 W exhausts as soon as the browser engine and the media stack are both running. Without more, the kernel starts killing processes and the kiosk appears to restart at random.
Useful flags: --server, --join-url, --package-url, --user, --profile, --swap,
--skip-reboot.
Configuration
Set in infra/.env:
| Variable | What it does |
|---|---|
LIVEKIT_API_KEY / LIVEKIT_API_SECRET |
Credentials shared by the server and LiveKit |
SESSION_SECRET |
Signs admin sessions, make it long and random |
BOOTSTRAP_ADMIN_EMAIL / _PASSWORD |
Creates the first admin account on startup |
PIN_ROTATION_SECONDS |
How often join codes change, 30 to 60 |
PIN_GRACE_SECONDS |
How long an old code keeps working after it changes |
PUBLIC_API_URL / PUBLIC_LIVEKIT_URL |
Addresses browsers and kiosks use to reach you |
CORS_ORIGINS |
Browser origins allowed to call the API. Kiosks are exempt, since a desktop app has no fixed web origin |
Video encoding on the kiosk is tunable per device, without rebuilding:
| Variable | Default | Notes |
|---|---|---|
PISTATION_VIDEO_MAX_WIDTH |
1920 |
Incoming video is scaled down past this |
PISTATION_VIDEO_FPS |
60 |
Frames beyond this are dropped, never queued |
PISTATION_VIDEO_QUALITY |
85 |
1 to 100 |
A Pi Zero 2W wants roughly 960, 12 and 70. Frames are dropped rather than queued at every
stage, so an overloaded kiosk loses frames instead of falling further behind.
Images
Published to GitHub Container Registry on every push to main and every v* tag:
docker pull ghcr.io/sirblobby/pistation-server:latest
docker pull ghcr.io/sirblobby/pistation-web:latest
The Compose stack builds from source by default. To run the published images instead, replace the
build: block on the server and web services in infra/docker-compose.yml with an image:
line pointing at the tag above.
Licence
MIT. Source at github.com/SirBlobby/pistation.