go2rtc Config Override

May 17, 2026 · View on GitHub

LightNVR generates a go2rtc.yaml from your stream definitions and starts go2rtc with it. When you need to tune go2rtc beyond what the Settings page exposes — custom ffmpeg templates, trace logging, MQTT bridges, ICE/TURN servers, HomeKit, ngrok tunnels, and so on — you supply a config override.

How overrides are applied

LightNVR writes two YAML files into ${go2rtc_config_dir} (default /etc/lightnvr/go2rtc):

FileOwnerContents
go2rtc.yamlLightNVRAuto-generated api, rtsp, webrtc, ffmpeg, and streams sections derived from your Streams and Settings. Regenerated on every start.
override.yamlYou (Settings → "go2rtc Config Override")Whatever you save in the textarea. Mode 0600. Removed when the field is empty.

go2rtc is started with both files:

go2rtc --config /etc/lightnvr/go2rtc/go2rtc.yaml --config /etc/lightnvr/go2rtc/override.yaml

go2rtc loads them in order, calling yaml.Unmarshal once per file onto the same struct. This means your override is merged by go2rtc's own parser, not by lightNVR — there's nothing to escape, no string concatenation, no duplicate-key risk.

Why two files? Earlier versions appended your override to the bottom of go2rtc.yaml. If your override touched any section lightNVR also wrote (ffmpeg, log, streams, …), the resulting file had duplicate top-level keys and gopkg.in/yaml.v3 rejected the whole document — your override was silently dropped. Two files lets each parse cleanly on its own.

Merge semantics

We verified the runtime merge behavior with the actual go2rtc binary in tests/unit/test_go2rtc_two_config_merge.c:

  • Mappings are merged key-by-key. Top-level scalars in override.yaml override scalars in go2rtc.yaml.
  • Nested maps (e.g., ffmpeg.h264, mqtt.host) deep-merge.
  • The streams: map specifically is merged: cameras you add in override.yaml appear alongside lightNVR's auto-generated cameras. But if you redefine a stream name lightNVR already wrote (streams.cam1), your version wins (replace, not list-append).
  • Sequences (e.g., webrtc.ice_servers, publish.<name>) are replaced, not appended. If you set webrtc.ice_servers in your override, lightNVR's defaults are gone — list every server you want.

Supported sections

Anything go2rtc accepts works in your override. These are the top-level keys we know about; an unknown key is allowed (forward-compat) but produces a warning:

SectionPurpose
apiHTTP/WS API server: listen, username, password, base_path, tls_listen, tls_cert, tls_key, unix_listen
rtspRTSP server: listen, username, password, default_query
webrtcWebRTC: listen, candidates, ice_servers, filters
ffmpegTranscoding: bin, global, timeout, h264, h265, opus, custom codec/input templates
logLogging: level, format, output, plus per-module overrides (api, rtsp, …)
streamsStream definitions — merges with lightNVR streams; redefining a name replaces
publishRTMP/S push destinations
hassHome Assistant auto-import
mqttMQTT bridge
hlsHLS output server
srtpSRTP server
homekitHomeKit accessory bridge
ngrok / pinggyTunnels
echoDynamic-URL shell expansion
preloadAuto-start streams (name → filter spec)
appModule enable/disable

Validation

Two tiers of checks happen before your override ever reaches go2rtc:

  1. Live, on-blur — the Settings UI POSTs to /api/settings/go2rtc/validate 200ms after you stop typing. Errors are inlined under the textarea with line/column.
  2. Pre-savePOST /api/settings runs the same validator and rejects with HTTP 400 if it fails.

The validator catches:

  • YAML syntax errors (line/column reported)
  • Duplicate top-level keysgopkg.in/yaml.v3 rejects these but libyaml-C silently accepts them, so we walk the event stream ourselves
  • Non-mapping root (sequence or scalar at top level)
  • Unknown top-level sections (warning, not error)

When libyaml isn't compiled into the lightNVR build, validation is skipped and the UI shows a "validation skipped" badge — go2rtc itself will still error out at startup if the YAML is malformed.

Effective-config preview

Click Show effective config in the Settings page (or GET /api/system/go2rtc/effective-config) to see exactly what go2rtc was started with. Both files are returned with secrets redacted via a YAML-aware walker:

  • api.password, api.username, rtsp.password, rtsp.username, mqtt.password — replaced with <redacted>
  • webrtc.ice_servers[*].credential and [*].username — replaced with <redacted>
  • streams.* URL userinfo — rtsp://user:pass@host becomes rtsp://<redacted>@host (host/path stay visible for diagnostics)

Block-scalar passwords (password: |\n secret) are caught the same as inline scalars — the walker reads libyaml's resolved scalar value, not the source text. When libyaml isn't available, redaction is a no-op pass-through and the response sets redaction_available: false.

Crash-loop quarantine

