How to install, configure, and operate Open Streamer end-to-end.
Companion docs: CONFIG.md (every config field + examples) · ARCHITECTURE.md (how it works) · APP_FLOW.md (pipeline + events) · FEATURES_CHECKLIST.md (what's implemented).
Latest release ships pre-built archives + Linux installer:
# Linux/systemd installer — downloads + verifies + installs as a service.
# Idempotent: re-running with a new tag uninstalls cleanly first.
sudo bash <(curl -sL https://raw.githubusercontent.com/ntt0601zcoder/open-streamer/main/build/install.sh) v4.0.0This installs the binary to /usr/local/bin/open-streamer, a systemd unit
to /etc/systemd/system/open-streamer.service, and creates the data dir
at /var/lib/open-streamer (preserved across version upgrades).
git clone https://github.com/ntt0601zcoder/open-streamer.git
cd open-streamer
make build # → bin/open-streamer
make run # run without persisting binaryRequires Go 1.25.x+. Transcoding is built on libavcodec (see § 2).
make docker-build # → open-streamer:local
make compose-up # docker compose upOpen Streamer transcodes in-process via libavcodec — a per-stream
open-streamer-transcoder subprocess driven over gRPC; ingest is pure Go.
There is no FFmpeg CLI in the media path.
The encoders the transcoder can use are fixed when the
open-streamer-transcoder binary is built against libav. The release
bundles and the builder image (Dockerfile.builder) ship:
- Always:
libx264(H.264 CPU),aac, MPEG-TS - GPU / extra:
h264_nvenc/hevc_nvenc(NVENC),libx265,libsvtav1,libopus,libmp3lame,ac3, HLS / DASH muxers
The POST /api/v1/config/transcoder/probe endpoint (UI "Test" button) is a
no-op that always reports ok.
Default storage is JSON flat-file under ./test_data/. To override
location or backend, set env vars BEFORE first boot:
export OPEN_STREAMER_STORAGE_DRIVER=yaml
export OPEN_STREAMER_STORAGE_YAML_DIR=/var/lib/open-streamer
./bin/open-streamerThe server starts unconfigured (no listeners, no streams) on first boot. Configure via the REST API or UI. To enable HTTP API, POST a server config:
curl -XPOST http://localhost:8080/api/v1/config -d '{
"server": { "http_addr": ":8080" },
"buffer": {},
"manager": {},
"publisher": { "hls": { "dir": "/var/hls" },
"dash": { "dir": "/var/dash" } },
"listeners": { "rtmp": { "enabled": true, "port": 1935 },
"rtsp": { "enabled": true, "port": 554 },
"srt": { "enabled": true, "port": 9999 } }
}'If the server isn't running on :8080 yet, initialize via the YAML
endpoint or pre-seed open_streamer.yaml in your data dir.
Streams are the central entity — each binds N inputs (with priority
failover), an optional transcoder ladder, output protocols, push
destinations, and DVR settings. Stream code accepts
[A-Za-z0-9_/-]+ — slashes namespace streams as
region/north/news. The route layer keeps the whole prefix as one
opaque key; .. and consecutive / are rejected.
Single-segment codes (news) use the live/ prefix; multi-segment
codes (region/north/news) use the raw path. The server strips a
leading live/ when present, so multi-segment codes also work via
live/region/north/news for non-canonical clients. Bare
single-segment URLs (e.g. rtmp://host/news) are rejected so a
half-typed URL can't accidentally hit a stream.
| Form | RTMP | RTSP | SRT |
|---|---|---|---|
Single-segment news |
rtmp://host/live/news |
rtsp://host/live/news |
srt://host?streamid=live/news |
Multi-segment region/north/news |
rtmp://host/region/north/news |
rtsp://host/region/north/news |
srt://host?streamid=region/north/news |
HLS / DASH delivery URLs (/{code}/index.m3u8, /{code}/index.mpd)
always use the raw code without live/.
curl -XPOST http://localhost:8080/api/v1/streams/news -d '{
"name": "News Channel",
"inputs": [
{ "url": "https://upstream.example.com/news/playlist.m3u8", "priority": 0 }
],
"protocols": { "hls": true }
}'Stream is up at http://localhost:8080/news/index.m3u8. No transcoding
— packets pass through untouched.
Add backup sources sorted by priority (lower = preferred):
curl -XPOST http://localhost:8080/api/v1/streams/news -d '{
"inputs": [
{ "url": "rtmp://primary.example.com/live/news", "priority": 0 },
{ "url": "rtmp://backup.example.com/live/news", "priority": 1 },
{ "url": "https://cdn.example.com/news/index.m3u8","priority": 2 }
],
"protocols": { "hls": true }
}'Stream Manager monitors the active input. On failure (configurable via
manager.input_packet_timeout_sec, default 30s) it switches to the
next-priority input within ~150ms — without restarting the transcoder. The
switch is recorded in runtime.switches[].
When the higher-priority input recovers (background probe succeeds), the manager fails back automatically. Override manually:
curl -XPOST http://localhost:8080/api/v1/streams/news/inputs/switch \
-d '{ "priority": 0 }'curl -XPOST http://localhost:8080/api/v1/streams/news -d '{
"transcoder": {
"global": { "hw": "nvenc" },
"audio": { "codec": "aac", "bitrate": 128 },
"video": {
"profiles": [
{ "width": 1920, "height": 1080, "bitrate": 5000, "codec": "h264" },
{ "width": 1280, "height": 720, "bitrate": 2500, "codec": "h264" },
{ "width": 854, "height": 480, "bitrate": 1200, "codec": "h264" }
]
}
},
"protocols": { "hls": true, "dash": true }
}'The transcoder subprocess decodes the source once and fans out to every
profile (one encoder per rendition), writing each into its own buffer. The
HLS master playlist auto-aggregates them at /news/index.m3u8. DASH MPD at
/news/index.mpd.
curl -XPOST http://localhost:8080/api/v1/streams/news -d '{
"push": [
{
"url": "rtmp://rtmp.example.com/live2/STREAM_KEY",
"enabled": true
},
{
"url": "rtmps://rtmps.example.com:443/rtmp/STREAM_KEY",
"enabled": true,
"limit": 10,
"retry_timeout_sec": 30
}
]
}'Per-destination state visible at runtime.publisher.pushes[] —
status (starting / active / reconnecting / failed), attempt
counter, last 5 errors, connected_at timestamp.
Per-stream opt-in:
curl -XPOST http://localhost:8080/api/v1/streams/news -d '{
"dvr": {
"enabled": true,
"segment_duration": 4,
"retention_sec": 604800,
"max_size_gb": 100
}
}'Storage path defaults to ./out/dvr/{streamCode}. Recording resumes
across restarts using the on-disk playlist.m3u8 + index.json.
#EXT-X-DISCONTINUITY markers on every gap (signal loss + server
downtime).
VOD playback — full recording:
GET /recordings/news/playlist.m3u8
Timeshift — last 30 minutes:
GET /recordings/news/timeshift.m3u8?offset_sec=-1800&duration=1800
Timeshift — absolute window:
GET /recordings/news/timeshift.m3u8?from=2026-04-26T10:00:00Z&duration=3600
A Template is a reusable bundle of config-like stream fields —
Transcoder, Protocols, Push, DVR, Watermark, Thumbnail, Inputs, Tags,
StreamKey, plus the auto-publish Prefixes list. Streams reference at
most one template via the template field; every field the stream
leaves at its zero value inherits from the template. The
non-inheritable fields are code (per-stream identity) and disabled
(per-stream runtime toggle).
Template code accepts [A-Za-z0-9_-]+ — no / because templates live
in a flat namespace.
curl -XPOST http://localhost:8080/api/v1/templates/news_profile -d '{
"name": "News profile",
"description": "1080p + 720p ABR ladder, HLS + DASH, DVR enabled",
"transcoder": {
"global": { "hw": "nvenc" },
"audio": { "codec": "aac", "bitrate": 128 },
"video": {
"profiles": [
{ "width": 1920, "height": 1080, "bitrate": 5000, "codec": "h264" },
{ "width": 1280, "height": 720, "bitrate": 2500, "codec": "h264" }
]
}
},
"protocols": { "hls": true, "dash": true },
"dvr": { "enabled": true, "retention_sec": 86400 }
}'curl -XPOST http://localhost:8080/api/v1/streams/news -d '{
"name": "News Channel",
"template": "news_profile",
"inputs": [
{ "url": "https://upstream.example.com/news/playlist.m3u8", "priority": 0 }
]
}'The stream now inherits Transcoder, Protocols, and DVR from
news_profile. The on-disk record still carries only code, name,
template, and inputs — clean and minimal. The operator can see
the inherited fields by fetching the template separately:
curl http://localhost:8080/api/v1/templates/news_profileAny non-zero value on the stream wins. Example: same template but a stream that needs an extra 480p rung and disables DASH.
curl -XPOST http://localhost:8080/api/v1/streams/sports -d '{
"template": "news_profile",
"inputs": [
{ "url": "rtmp://primary.example.com/live/sports", "priority": 0 }
],
"transcoder": {
"global": { "hw": "nvenc" },
"audio": { "codec": "aac", "bitrate": 128 },
"video": {
"profiles": [
{ "width": 1920, "height": 1080, "bitrate": 5000 },
{ "width": 1280, "height": 720, "bitrate": 2500 },
{ "width": 854, "height": 480, "bitrate": 1200 }
]
}
},
"protocols": { "hls": true }
}'Note: the override REPLACES the inherited value completely — the
stream's transcoder carries the full 3-rung ladder, the template's
2-rung ladder is not concatenated.
POST /templates/{code} on an existing template walks every running
stream that inherits from it and dispatches the same diff-based
coordinator.Update used by POST /streams/{code}. Stopped streams
skip the reload — the next start picks up the new template.
# Add 480p to the template — every running stream that doesn't
# already override `transcoder` picks up the new rung.
curl -XPOST http://localhost:8080/api/v1/templates/news_profile -d '{
"transcoder": {
"video": {
"profiles": [
{ "width": 1920, "height": 1080, "bitrate": 5000 },
{ "width": 1280, "height": 720, "bitrate": 2500 },
{ "width": 854, "height": 480, "bitrate": 1200 }
]
}
},
"protocols": { "hls": true, "dash": true },
"dvr": { "enabled": true, "retention_sec": 86400 }
}'Templates can declare a prefixes list. When an encoder pushes (RTMP)
to a path matching one of the prefixes on a segment boundary AND the
template has a publish:// input, the server materialises a runtime
stream on the fly — no POST /streams/{code} needed beforehand.
curl -XPOST http://localhost:8080/api/v1/templates/auto_news -d '{
"name": "Auto-news template",
"prefixes": ["live"],
"inputs": [{ "url": "publish://" }],
"protocols": { "hls": true }
}'Now an encoder pushing to rtmp://host:1935/live/foo triggers a new
runtime stream with code live/foo (the FULL incoming path, not just
the suffix). The stream appears in GET /streams with
source: "runtime". Runtime streams are RAM-only — they are NOT
persisted and disappear 30 s after the last packet reaches the buffer
hub. Stop the encoder → the reaper tears down the pipeline.
curl http://localhost:8080/api/v1/streams
# {
# "data": [
# { "code": "live/foo", "template": "auto_news",
# "source": "runtime", "runtime": { ... } },
# ...
# ]
# }Prefix rules:
- Prefix
livematcheslive/foo/barandliveitself but NOTlivestream/foo(segment boundary). - Prefixes are unique across templates —
POST /templates/{code}returns 409PREFIX_OVERLAPwhen any prefix is a path-prefix of another template's prefix (including equality). - Without at least one
publish://input on the template, prefix match rejects the push (debug-logged "matching template has no publish:// input").
DELETE /templates/{code} refuses with 409 TEMPLATE_IN_USE while
any stream still references it. The response lists the dependent stream
codes so the operator knows which to detach:
curl -XDELETE http://localhost:8080/api/v1/templates/news_profile
# 409 Conflict
# { "error": "TEMPLATE_IN_USE", "streams": ["news", "sports"], ... }
# Detach a stream by setting template: null, or pick another template
curl -XPOST http://localhost:8080/api/v1/streams/news -d '{ "template": null, "transcoder": {...}, "protocols": {...} }'GET /streams/{code} and GET /streams return the raw stored
record — overrides only, never merged with the template. Clients that
need the effective config fetch the template separately. Mixing the two
shapes would obscure which fields the operator actually set vs. which
come from the template.
PUT /streams/{code} (or repeat POST) merges only the changed fields:
# Add 360p rung — restarts the stream's transcoder subprocess (renditions share one decode).
curl -XPOST http://localhost:8080/api/v1/streams/news -d '{
"transcoder": {
"video": {
"profiles": [
{ "width": 1920, "height": 1080, "bitrate": 5000 },
{ "width": 1280, "height": 720, "bitrate": 2500 },
{ "width": 854, "height": 480, "bitrate": 1200 },
{ "width": 640, "height": 360, "bitrate": 800 }
]
}
}
}'The diff engine handles 5 categories independently — see ARCHITECTURE.md § Coordinator.
To force a full pipeline restart (e.g. switch stream key):
curl -XPOST http://localhost:8080/api/v1/streams/news/restartTo remove a stream entirely:
curl -XDELETE http://localhost:8080/api/v1/streams/newsSubscribe to lifecycle events with one of two delivery backends:
# HTTP webhook — events ship as a JSON ARRAY (batched). HMAC signing
# covers the entire array body when `secret` is set.
curl -XPOST http://localhost:8080/api/v1/hooks -d '{
"id": "log-everything",
"type": "http",
"target": "https://ops.example.com/streams/events",
"secret": "shared-secret-for-hmac",
"enabled": true,
"max_retries": 5,
"timeout_sec": 10,
// HTTP batching knobs — leave 0 to use server-wide defaults.
"batch_max_items": 50, // ship after every 50 events
"batch_flush_interval_sec": 2, // ... or every 2 seconds, whichever first
"batch_max_queue_items": 20000 // memory cap if target goes down
}'
# File sink — appends one JSON event per line. NOT batched (one event per
# write) so log shippers (Filebeat / Vector / Promtail) tail-and-ship one
# line at a time.
curl -XPOST http://localhost:8080/api/v1/hooks -d '{
"id": "audit-log",
"type": "file",
"target": "/var/log/open-streamer/events.log",
"enabled": true,
"event_types": ["stream.started", "stream.stopped", "input.failover"]
}'- Each event is enqueued to a per-hook in-memory buffer (~µs latency).
- A flusher goroutine ships the buffer when EITHER
batch_max_itemsis reached ORbatch_flush_interval_secelapses since the last flush. - POST body is a JSON array (
[{event1},{event2},…]). TheX-OpenStreamer-Batch-Sizeheader reports the array length so receivers can tune ingestion. max_retriesretries within a single flush attempt (with 1s/5s/30s backoff). If all retries fail, events re-queue at the FRONT of the buffer for the next flush — chronological order preserved.batch_max_queue_itemscaps the buffer when the downstream is unreachable. Overflow drops the OLDEST events with a warning log.- On graceful server shutdown (SIGTERM), every batcher gets one last best-effort flush.
The file backend creates the target on first delivery (mode 0644). The
parent directory must already exist and be writable by the open-streamer
process. Concurrent deliveries on the same path serialise via a
per-target mutex; different paths run in parallel. Each line is one
complete JSON event followed by \n — the same envelope the HTTP
backend wraps in an array.
Filter by event type or stream code:
{
"event_types": ["input.failover", "transcoder.error"],
"stream_codes": { "only": ["news", "sports"] }
}Test delivery:
curl -XPOST http://localhost:8080/api/v1/hooks/log-everything/testHook server-wide settings (worker pool size) are at
global_config.hooks — see CONFIG.md. Per-hook
defaults: 3 retries, 10s timeout (overridden via Hook fields above).
For the full event catalogue see APP_FLOW.md § Events reference.
Re-stream another in-process stream as input. No network round-trip; the downstream stream subscribes directly to the upstream's published buffer.
{
"code": "news_backup",
"inputs": [
{ "url": "copy://news", "priority": 0 }
],
"protocols": { "hls": true }
}If news has an ABR ladder, news_backup mirrors every rendition into
a parallel ABR ladder (no transcoding, just buffer subscriptions). This
is ABR-copy mode — the coordinator detects it and bypasses ingest +
transcoder entirely.
If news has no ladder, copy://news subscribes to its main playback
buffer — single-rendition copy.
Cycle detection prevents copy://A ↔ copy://B infinite loops via the
copy-graph validator at save time.
{
"code": "tv_with_radio",
"inputs": [
{ "url": "mixer://tv_silent?audio=radio_fm", "priority": 0 }
],
"protocols": { "hls": true }
}Video tracks come from tv_silent, audio tracks from radio_fm.
Useful for muting a TV broadcast and overlaying a radio commentary.
Both streams must already exist; mixer:// validates at save time
(audio= param required, no self-mix, no nested mixer chains).
Apply text or image overlays to the encoded video. Two-step workflow for image watermarks: upload to the asset library, then reference by asset ID from a stream.
curl -XPOST http://localhost:8080/api/v1/watermarks?name=ChannelLogo \
-F "file=@channel-logo.png"
# 201 Created
# {
# "data": {
# "id": "8a3f1c0e2b9d",
# "name": "ChannelLogo",
# "file_name": "channel-logo.png",
# "content_type":"image/png",
# "size_bytes": 4521,
# "uploaded_at": "2026-04-28T15:00:00Z"
# }
# }PNG / JPG / GIF supported. Cap: 8 MiB per asset. Library stays under
watermarks.dir (default ./watermarks).
curl http://localhost:8080/api/v1/watermarks | jq . # list
curl http://localhost:8080/api/v1/watermarks/8a3f1c0e2b9d/raw -o /tmp/preview.png
curl -XDELETE http://localhost:8080/api/v1/watermarks/8a3f1c0e2b9dImage watermark referencing the uploaded asset:
curl -XPOST http://localhost:8080/api/v1/streams/news -d '{
"watermark": {
"enabled": true,
"type": "image",
"asset_id": "8a3f1c0e2b9d",
"position": "top_right",
"offset_x": 30, "offset_y": 30,
"opacity": 0.85
}
}'Text watermark with a live clock:
curl -XPOST http://localhost:8080/api/v1/streams/news -d '{
"watermark": {
"enabled": true,
"type": "text",
"text": "LIVE %{localtime\\:%H\\:%M}",
"font_size": 28,
"font_color": "white",
"opacity": 0.9,
"position": "bottom_right",
"offset_x": 20, "offset_y": 20
}
}'Custom position (raw libavfilter expression — full power):
curl -XPOST http://localhost:8080/api/v1/streams/news -d '{
"watermark": {
"enabled": true,
"type": "image",
"asset_id": "8a3f1c0e2b9d",
"position": "custom",
"x": "main_w-overlay_w-50",
"y": "if(gt(t,5),10,-100)"
}
}'The y example slides the watermark in from above the frame after 5
seconds — useful for animated brand intros without an external editor.
| Position | Where | offset_x / offset_y mean |
|---|---|---|
top_left |
upper-left corner | inward padding from edge |
top_right |
upper-right corner | inward padding |
bottom_left |
lower-left corner | inward padding |
bottom_right |
lower-right corner (default) | inward padding |
center |
exact frame centre | offsets ignored |
custom |
wherever your x / y expression evaluates |
offsets ignored |
GPU pipelines (NVENC) automatically round-trip via CPU for the watermark filter — costs ~5% CPU per rendition; no operator action needed. The filter graph is built per rendition so every variant draws the watermark independently.
Changing any watermark field on a stream that is currently transcoding restarts the transcoder pipeline (~2-3s downtime per stream):
- Buffer hub keeps running; rendition buffers are recreated on the new start path
- HLS / DASH / RTMP / SRT / RTSP viewers see one
#EXT-X-DISCONTINUITY(HLS) or equivalent gap, then resume - DVR pauses for the gap and adds a
DVRGapentry
This is necessary because the libavfilter graph is fixed when the encoder is built — there's no way to live-swap it without restarting the subprocess. The diff engine routes any watermark-related field change through the topology-reload path so the new filter graph applies.
Watermark on a passthrough stream (no transcoder) is silently ignored — no transcoder is running so the filter graph never exists. Enable transcoding to get a server-side watermark; otherwise overlay client-side in your player.
Open Streamer tracks every active player across all delivery protocols (HLS / DASH / RTMP / SRT / RTSP). State is in-memory only — restart loses records, viewers reconnect into fresh sessions.
Sessions tracking is opt-in — POST a sessions config section:
curl -XPOST http://localhost:8080/api/v1/config -d '{
"sessions": { "enabled": true, "idle_timeout_sec": 30 }
}'Hot-reloadable: toggling enabled or changing idle_timeout_sec
takes effect on the next reaper tick (≤ 5s) without restart.
# All active sessions
curl http://localhost:8080/api/v1/sessions | jq .
# Per-stream
curl http://localhost:8080/api/v1/streams/news/sessions | jq .data
# Filter by protocol
curl 'http://localhost:8080/api/v1/sessions?proto=hls' | jq .dataEach session record includes:
{
"id": "abc123…", // fingerprint or UUID
"stream_code": "news",
"proto": "hls", // hls|dash|rtmp|srt|rtsp
"ip": "203.0.113.42",
"user_agent": "Mozilla/5.0 …",
"country": "VN", // when GeoIP wired
"bytes": 1481726,
"opened_at": "2026-04-28T15:01:00Z",
"updated_at": "2026-04-28T15:04:13Z",
"duration_sec": 193
}The list response also includes aggregate stats (active /
opened_total / closed_total / idle_closed_total / kicked_total).
curl -XDELETE http://localhost:8080/api/v1/sessions/abc123…
# 204 No ContentIdempotent — calling delete on an already-closed session returns 404.
Emits EventSessionClosed with reason=kicked on the event bus —
hooks subscribed to the event see the kick in the same channel as
organic disconnects.
curl -XPOST http://localhost:8080/api/v1/hooks -d '{
"id": "viewer-analytics",
"type": "file",
"target": "/var/log/open-streamer/sessions.log",
"event_types": ["session.opened", "session.closed"]
}'The bus carries an event for every open/close — feed Loki, ClickHouse, or your analytics pipeline. The sessions package stays in-memory; long- term persistence is the hook's responsibility.
- Fingerprint sessions (HLS / DASH) collapse repeated GETs from one
viewer onto one record while the idle window is open. Two viewers
behind shared NAT without a
?token=…will merge into one session — add a token query param to disambiguate (the tracker stores it as the user_name andnamed_by="token"). - RTMP / SRT / RTSP are connection-bound — one TCP/UDP session per record, closed exactly when the transport ends.
- RTSP bytes are always 0: gortsplib's mux is internal and there's no per-subscriber hook today. Other counters are accurate.
- GeoIP field is empty unless an operator wires a custom resolver —
the default
NullGeoIPalways returns"". Thegeoip_db_pathconfig field is reserved for the future MaxMind integration.
GET /healthz— liveness (always 200 if process is up)GET /readyz— readiness (200 once services initialised)
Prometheus scrape endpoint at GET /metrics:
manager_failovers_total{stream_code}— rate of input switchestranscoder_restarts_total{stream_code}— transcoder subprocess crash counttranscoder_workers_active{stream_code}— running profile countmanager_input_health{stream_code, input_priority}— 1 healthy, 0 degraded- Buffer depth, bytes/packets per stream
curl http://localhost:8080/api/v1/streams/news | jq .data.runtimeReturns:
status—active/degraded/stopped/idlepipeline_active— boolactive_input_priority— current sourceexhausted— true when all inputs degradedinputs[]— per-input health + last 5 errorsswitches[]— last 20 active-input switches with reasontranscoder.profiles[]— per-rung restart count + errorspublisher.pushes[]— per-destination state
A stream is degraded when EITHER:
- All inputs exhausted (manager: no failover candidate), OR
- Transcoder is in a crash loop (3 consecutive subprocess crashes < 30s apart). Recovers automatically when it runs > 30s sustained.
curl http://localhost:8080/api/v1/config/yaml > backup.yaml
# edit backup.yaml...
curl -XPUT http://localhost:8080/api/v1/config/yaml \
-H "Content-Type: application/yaml" \
--data-binary @backup.yamlRound-trips GlobalConfig + all streams + all hooks. Useful for ops-as-code workflows.
slog structured output to stderr. Configurable via log.level (debug
/ info / warn / error) and log.format (text / json). Common ops grep:
journalctl -u open-streamer | grep -E '(failover|crashed|degraded)'| Symptom | Likely cause | Where to look |
|---|---|---|
Status degraded while inputs healthy |
Transcoder is in a crash loop on at least one rung | runtime.transcoder.profiles[].errors[] (UI or /streams/{code}) |
| Encoder rejects an option | Codec/preset typo, or option unsupported by the resolved encoder | Stream config + runtime.transcoder.profiles[].errors[] |
| Stream up but HLS 404 | publisher.hls.dir is empty, or transcoder not producing output |
Server logs (publisher: HLS disabled — … etc.) |
Push stuck in reconnecting |
Destination reject, auth fail, or network loss | runtime.publisher.pushes[].errors[] |
| GPU encoder near 100% saturation | NVENC chip overloaded — reduce profile count / framerate, or set bframes=0 |
nvidia-smi or Grafana GPU dashboard |
copy://X save rejected |
X doesn't exist, OR cycle detected, OR shape constraint violated |
API error message details which check failed |
| Encoder unavailable at transcode start | The open-streamer-transcoder binary lacks that encoder (fixed at build time) |
Server logs / runtime.transcoder.profiles[].errors[]; rebuild against a libav with the encoder (see Dockerfile.builder) |
/sessions returns empty after enabling |
Tracker config not persisted (UI didn't save) OR sub-section absent before restart | curl /api/v1/config | jq .sessions to verify; restart once after first enabling |
Watermark POST returns INVALID_WATERMARK |
image_path and asset_id both set, OR custom position with empty x/y, OR opacity outside [0,1] |
API error message names the rule; fix the config JSON |
| Watermark image not appearing on output | Asset deleted while stream running, OR image_path not absolute / unreadable by the open-streamer user |
Server logs (coordinator: watermark asset resolve failed); chown the asset / re-upload |
For deeper troubleshooting see APP_FLOW.md — covers exact event sequences, status reconciliation logic, and what each error in the runtime snapshot means.
-
open-streamer-transcoderbinary built with the required encoders (seeDockerfile.builder) - HLS + DASH dirs are different (when both enabled)
- HTTP server bind address chosen — reverse proxy in front for TLS
- Storage backend chosen (
jsonflat-file oryamlsingle-doc) - Hooks configured for at least
stream.stopped+transcoder.errorso ops gets paged on crashes - DVR retention sized against disk capacity
-
manager.input_packet_timeout_sectuned per-protocol (HLS pull bursts may need ≥ segment duration × 2) - Prometheus scrape configured against
/metrics - Pre-commit hook installed for contributors (
make hooks-install)
# Use the provided installer for atomic upgrades on systemd hosts:
sudo bash build/install.sh v4.0.0Stops the service, swaps the binary + systemd unit, restarts. Data directory (streams, recordings, hooks) is preserved across upgrades.
For source builds: git pull && make build && systemctl restart open-streamer.
- CONFIG.md — every config field explained, with examples
- ARCHITECTURE.md — design rationale, data flow, invariants
- APP_FLOW.md — pipeline lifecycle, event sequences, status reconciliation
- FEATURES_CHECKLIST.md — what's implemented vs planned