StreamForge is a self-hosted control layer for small, low-latency live and recording deployments. An OBS producer remains the source of truth for physical output, a durable control plane orders operator intent, and a pinned ZLMediaKit service handles ingest, protected playback, and MP4 recording. The repository contains the product code, deployment, tests, upstream locks, and a reproducible acceptance runner; it is not a hosted service.
operator / streamforgectl
|
| authenticated desired state, status, events, metrics
v
+-------------------+ hooks and reconciliation +------------------+
| control plane | <-----------------------------------> | ZLMediaKit |
| SQLite + FIFO | | RTMP / HTTP-FLV |
+-------------------+ | RTSP / MP4 |
^ +------------------+
| leased commands + acknowledged OBS facts |
| v
+-------------------+ finalized recordings
| OBS Dock adapter | + retention
| real frontend API | recording-pruner
+-------------------+
The implemented release provides:
- Authenticated, channel-scoped operator, producer, publish, and play roles.
- Atomic SQLite state transitions, ordered producer commands, idempotent operator mutations, bounded retention, admission limits, and reconciliation.
- A C++/Qt OBS Dock that invokes real OBS frontend streaming and recording APIs, reports physical output facts, and fails closed on stale lifecycles or unsafe identity changes.
- Pinned ZLMediaKit ingest with exact-origin publication policy, protected HTTP-FLV/RTSP playback, MP4 finalization, and a network-isolated retention worker that ignores dot-prefixed active files.
- Health and Prometheus-style metrics endpoints, a dependency-free operator CLI, Docker Compose deployment, and one-command acceptance coverage.
Requirements are Docker with Compose, Linux-container support, and PowerShell 7. From the repository root:
Copy-Item deploy/.env.example deploy/.envEdit deploy/.env and replace all six replace-* credential placeholders with
distinct, URL-safe random values of at least 16 characters. Do not reuse the
published ZLMediaKit default secret. Then validate and start the stack:
pwsh ./scripts/dev.ps1 -Action Config
pwsh ./scripts/dev.ps1 -Action StartThe local-safe defaults bind only to 127.0.0.1:
| Surface | Address | Purpose |
|---|---|---|
| Control API | http://127.0.0.1:8081 |
API, /docs, /health/ready, /metrics |
| HTTP media | http://127.0.0.1:8080 |
Protected HTTP-FLV playback |
| RTMP ingest | rtmp://127.0.0.1:1935 |
OBS publication |
| RTSP media | rtsp://127.0.0.1:8554 |
Protected RTSP playback |
Build and install the OBS component using the platform-specific instructions in
components/obs-plugin/README.md. Launch OBS
from an environment that supplies STREAMFORGE_PRODUCER_TOKEN and, if not using
the Dock fields, the control URL, channel, and stable agent ID.
For the default demo channel, configure the OBS streaming service as:
Server: rtmp://127.0.0.1:1935/live
Stream key: demo?token=<the STREAMFORGE_PUBLISH_TOKEN value from deploy/.env>
Set the Dock control URL to http://127.0.0.1:8081, channel to demo, and agent
to a stable identifier. Use the Dock's Start stream action: it first arms the
channel through the control plane, then the returned producer command starts
OBS. Directly pressing OBS's native Start Streaming button before the channel is
armed is intentionally rejected by the media hook.
An authenticated default playback URL is:
http://127.0.0.1:8080/live/demo.live.flv?token=<STREAMFORGE_PLAY_TOKEN>
The operator CLI reads its bearer credential from the environment rather than from process arguments. In PowerShell, enter the configured value without echoing it and query the running system:
$env:STREAMFORGE_OPERATOR_TOKEN = Read-Host -MaskInput 'Operator token'
python ./tools/streamforgectl.py --url http://127.0.0.1:8081 --channel demo status
python ./tools/streamforgectl.py --url http://127.0.0.1:8081 --channel demo events
python ./tools/streamforgectl.py --url http://127.0.0.1:8081 metricsStop the stack without deleting the named state and recording volumes:
pwsh ./scripts/dev.ps1 -Action StopCommit 0244cb1393210a3f4346534b529be249a57286c6 was exercised with exactly:
pwsh ./scripts/acceptance.ps1 -Case AllRun 20260904T194934Z-7c083dd6 completed on 2026-09-05 with 15/15 cases
PASS and scoped cleanup PASS. Both provenance snapshots record that exact
HEAD, git_dirty: false, head_is_exact_source: true, and source-tree SHA-256
bd07b1386bb62727ae015c65a1d0bd85c57c027f0fc881219b7560df304dd6a0;
the source remained unchanged throughout the run.
The run includes 221 control-plane tests, 2 operator CLI security tests, all 4
real-SDK OBS CTest targets, and both Qt-only CTest targets. A real OBS 30.0.2
process—not FFmpeg—published H.264/AAC and ACKed START_STREAM,
START_RECORDING, STOP_RECORDING, and STOP_STREAM. OBS finalized its own
445,666-byte, 4.400-second H.264/AAC MKV. The separate ZLMediaKit recording case
finalized a 2,047,917-byte, 7.433-second MP4 and exercised retention safety.
The five protected HTTP-FLV player-start samples were 1168.262, 1004.926,
1009.177, 979.333, and 993.148 ms (mean 1030.969 ms; nearest-rank p95
1168.262 ms). This metric ends when ffprobe receives its first selected video
packet. It is not capture-to-display or glass-to-glass latency.
- Human-readable record:
docs/ACCEPTANCE.md - Tracked machine-readable record:
docs/evidence/acceptance-manifest.json - Focused OBS-loop record:
docs/evidence/obs-loop-recording-manifest.json - Raw local summary (not shipped in public clones):
artifacts/acceptance/20260904T194934Z-7c083dd6/summary.json(62,948bytes; SHA-25637175512356dba5a38abea15905f9e88692bb3f61760acd0010fa6269e77a7f3)
Raw acceptance artifacts are intentionally Git-ignored because they include runtime logs and media. The tracked manifests carry the stable, secret-free release facts and raw-summary hash. The runner found zero credential leaks in 108 pre-redactor source-form checks; a separate read-only scan found zero raw credential hits across 519 evidence files.
| Path | Responsibility |
|---|---|
components/obs-plugin |
C++/Qt OBS producer adapter and four test targets |
services/control-plane |
HTTP API, SQLite state/command engine, hooks, reconciliation |
deploy |
Compose stack, pinned ZLMediaKit build, recording pruner |
tools/streamforgectl.py |
Secret-safe operator CLI |
scripts |
Local lifecycle and reproducible acceptance entrypoints |
tests/e2e |
End-to-end acceptance implementation |
docs |
Product boundary, upstream, and acceptance records |
upstreams.lock.json |
Audited upstream commits and local patch lock |
- The OBS source reference is
lite-tx/obs-studioata2400fe9803811b6e691a019e8bc2b1acfaf6250. No fork-only delta was identified. Formal acceptance compiled and ran against Ubuntu's packaged OBS 30.0.2 SDK and process; it did not build or execute the audited OBS 32 source commit. - ZLMediaKit is pinned at
9f90548a67df0a9f1425a1c884186bf48eb7be21; the applied hook-log patch is locked inupstreams.lock.json. - The automated OBS loop uses Xvfb, an empty scene, and a real OBS process. It
proves module loading, OBS-owned publication and local recording, all four
stream/record command acknowledgements, protected playback, finalized-file
probing, and clean stop—not interactive Dock ergonomics or a production
capture scene. The ZLMediaKit
Recordingcase remains a separate server-side MP4 proof. - No vulnerability scan, penetration test, legal clearance, HA/failover test, WAN test, or capture-to-display latency study is claimed. Before Internet exposure, add TLS termination, network policy, secret management, backups, monitoring, and an environment-specific security review.
See SECURITY.md,
docs/architecture/ADR-001-product-boundaries.md,
and docs/UPSTREAM_AUDIT.md for the detailed trust and
provenance boundaries.
StreamForge is distributed under GPL-2.0-or-later; see LICENSE.
The OBS-linked module therefore remains GPL-compatible. ZLMediaKit is deployed
as a separate service under its MIT-derived license and attribution conditions.
Source URLs, exact revisions, local patch provenance, and notices are recorded
in upstreams.lock.json and
THIRD_PARTY_NOTICES.md.