A semantically-valid-but-runtime-broken override (e.g. api: { listen: ":99999" }) can put go2rtc into a crash loop. LightNVR detects this:

  • Track per-instance lifetime (g_last_start_time). An exit within 10s of start counts as a "fast death."
  • If 3+ fast-deaths happen within a 60s window AND override.yaml is in use, lightNVR:
    1. Renames override.yamloverride.quarantined.yaml
    2. Persists the last 2KB of go2rtc.log plus a header to system_settings.go2rtc_config_override_disabled_reason
    3. Restarts go2rtc with base config only
  • The Settings page surfaces the reason as a yellow banner above the editor with a one-click "Restore quarantined version into the editor" button.
  • Saving any new value to the override field clears the quarantine.

Upgrade-time quarantine

The first boot after each lightNVR release validates your existing override against the current rules. If it fails (most commonly because of an old append-era duplicate-key shape that was previously silently ignored):

  • The original is copied to system_settings.go2rtc_config_override_quarantined
  • The live go2rtc_config_override is cleared
  • The same banner appears, with the "Restore" button to repopulate the editor

A go2rtc_override_validated_version marker prevents re-checking on subsequent boots.

Troubleshooting

SymptomCheck
"No go2rtc binary available"LightNVR probes /bin/go2rtc, /usr/local/bin/go2rtc, /usr/bin/go2rtc, /opt/go2rtc/go2rtc, /rootfs/usr/local/go2rtc/go2rtc, /go2rtc, then $PATH. Each candidate is run as <path> --version (2 s timeout) and accepted only if stdout contains go2rtc version . The Settings → "go2rtc Binary Path" field overrides the probe.
Override saves but doesn't take effectCheck the banner — your override may be quarantined. Click "Show effective config" to confirm override.yaml is listed under merged_source_order.
"port already in use" after override saveYour override likely sets api.listen or rtsp.listen to a port another process holds. T4b will auto-quarantine after 3 fast crashes; check the banner.
ICE/STUN settings ignoredSequences are replaced, not merged. Your webrtc.ice_servers overwrites lightNVR's defaults — list every server you want.
Block-scalar password not redacted in previewMake sure libyaml is installed in your runtime container; the response field redaction_available tells you. The Docker images ship with libyaml and libyaml-dev.

Examples

Enable trace logging

log:
  level: trace

Pass camera streams through without re-encoding

ffmpeg:
  h264: "-codec:v copy -codec:a copy"
  h265: "-codec:v copy -codec:a copy"

Bridge to MQTT

mqtt:
  host: mqtt.example.com
  port: 1883
  username: lightnvr
  password: hunter2

Add a TURN server

webrtc:
  ice_servers:
    - urls: [stun:stun.l.google.com:19302]
    - urls: [turn:turn.example.com:3478]
      username: USER
      credential: PASS

Wyze cameras (stock firmware, no reflash)

go2rtc speaks Wyze's account API natively through the wyze:// source. Drop your developer API credentials into the override and go2rtc enumerates every camera on your Wyze account, fetches the per-camera DTLS keys, and connects to each camera directly over the LAN — Wyze's cloud is only touched for the initial key exchange.

# Account-wide credentials — fetched once at startup. Get api_id / api_key from
# https://developer-api-console.wyze.com/ ; password is your Wyze account password.
wyze:
  api_id: YOUR_WYZE_API_ID
  api_key: YOUR_WYZE_API_KEY
  password: YOUR_WYZE_ACCOUNT_PASSWORD

Save the override, restart go2rtc, then open the go2rtc web UI and click Suggest. Each Wyze camera on your account is returned as a fully-formed wyze:// URL:

wyze://10.0.1.242?dtls=true&enr=<encrypted-key>&mac=2CAA8E1460B5&model=WYZEC1-JZ&uid=11D6C733WYJZC2LA111A

You can either paste those URLs into Settings → Streams in the LightNVR dashboard, or define them config-as-code in the same override file:

streams:
  # Primary producer (the wyze:// URL from Suggest) plus optional ffmpeg shims
  # that expose hardware-decoded H.264 video and a separate AAC audio track.
  # Browsers and the LightNVR recorder consume whichever fits their codec needs.
  front_door:
    - wyze://10.0.1.242?dtls=true&enr=<...>&mac=<...>&model=WYZEC1-JZ&uid=<...>
    - ffmpeg:front_door#audio=aac
    - ffmpeg:front_door#video=h264#hardware

A few things worth knowing:

  • The streams: map merges with lightNVR's auto-generated entries — Wyze cameras you list here appear alongside any RTSP cameras you added through the Streams page (see Merge semantics).
  • Credentials are redacted in GET /api/system/go2rtc/effective-config and the Show effective config modal, but they sit in plaintext inside override.yaml (mode 0600 on disk). Don't share the override.
  • Wyze's DTLS keys rotate periodically. go2rtc handles re-keying automatically using your stored credentials. If reconnects start failing days later, regenerate the API key at the Wyze developer console.
  • Pan/tilt controls for the Wyze Pan v3 stay in the Wyze app — go2rtc only bridges the video/audio stream.

Full walkthrough on the marketing site: Connect Wyze Cameras to LightNVR Cloud.

Reference