A headless, disposable web page video streamer designed to deliver high-fidelity web content, schedules, and loops directly to public displays.
page-stream launches a target URL or local HTML page in a Playwright-controlled Chromium browser under Xvfb (Virtual Framebuffer), captures the visual screen with ffmpeg in real-time, encodes it into a highly optimized video stream (H.264), and broadcasts it to any target ingest endpoint (such as Kaltura, YouTube, or local SRT/RTMP listeners).
- Stream any web page or local static layout in containerized environments.
- Dynamically inject custom CSS stylesheets and Vanilla JavaScript scripts to format, automate, and skin web pages specifically for layouts.
- Composite multiple streaming sources into dynamic collage-style layouts (e.g., side-by-side splits). See COMPOSITOR-ARCHITECTURE.md.
- Stream pre-recorded
.mp4video files continuously in a loop without browser overhead, featuring overlay watermarks and citations. - Robust exponential backoff retry mechanisms for handling SRT and RTMP network drops.
- Optimized and validated for high-performance execution on macOS and Linux.
- Splits core page-stream codebase from orchestration, website targets, and streaming keys.
For production environments, combine:
- Public Code Repository (
page-stream): open-source streaming engine, Dockerfiles, direct video file configurations, and local utility scripts. - Private Ops Repository (e.g.,
page-stream-config): Live target maps (example.env), custom visual styles (assets/), and Docker Compose orchestration workflows. Secrets such as ingest stream keys stored as GitHub Repository Secrets.
- Clone the codebase.
git clone https://github.com/pu-orfe/page-stream.git
cd page-stream- Run the bootstrapper.
./bootstrap-runner.shChoose Option 5) Bootstrap a New Private Ops Repository
-
Answer the prompts. The wizard will guide you through:
- Entering your department code (e.g.,
ECO). - Generating a local, custom, structured configuration repository folder containing templates for your
docker-compose.yml, website target mappingeconomics.env, and visual assets (assets/custom.css). - Automatically logging into GitHub and creating a brand new private configuration repository on your account (e.g.,
economics/page-stream-config-economics) using your authenticatedghsession.
- Entering your department code (e.g.,
-
Configure & Launch Once completed, follow the printed completion instructions:
- Add your private ingest URLs (Kaltura, etc) to your new repository's GitHub Secrets (as
STANDARD_1_INGEST). - Add your target website URLs to your new config file (e.g.,
economics/economics.env). - Register your self-hosted runner for your new private repository, and trigger the Deploy Action.
- Add your private ingest URLs (Kaltura, etc) to your new repository's GitHub Secrets (as
Note
When launching the stack via Docker Compose, an automatic system requirements check runs inside a helper container. If your Docker VM (e.g., Colima) is allocated too little RAM or CPU, the stack will halt with helpful allocation guidance.
If you are using Colima, allocate resources by running:
colima stop
colima start --cpu 6 --memory 16To run a stable test stack on your local host using standard fallback mock targets and local SRT output files (without sending to external Kaltura streams):
Copy the example stable environment file:
cp .env.stable.example .envEdit .env to define your target websites (e.g., STANDARD_1_URL, SOURCE_LEFT_URL, etc.).
# Compile the local Docker image
docker build -t page-stream:latest .
# Bring down any colliding containers and start a fresh stable stack
docker compose -f docker-compose.stable.yml down
docker compose -f docker-compose.stable.yml up -dAll outputs will appear as real-time transport stream files inside your local ./out/ directory for VLC/ffplay verification!
For looping pre-recorded videos without browser/render overhead:
- Place your video files (e.g.,
input.mp4) inside the./videos/directory (git-ignored, mounted read-only into container). - Start streaming with direct file loop:
docker run --rm \
-v $(pwd)/videos:/videos:ro \
page-stream:latest \
--ingest srt://your-srt-ingest-url \
--video-file /videos/input.mp4 \
--video-looppage-stream --ingest <URI> [options]
Required:
-i, --ingest <uri> Ingest URI (SRT/RTMP/etc)
Optional:
-u, --url <url> Page URL or local file (default: demo)
--width <n> Width (default 1280)
--height <n> Height (default 720)
--fps <n> FPS (default 30)
--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 args
--no-headless Disable headless Chromium
--no-fullscreen Disable fullscreen mode
--no-app-mode Disable Chromium app mode (shows normal browser chrome)
--refresh-signal <sig> Signal for page reload (default SIGHUP)
--graceful-stop-signal Signal for graceful stop (default SIGTERM)
--reconnect-attempts Max reconnect attempts (0 = infinite, default 0)
--auto-refresh-seconds Auto page reload interval in seconds (0=disable)
--inject-css <file> Inject CSS from file into the page
--inject-js <file> Inject JavaScript from file into the page
--video-file <path> Stream video file directly (bypasses browser)
--video-loop Loop video file continuously
--fallback-demo-page Stream the bundled demo page if a local --url is
missing, instead of exiting (legacy behaviour)
--url accepts a remote URL, a file:// URL, or a filesystem path (absolute or
relative). Query strings and fragments are preserved in every form, so a page can
configure itself from its own URL:
page-stream --ingest "$INGEST" --url '/assets/slate.html?channel=Studio%20A'
page-stream --ingest "$INGEST" --url 'file:///assets/slate.html#section'A missing local page is fatal. If the file named by --url does not exist,
page-stream exits non-zero with the resolved path rather than starting the stream:
Error: Local page not found: /out/assets/slate.html (from --url '/out/assets/slate.html').
Pass a filesystem path or file:// URL that exists inside the container - a bind
mount may be missing. Use --fallback-demo-page to stream the bundled demo page
instead of exiting.
Earlier versions warned and quietly streamed the bundled demo page instead. That is
the worst outcome for an unattended display: the stream stays up and healthy, so
nothing alerts, and the wrong content plays until someone happens to look at the
screen. --fallback-demo-page restores the old behaviour where it is genuinely
wanted.