OBS-free, 24/7 multi-camera livestream orchestrator driven by FFmpeg and ZMQ.
Growcast is a personal experiment in running a continuous livestream without OBS. It was built to monitor a grow tent and stream around the clock to platforms like Twitch and Owncast — with automated scene rotation, live sensor overlays, and radio audio — all from a Docker container on a Linux host.
You can see it running at grow.dwot.io.
Deployment note: Growcast is designed for LAN use only. Do not expose the container or API port directly to the internet. There is no TLS, no rate limiting, and API authentication is optional. Keep it behind a firewall or VPN.
- Multi-camera compositing — single, picture-in-picture, and 2×2 grid layouts
- Seamless scene switching — ZMQ-driven
streamselectfilter; the output stream never reconnects or drops frames on a scene change - Auto-rotation — cycles through a configurable scene order on a timer
- Rich overlay — live sensor stats, rotating plant ticker, and now-playing track info
- Optional audio source — attach any HTTP audio stream (e.g. an Azuracast radio station)
- Multi-destination fan-out — encode once, stream to multiple RTMP/RTMPS endpoints via the tee muxer
- NVIDIA NVENC/CUVID acceleration — optional GPU encode and decode paths
- Camera health monitoring — background health checks with automatic status tracking
- REST API — scene control and status endpoints, with optional API key authentication
Growcast's overlay system is designed to work with Isley, a grow management app that exposes sensor readings (temperature, humidity, VPD), plant data, and moisture levels via a JSON API. Set stats_api_url in the overlay config to point to your Isley instance and the overlay will display live sensor data and a rotating plant ticker automatically.
Set audio_source.url to an Azuracast (or any HTTP) audio stream to add a live audio track to the output. Optionally enable now_playing_enabled to poll the Azuracast NowPlaying API and display the current artist and track title as a stacked overlay element.
- Growcast has no TLS and no rate limiting. It is not designed for direct internet exposure.
- The API is unauthenticated by default. Set
api_keyinconfig.yamlif you need to restrict access — all endpoints except/healthwill then require anX-API-Keyheader. - For Docker deployments,
host: "0.0.0.0"is correct — external access is controlled by theports:mapping indocker-compose.yml. - For bare-metal deployments, set
host: "localhost"to restrict the API to the local machine.
- Docker and Docker Compose
- RTSP camera streams
- RTMP/RTMPS streaming destination(s)
Note: The Docker image bundles FFmpeg built with ZMQ support. No separate FFmpeg installation is required.
For bare-metal builds, FFmpeg 6.0+ must be in PATH and built with --enable-libzmq.
1. Copy the example config and fill in your values:
cp config.example.yaml config.yamlEdit config.yaml with your camera URLs, stream destinations, and scene definitions. See Configuration below.
2. Start with Docker Compose:
docker compose up -dThe container mounts ./config.yaml read-only and persists overlay temp files in a named Docker volume.
3. Check that it's running:
curl http://localhost:8080/health
# {"status":"ok"}
curl http://localhost:8080/statusAll configuration lives in config.yaml. The full annotated reference is in config.example.yaml.
Only cameras, scenes, and outputs are required. Everything else is optional.
cameras:
cam1:
name: "Camera 1"
rtsps_url: "rtsp://192.168.1.100:554/stream"
enabled: true
scenes:
scene1:
name: "Scene 1"
cameras: [cam1]
layout: "single"
outputs:
out1:
name: "Stream"
type: "rtmp"
url: "rtmp://your-server/live"
key: "your-stream-key"
enabled: truecameras:
cam1:
id: cam1
name: "Grow Tent - Front"
rtsps_url: "rtsps://192.168.1.100:7441/stream/unicam/xxxxxx?enableSrtp"
username: "user"
password: "password"
enabled: true
width: 1920
height: 1080scenes:
single_cam1:
id: single_cam1
name: "Single - Front"
cameras: [cam1]
layout: "single"
pip_view:
id: pip_view
name: "PiP View"
cameras: [cam1, cam2] # first camera is primary
layout: "pip"
quad_view:
id: quad_view
name: "Quad View"
cameras: [cam1, cam2, cam3, cam4]
layout: "grid"Layouts:
| Layout | Cameras | Description |
|---|---|---|
single |
1 | Full-frame single camera |
pip |
2 | Primary camera full-frame with second camera inset |
grid |
2–4 | 2×2 grid (unused slots are black) |
outputs:
twitch:
id: twitch
name: "Twitch"
type: "rtmps"
url: "rtmps://live-iad.twitch.tv:443/app"
key: "your-stream-key"
enabled: trueMultiple outputs are supported. FFmpeg encodes once and fans out to all enabled destinations via the tee muxer.
encoding:
bitrate: "5000k"
preset: "veryfast" # x264: ultrafast…veryslow | NVENC: p1 (fast) … p7 (quality)
keyframe_seconds: 1 # Streamplace requires ≤7s (1s recommended)
x264_params: "bframes=0" # Ignored when NVENC is active| Field | Default | Notes |
|---|---|---|
bitrate |
5000k |
Target output bitrate |
preset |
veryfast |
Encoder speed/quality tradeoff |
keyframe_seconds |
2 |
Keyframe interval in seconds |
x264_params |
(empty) | Extra x264 options; ignored with NVENC |
Optional NVIDIA GPU encode/decode. Requires the GPU Docker image (see Docker).
hardware_acceleration:
enabled: false
type: "nvenc" # Only "nvenc" is supported
device: "0" # GPU device index (0 = first GPU)
decode: false # Also decode camera streams on GPU (h264_cuvid)- Encode path (
enabled: true): usesh264_nvencwith CPU-side yuv420p frames. Nohwupload_cudafilter — the encoder handles the CPU→GPU upload internally. - Decode path (
decode: true): usesh264_cuvidper camera input, then downloads frames back to system memory before the software filter graph runs. Requires FFmpeg built with--enable-cuvid. - On the host,
nvidia-smimust be working andnvidia-container-toolkitmust be installed.
Growcast supports two overlay modes. Set stats_api_url to use API mode (recommended with Isley); otherwise set stats_file_path for file mode.
API mode (Isley):
overlay:
enabled: true
position: "top-right" # top-left | top-right | bottom-left | bottom-right
font_size: 26
font_color: "white"
shadow_color: "black"
font_file: "./fonts/Anton-Regular.ttf" # bundled; mount your own at this path to override
stats_api_url: "https://your-isley-host/api/overlay"
stats_api_interval: 15s
stats_api_key: "your_api_key_here"
temp_sensor_id: 1 # sensor ID for temperature
humidity_sensor_id: 2 # sensor ID for relative humidity
vpd_sensor_id: 3 # sensor ID for VPD
plant_ticker_enabled: true
plant_ticker_interval: 10s
moisture_sensor_source: "" # e.g. "192.168.1.21" — appends soil % to tickerNow-playing overlay (Azuracast):
overlay:
# ...
now_playing_enabled: false
now_playing_api_url: "https://radio.example.com/api/nowplaying/station_slug"
now_playing_api_key: "" # Required if station is not public
now_playing_interval: 15s
now_playing_position: "bottom-left"File mode (legacy):
overlay:
enabled: true
stats_file_path: "/var/growcast/stats.txt"
refresh_interval: 5sAnton-Regular.ttf is bundled in the repo and baked into the Docker image. To use a different font, mount it over the bundled one:
- ./fonts:/app/fonts:ro.
audio_source:
enabled: true
url: "http://your-azuracast-server:8000/radio.mp3"
volume: 1.0
reconnect: true
reconnect_delay: 2scene_switch:
enabled: true
default_scene: "single_cam1"
scene_order:
- single_cam1
- pip_view
- quad_view
interval: 5mAuto-rotation cycles through scene_order on the configured interval. Scene switching can also be triggered via the API at any time.
api:
enabled: true
host: "0.0.0.0" # correct for Docker; use "localhost" for bare-metal
port: 8080
# api_key: "" # Optional. If set, all endpoints (except /health) require X-API-Key header.| Method | Endpoint | Description |
|---|---|---|
GET |
/health |
Health check (always unauthenticated) |
GET |
/status |
Current scene, camera states, timestamp |
GET |
/scene/current |
Active scene ID |
GET |
/cameras |
All camera configurations and states |
GET |
/cameras/{id} |
Single camera |
POST |
/scene/{id}/start |
Switch to a scene immediately |
POST |
/camera/{id}/restart |
Restart the current scene (reconnects all cameras) |
GET |
/info |
App version and camera count |
Without API key (default):
curl -X POST http://localhost:8080/scene/pip_view/startWith API key configured:
curl -X POST http://localhost:8080/scene/pip_view/start \
-H "X-API-Key: your-key-here"The API has no authentication by default. To restrict access, set api_key in config.yaml — all endpoints except /health will then require an X-API-Key header.
Pre-built image from Docker Hub:
docker pull dwot/growcast:latest
docker compose up -dPre-built image from Docker Hub:
docker pull dwot/growcast:gpu-latestPrerequisites on the Docker host:
- NVIDIA driver installed (
nvidia-smishould work) nvidia-container-toolkitinstalled and configured- Docker daemon configured to use the NVIDIA runtime
Run with GPU compose file:
docker compose -f docker-compose.gpu.yml up -dThen enable hardware_acceleration in config.yaml.
# CPU
docker compose up --build -d
# GPU
docker compose -f docker-compose.gpu.yml up --build -dGrowcast has no hot-reload. After editing config.yaml, restart with:
docker compose up -dUse
up -d, notrestart—restartonly restarts the container process but does not re-mount the config or rebuild the FFmpeg filter graph.
docker compose logs -fRequires Go 1.24+ and FFmpeg 6.0+ with --enable-libzmq in PATH.
go build -o growcast ./cmd/growcast/...
./growcast -config config.yaml
./growcast -config config.yaml -log-level debugTests:
go test ./...