Headless, disposable web page video streamer for public displays
Load any URL or local HTML in a containerized headless browser, capture it with FFmpeg, and push encoded video to SRT or RTMP ingest endpoints — no broadcast software required.
Container launches Xvfb at your requested resolution.
Playwright-controlled Chromium loads the target page.
FFmpeg performs x11grab of the display and encodes with libx264.
Output is pushed via SRT, RTMP, or saved to a local file.
Containerized streaming of any web page or local HTML file with one command. No GUI, no desktop, no broadcast software.
Inject custom CSS overrides and JavaScript into the streamed page for styling transformations and interactive behavior.
Compose multiple page sources into collage-style layouts using FFmpeg filter graphs. Side-by-side, tiled, or custom arrangements.
Primary support for SRT (Secure Reliable Transport), secondary RTMP/RTMPS, extensible to any FFmpeg output.
Loop pre-recorded video files directly to ingest endpoints without browser overhead. Auto-scales, pads, and normalizes frame rates.
Exponential backoff reconnection for SRT/RTMP failures. Structured JSON health logging for monitoring.
Optional browser-based VNC viewer to interact with the headless Chromium session in real time. Disabled by default.
Automatic system requirements validation, per-container configuration, secret management, and operational runbooks.
Browser-based control panel for managing streams, monitoring health, and adjusting configuration in real time.
Coming Sooncp .env.stable.example .env
# Edit .env with your target URLs:
# STANDARD_1_URL, STANDARD_2_URL, ...
# SOURCE_LEFT_URL, SOURCE_RIGHT_URL
If your ingest URLs contain # characters (e.g. Kaltura stream IDs):
cp env.secrets.sh.example .env.secrets.sh
# Edit .env.secrets.sh with your actual stream IDs
docker build -t page-stream:latest .
# With production secrets:
source .env.secrets.sh && \
docker-compose -f docker-compose.stable.yml up -d
# Or for local testing:
docker-compose -f docker-compose.stable.yml up -d
Streams appear in the out/ folder — open with VLC or ffplay.
An automatic check validates resources at startup. For macOS with Colima:
colima stop
colima start --cpu 8 --memory 16
docker run --rm \
-e WIDTH=1920 -e HEIGHT=1080 \
page-stream:latest \
--ingest srt://example.com:9000?streamid=live.slp \
--url https://your-dashboard.example.com
docker run --rm \
-e WIDTH=1280 -e HEIGHT=720 \
page-stream:latest \
--ingest rtmp://live.example.com/app/streamKey \
--format flv \
--url https://your-page.example.com
docker run --rm \
-v $(pwd)/custom.css:/custom.css \
-v $(pwd)/custom.js:/custom.js \
page-stream:latest \
--ingest srt://host:9000?streamid=demo \
--url https://your-page.example.com \
--inject-css /custom.css \
--inject-js /custom.js
# Place video in ./videos/ directory
cp /path/to/video.mp4 ./videos/input.mp4
docker run --rm \
-v $(pwd)/videos:/videos:ro \
page-stream:latest \
--ingest srt://host:9000?streamid=test \
--video-file /videos/input.mp4 \
--video-loop
docker run --rm \
-e WIDTH=1280 -e HEIGHT=720 \
page-stream:latest \
--ingest srt://host:9000?streamid=live \
--url https://live-data.example.com \
--auto-refresh-seconds 300
docker run --rm \
-v $(pwd)/out:/out \
page-stream:latest \
--ingest /out/test.ts \
--format mpegts \
--url demo/index.html
page-stream --ingest <URI> [options]
Required:
-i, --ingest <uri> Ingest URI (SRT/RTMP/file)
Page Options:
-u, --url <url> Page URL or local file (default: demo)
--width <n> Viewport width (default: 1280)
--height <n> Viewport height (default: 720)
--fps <n> Frames per second (default: 30)
Encoding:
--preset <p> x264 preset (default: veryfast)
--video-bitrate <kbps> Video bitrate (default: 2500k)
--audio-bitrate <kbps> Audio bitrate (default: 128k)
--format <fmt> Container format (default: mpegts)
--extra-ffmpeg <args...> Additional raw FFmpeg arguments
Browser:
--no-headless Show browser window
--no-fullscreen Disable fullscreen/kiosk mode
--no-app-mode Show full browser chrome
--crop-infobar <px> Crop N pixels from top (remove banner)
Refresh & Control:
--refresh-signal <sig> Reload signal (default: SIGHUP)
--graceful-stop-signal <sig> Stop signal (default: SIGTERM)
--auto-refresh-seconds <n> Auto reload interval (0 = disable)
Reconnection:
--reconnect-attempts <n> Max retries, 0 = infinite (default: 0)
--reconnect-initial-delay-ms Initial backoff delay (default: 1000)
--reconnect-max-delay-ms Max backoff delay (default: 15000)
Monitoring:
--health-interval-seconds <n> Health log interval (default: 30)
Injection:
--inject-css <file> Inject CSS from file
--inject-js <file> Inject JavaScript from file
Video File (bypass browser):
--video-file <path> Stream video file directly
--video-loop Loop video continuously
Each standard instance runs independently on the default network, streaming a single full-HD page to its own ingest endpoint.
standard-1 (1920×1080) → SRT ingest standard-2 (1920×1080) → SRT ingest standard-3 (1920×1080) → SRT ingest standard-4 (1920×1080) → SRT ingest
Two half-width sources are composed into a single full-HD stream via an FFmpeg filter graph on an isolated network.
source-left (960×1080) →⎯
→ compositor → SRT ingest
source-right (960×1080) →⎯
┌───────────────────────────────────────────────────────────┐ │ requirements-check │ │ Validates CPU/RAM before other services start │ ├────────────────────────────┼──────────────────────────────┤ │ default network │ compositor_net │ │ ┌────────────────────────┐ │ ┌──────────────────────────┐ │ │ │ standard-1 standard-2 │ │ │ source-left │ │ │ │ standard-3 standard-4 │ │ │ source-right │ │ │ │ srt-test-listener │ │ │ compositor │ │ │ └────────────────────────┘ │ │ srt-ingest │ │ │ │ └──────────────────────────┘ │ └────────────────────────────┴──────────────────────────────┘
Structured JSON health lines are emitted at configurable intervals:
{
"type": "health",
"ts": "2024-01-01T00:00:00.000Z",
"uptimeSec": 30.1,
"ingest": "srt://example.com:9000?streamid=test",
"protocol": "SRT",
"restartAttempt": 0,
"lastFfmpegExitCode": null,
"retrying": false
}
| Code | Meaning |
|---|---|
0 | Graceful stop |
1 | Unhandled internal error |
10 | SRT/RTMP reconnect attempts exhausted |
11 | Non-retry protocol failure |