A self-hostable web console for viewing and managing RTSP video streams in the browser.
RTSP Stream Console accepts RTSP sources, processes them with FFmpeg, and delivers browser-compatible H.264 MPEG-TS video over WebSockets. Multiple viewers can share one active stream runtime instead of starting a separate FFmpeg process for every browser.
Built with React, TypeScript, PatternFly, Go, FFmpeg, Docker Compose, Caddy, Terraform, and GitHub Container Registry.
- Architecture - runtime ownership, media flow, lifecycle, and deployment boundaries
- Security - RTSP validation, authentication, resource controls, and known limitations
- Deployment - local Compose, Azure, GHCR, DNS, TLS, updates, and rollback
- Scaling - transcoding cost, viewer fan-out, worker evolution, WebRTC, and HLS
- Contributing - development setup and contribution workflow
- Register and manage multiple RTSP sources
- Start and stop stream processing from the browser
- View multiple streams in a responsive grid
- Play and pause individual browser viewers
- Share one stream runtime across multiple viewers
- Fan out MPEG-TS media over WebSockets
- Surface stream lifecycle and runtime errors
- Limit active FFmpeg runtimes
- Limit concurrent viewers per stream
- Rate-limit expensive API operations
- Restrict RTSP destinations for public deployments
- Run locally with Docker Compose
- Deploy to Azure with Terraform and deployment automation
- Pull prebuilt application images from GHCR
A hosted deployment is available at:
https://streams.dataolympus.com
Access may be restricted, and the hosted demo may be taken offline outside active review or demonstration periods.
The hosted environment includes a synthetic RTSP camera so the complete stream path can be tested without an external camera.
RTSP source
|
v
+---------+
| FFmpeg |
+---------+
|
MPEG-TS / H.264
|
v
+--------------+
| Go backend |
| media Hub |
+--------------+
| | |
| | |
v v v
WS WS WS
| | |
v v v
mpegts.js clients
| | |
v v v
<video> elements
The browser never connects directly to RTSP.
Each started stream record owns its own FFmpeg runtime. Multiple viewers of that record share the same backend media pipeline.
Creating multiple stream records with the same RTSP URL can therefore create multiple FFmpeg processes. The current implementation does not deduplicate runtimes by URL.
Browser
|
| HTTPS / WSS
v
Caddy
|
+------ React frontend
|
+------ /api/* ----------+
| |
+------ WebSocket -----+ |
| |
v v
Go backend
|
| RTSP
v
FFmpeg
|
| MPEG-TS
v
fan-out Hub
|
v
browser viewers
created
|
v
connecting
|
v
live
|
v
stopping
|
v
stopped
Runtime or source failures move a stream into the error state.
Pausing playback in the browser disconnects only that viewer. It does not stop the shared backend stream runtime.
- Docker
- Docker Compose
Clone the repository:
git clone https://github.com/dataolympus/rtsp-stream-console.git
cd rtsp-stream-consoleVerify the source checkout
If you are developing or reviewing the source, run the project checks before starting the application:
./scripts/check.shThis verifies:
- Go tests
- Go vet
- frontend tests
- frontend production build
- local Docker Compose configuration
A normal release deployment using published GHCR images does not require running the source test suite locally.
Start the local stack with the demo profile:
docker compose \
--env-file deploy/compose/demo.env \
-f deploy/compose/compose.yaml \
-f deploy/compose/compose.local.yaml \
--profile demo \
up --build -dOpen:
http://localhost:8082
Add the included synthetic RTSP stream:
Name: Camera 1
URL: rtsp://mediamtx:8554/camera-1
Then click Start.
The demo profile runs four containers:
backend
frontend
mediamtx
test-camera
The local development flow does not enable Basic Auth.
Stop the stack with:
docker compose \
--env-file deploy/compose/demo.env \
-f deploy/compose/compose.yaml \
-f deploy/compose/compose.local.yaml \
--profile demo \
downThe deployment model separates infrastructure from application deployment.
Terraform
|
v
VM + network + public IP + Docker
|
v
deploy-azure.sh
|
+---- checkout exact Git ref
|
+---- configure production environment
|
+---- pull published GHCR images
|
v
Docker Compose
Terraform configuration is available under:
deploy/terraform/azure/
It provisions:
- Azure Resource Group
- VNet and subnet
- Network Security Group
- restricted SSH access
- public HTTP and HTTPS access
- static Standard public IPv4
- Linux VM
- Docker and Docker Compose through cloud-init
Example:
cd deploy/terraform/azure
terraform init
terraform plan
terraform applyTerraform manages the host infrastructure. Application credentials are intentionally kept outside Terraform state.
The Azure deployment wrapper discovers the VM through Terraform output and deploys an exact Git revision.
For a versioned release:
./scripts/deploy-azure.sh \
--ref v0.1.0 \
--release v0.1.0 \
--site streams.example.comEnable the synthetic camera for a demonstration deployment:
./scripts/deploy-azure.sh \
--ref v0.1.0 \
--release v0.1.0 \
--site streams.example.com \
--demoFor an existing deployment whose environment and credentials should be preserved:
./scripts/deploy-azure.sh \
--ref v0.1.0 \
--release v0.1.0 \
--reuse-env \
--demoWhen a release is deployed, its backend and frontend image references are persisted in the production environment. A later --reuse-env deployment therefore continues using those exact images unless another --release version is supplied.
For development builds, a commit SHA and the dev image tag can be used instead.
Application images are built by GitHub Actions and published to GitHub Container Registry:
ghcr.io/dataolympus/rtsp-stream-console-backend
ghcr.io/dataolympus/rtsp-stream-console-frontend
Published images can be pulled anonymously.
Development deployments currently use:
:dev
Versioned releases publish image tags such as:
:v0.1.0
:0.1
:sha-<commit>
Production deployments should use the exact version tag such as :v0.1.0. The dev tag is intended for development and testing.
Public deployments use Caddy Basic Auth at the edge.
There is deliberately no default password.
The deployment helper prompts for a password and stores only its bcrypt hash in:
deploy/compose/prod.env
The file is excluded from Git.
The equivalent manual hash-generation flow is:
read -rsp "Reviewer password: " DEMO_PASSWORD
echo
DEMO_PASSWORD_HASH="$(
printf '%s\n' "$DEMO_PASSWORD" |
docker run --rm -i caddy:2-alpine \
caddy hash-password
)"
unset DEMO_PASSWORDA production environment contains values similar to:
SITE_ADDRESS=streams.example.com
DEMO_USERNAME=reviewer
DEMO_PASSWORD_HASH='$2a$...'Never store the plaintext password in prod.env.
Basic Auth should only be used over HTTPS.
The public deployment path includes several defensive controls.
By default, public deployments reject RTSP targets that resolve to:
- private networks
- loopback addresses
- link-local addresses
- multicast addresses
- unspecified addresses
RTSP URLs containing embedded credentials are also rejected.
Controlled internal hostnames can be explicitly allowlisted.
Self-hosted deployments that intentionally access private camera networks can opt into private-network RTSP sources.
The URL policy is a baseline SSRF mitigation. It should not be treated as a complete defense against every DNS-rebinding or network-level attack.
Configurable protections include:
MAX_ACTIVE_STREAMS
MAX_VIEWERS_PER_STREAM
EXPENSIVE_REQUESTS_PER_MINUTE
BACKEND_CPUS
BACKEND_MEMORY_LIMIT
BACKEND_PIDS_LIMIT
The hosted demo currently limits:
active stream runtimes 2
viewers per stream 4
backend CPU 2
backend memory 1 GiB
backend PID count 128
Slow WebSocket subscribers use bounded buffers and can be disconnected rather than allowing one viewer to stall the shared media pipeline.
A typical production deployment exposes only:
22/tcp restricted administrator SSH
80/tcp HTTP redirect / certificate challenge
443/tcp HTTPS application traffic
Backend port 8080 and RTSP port 8554 are not exposed publicly.
Basic Auth and application-level rate limits reduce casual abuse. Volumetric DDoS protection belongs at the cloud or edge layer.
Common production settings include:
RTSP_ALLOW_PRIVATE_NETWORKS=false
RTSP_ALLOWED_HOSTS=
MAX_ACTIVE_STREAMS=4
MAX_VIEWERS_PER_STREAM=8
EXPENSIVE_REQUESTS_PER_MINUTE=10
TRUST_PROXY_HEADERS=true
BACKEND_CPUS=2.0
BACKEND_MEMORY_LIMIT=1g
BACKEND_PIDS_LIMIT=128TRUST_PROXY_HEADERS=true should only be enabled when the backend is behind a trusted reverse proxy.
The current architecture is intentionally optimized for a small self-hosted deployment.
Important scaling dimensions include:
- number of active RTSP sources
- number of concurrent viewers
- source bitrate and resolution
- software transcoding CPU cost
- geographic distance between cameras, compute, and viewers
The current implementation runs one FFmpeg process per started stream record and transcodes to H.264 for predictable browser compatibility.
For larger deployments, likely evolution paths include:
- H.264 stream-copy when compatible with the source
- hardware-accelerated transcoding
- distributed stream workers
- external runtime/state coordination
- WebRTC with an SFU for interactive fan-out
- HLS or LL-HLS with CDN distribution for large viewer counts
The current in-memory stream registry is appropriate for the demo and single-node deployment but is not intended as durable distributed state.
rtsp-stream-console/
├── backend/
│ └── Go API, stream runtime, FFmpeg integration and WebSocket fan-out
├── frontend/
│ └── React + TypeScript + PatternFly application
├── deploy/
│ ├── compose/
│ │ └── local and production Docker Compose configuration
│ └── terraform/
│ └── azure/
│ └── reproducible Azure infrastructure
├── docs/
│ └── deeper architecture and operational documentation
├── scripts/
│ ├── deploy.sh
│ ├── deploy-azure.sh
│ └── development helpers
└── .github/
└── GitHub Actions workflows
Frontend React + TypeScript + PatternFly
Browser media mpegts.js
Backend Go
Stream processing FFmpeg
Media transport MPEG-TS over WebSockets
Demo RTSP server MediaMTX
Edge Caddy
Packaging Docker + Docker Compose
Infrastructure Terraform
Container registry GitHub Container Registry
Cloud example Microsoft Azure
RTSP Stream Console is under active development.
The current implementation has been exercised end to end with:
- HTTPS and automatic certificate management
- authenticated browser access
- RTSP playback over secure WebSockets
- multiple viewers sharing a stream runtime
- active-stream and viewer capacity enforcement
- slow-viewer eviction
- public RTSP destination filtering
- container resource limits
- Terraform-managed Azure infrastructure
- GitHub Actions-built GHCR images
- automated Azure application deployment
Versioned releases are verified by CI and published as Git tags and GHCR container images.
Licensed under the Apache License 2.0.
