Appearance
Configuration Reference
Minimal working configuration config.yaml:
yaml
record:
format: fmp4
buffer_size_mb: 4
segmentDuration: 900
cameraDefaults:
record_path: "./recordings/%path/%Y-%m-%d_%H-%M-%S_%f.mp4"
record: true
rtsp:
enabled: true
port: 8554
cameras:
camera_001:
source: "rtsp://admin:password@192.168.1.100:554/stream1"Configuration File
By default, ForgeVis looks for config.yaml in the current directory. You can specify a different file:
bash
./forgevis my-config.yamlSettings
cameraDefaults
Default settings for all cameras. Can be overridden per camera. Any camera setting from Cameras except source, sub and snapshot can be set here.
record_path
Path template for recordings with placeholders:
%path— camera identifier (key from thecamerasmap)%Y,%m,%d,%H,%M,%S— the segment's start time: 4-digit year, 2-digit month, day, hour, minute, second%f— fragment number within the segment (6 digits, zero-padded)
Example: ./recordings/%path/%Y-%m-%d_%H-%M-%S_%f.mp4 becomes./recordings/camera_001/2025-12-04_22-00-00_000000.mp4
To change the path: create the directory, give it to the service user (chown forgevis:forgevis <directory>), change record_path and restart the service. Recordings under the old path are not seen in the archive — it is looked up by the new template; move them into the new directory with the same layout, or leave them where they are.
retention_days
How many days of recordings to keep:
yaml
cameraDefaults:
retention_days: 30Recordings are deleted by the forgevis --retention command, not by the service — put it in cron. With no retention_days nothing is deleted. See Archive retention.
record
Enable or disable recording by default for all cameras:
record(boolean, optional, inherited from defaults) — whentrue, camera is recorded unless overridden per camera
To disable recording by default, set record: false:
yaml
cameraDefaults:
record: false # disable recording for all camerasreconnect
Pauses between attempts to connect to a camera that does not answer or dropped its stream. One schedule for the node — recording, HLS, the RTSP restream and viewers; failures are counted separately for each camera's main stream and substream. The defaults:
yaml
reconnect:
healthy_reset_sec: 30
jitter_percent: 20
backoff:
- { delay_sec: 0, attempts: 1 }
- { delay_sec: 5, attempts: 5 }
- { delay_sec: 10, attempts: 5 }
- { delay_sec: 15, attempts: 5 }
- { delay_sec: 30, attempts: 0 }backoff— steps in order:attemptsattempts with a pause ofdelay_secseconds before each.attempts: 0on the last step means "from here on"; the last step's pause must be above zerojitter_percent(0 to 100) — each pause is stretched or shortened at random by up to this share, so cameras that went down together do not reconnect in the same secondhealthy_reset_sec— how many seconds a stream has to run for its failure count to reset
A pause is the time between attempts; an attempt itself takes up to 15 seconds. During a pause the node leaves the camera alone, and a request for its stream is refused at once with the cause and the time of the next attempt. Changing a camera's settings starts the count afresh. The schedule is applied without restarting the service.
record
Global recording settings:
yaml
record:
format: fmp4
buffer_size_mb: 4
segmentDuration: 900
runOnSegmentCreate: "./hooks/on-create.sh"
runOnSegmentComplete: "./hooks/on-complete.sh"format(string, defaultfmp4) — recording format:fmp4(fragmented MP4, playable while recording) ormp4(plain MP4)buffer_size_mb(integer, default 4) — writer buffer size per camera (MB)segmentDuration(integer, default 900) — segment duration in secondsrunOnSegmentCreate(string, optional) — shell command executed when a new segment startsrunOnSegmentComplete(string, optional) — shell command executed when a segment is finalizedrunPeriodically— periodic hook:command,intervalSec(default 60),maxConcurrent(default 50). See Event Hooks
Hook environment variables:
SF_PATH— camera IDSF_SEGMENT_PATH— full segment file pathSF_SEGMENT_DURATION— segment duration in seconds (only forrunOnSegmentComplete)
RTSP Server
Configuration for restreaming server:
enabled(boolean, default true) — enable/disable RTSP serverport(integer, default 8554) — TCP port for RTSP serverudp_rtp_port(integer, default 8000) — UDP port for RTP packetsudp_rtcp_port(integer, default 8001) — UDP port for RTCP packetsidle_timeout_sec(integer, default 10) — disconnect from camera after N seconds of no clients
The RTSP server supports both TCP (interleaved) and UDP transports. The restream URLs will be: rtsp://localhost:8554/{camera_id}
Management API
Configuration for the HTTP management API:
yaml
api:
enabled: true
address: "0.0.0.0:9997"
allowOrigin: "*"
rateLimit:
enabled: true
perMinute: 600
burst: 60
archiveJobTimeoutSec: 3600
archiveExportDir: "/var/lib/forgevis/exports"enabled(boolean, default true) — enable Management APIaddress(string, default "0.0.0.0:9997") — listen address and portallowOrigin(string, default "*") — CORS allowed origin.*is any origin, otherwise one or several separated by commas (https://a.example, https://b.example); a value that is not an origin is ignored with a warning in the lograteLimit.enabled(boolean, default true) — per-IP throttling on APIrateLimit.perMinute(int, default 600) — sustained rate per IPrateLimit.burst(int, default 60) — token bucket capacity (initial burst)archiveJobTimeoutSec(int, default 3600) — longest one ffmpeg run of an archive export or timelapse may take, in seconds; a run past it is stopped and the job failsarchiveExportDir(string, default/var/lib/forgevis/exports) — directory holding finished export and timelapse files, along with the parts an assembling node downloads from its peers. Created on the first job, accessible to the service alone. The path is absolute: these files run to gigabytes, and a relative one would depend on the directory the service was started from
When the bucket is empty the API returns 429 Too Many Requests. The defaults are intentionally generous for internal-network deployments; tighten if you expose the API to less-trusted networks.
tls
TLS for every HTTP listener on the node at once: the management API, HLS, WebRTC signalling, archive playback and metrics.
yaml
tls:
enabled: false
cert: /etc/forgevis/tls/node.pem
key: /etc/forgevis/tls/node.keyenabled(boolean, defaultfalse) — serve every HTTP listener over HTTPScert(string) — PEM certificate chainkey(string) — PEM private key, PKCS#8 or PKCS#1
One certificate for the node rather than one per subsystem: a browser that loaded the interface over https will not fetch an HLS playlist or a WHEP endpoint over http.
enabled: true without a readable cert and key fails the startup. The certificate is read once at startup, so replacing it needs a restart.
For the details — issuing a self-signed certificate, and how this differs from cluster.tls — see Security.
Stream Security
Optional access control for live streams, API and metrics. By default, if the security block is omitted or contains no users, all requests are allowed as in previous versions.
yaml
security:
auth_method: internal
users:
# Local admin: API and metrics from localhost without credentials
- user: any
pass:
ips: ["127.0.0.1/32", "::1/128"]
permissions:
- action: api
- action: metrics
# Viewer for all live streams (RTSP / HLS / WebSocket)
- user: viewer
pass: "change-me"
ips: []
permissions:
- action: readauth_method(string, defaultinternal) — authentication backend; currently onlyinternalis implemented.users— list of user rules:user— username, special valueanymeans any (anonymous) user.pass— optional password. Supports plain text orsha256:<base64_sha256>format (ignored whenuser: any).ips— optional list of IPs or CIDRs allowed for this user (empty = any IP).permissions— list of permissions:action— one ofpublish,read,api,metrics.path— optional camera/path restriction (empty or omitted = any).
lockout— protection against password guessing:attempts(number, default 10) — failed sign-ins in a row;0turns the protection off.duration_sec(number, default 300) — how many seconds the user is then locked out from that address.
SHA256 credentials
If you don't want to store plain credentials in config, hash them with SHA256 and Base64:
bash
echo -n "mypass" | openssl dgst -binary -sha256 | openssl base64Then use sha256: prefix in config:
yaml
security:
auth_method: internal
users:
- user: sha256:jGl25bVBBBW96Qi9Te4V37Fnqchz/Eu4qB9vKrRIqRg=
pass: sha256:6nHCWnpgIka0w5gkuFVniJSpb0O7m3ExnDlwCh4EUiI=
permissions:
- action: readFor full examples and migration strategy, see Security & Authentication.
When at least one user is defined in security.users:
- all live read endpoints (RTSP restreaming, HLS, WebSocket
/websocket/{camera_id}) require a matching permission withaction: read(and optionalpathfor camera scoping), and - the
/metricsendpoint requires a permission withaction: metrics.
The Management API then requires a permission with action: api.
WebSocket Streaming
Configuration for Low-Latency MSE Streaming (WebSocket):
yaml
websocket:
enabled: true
allowOrigin: "*"enabled(boolean, default true) — enable WebSocket low-latency streamingallowOrigin(string, default "*") — pages allowed to open the WebSocket. Browsers apply no CORS to a WebSocket, so the node checks theOriginheader itself and answers403to a page not on the list; a request withoutOrigin(not from a browser) is let through.*is any page, otherwise one or several origins separated by commas
WebRTC
Configuration for WebRTC signaling and browser playback:
yaml
webrtc:
enabled: true
address: ":8889"
allowOrigin: "*"
localUdpAddress: ":8189"
localTcpAddress: ""
ipsFromInterfaces: true
ipsFromInterfacesList: []
additionalHosts: []
iceServers2:
- url: "stun:stun.l.google.com:19302"
username: ""
password: ""
client_only: false
handshakeTimeout: "10s"
trackGatherTimeout: "2s"
stunGatherTimeout: "5s"enabled(boolean, defaultfalse) — enable WebRTC serveraddress(string, default:8889) — WebRTC signaling HTTP listen addressallowOrigin(string, default*) — CORS allowed origin for signaling endpoints.*is any origin, otherwise one or several separated by commas (https://a.example, https://b.example); a value that is not an origin is ignored with a warning in the loglocalUdpAddress(string, default:8189) — local UDP bind address for ICElocalTcpAddress(string, default empty) — local TCP bind address for ICE TCPipsFromInterfaces(boolean, defaulttrue) — include host candidates from local interfacesipsFromInterfacesList(string[], default[]) — allowlist of interfaces used for host candidatesadditionalHosts(string[], default[]) — additional public/private host candidatesiceServers2— ICE servers list (STUN/TURN):url(string, required) — ICE server URL (e.g.stun:...,turn:...)username(string, optional) — username for TURN authpassword(string, optional) — password for TURN authclient_only(boolean, defaultfalse) — expose this ICE server only to clients
handshakeTimeout(string, default10s) — max WebRTC handshake durationtrackGatherTimeout(string, default2s) — max wait for track/media readinessstunGatherTimeout(string, default5s) — max wait for STUN candidate gathering
For endpoint usage and browser flow, see WebRTC Guide.
HLS
Browser viewing over HLS. In detail, with the quality choice and the 720p step — HLS Streaming.
yaml
hls:
enabled: true
address: ":8888"
variant: lowLatency
segmentCount: 7
segmentDuration: 1s
partDuration: 200ms
muxerCloseAfter: 60s
transcodeMaxConcurrent: 2enabled(boolean, defaultfalse) — enable the HLS serveraddress(string, default:8888) — listen address and portallowOrigin(string, not set by default) — value of theAccess-Control-Allow-Originheader; without it the header is not sent.*is any origin, otherwise one or several separated by commas (https://a.example, https://b.example); a value that is not an origin is ignored with a warning in the logalwaysRemux(boolean, defaultfalse) — keep HLS ready for every camera instead of starting it on the first requestvariant(string, defaultlowLatency) —lowLatency(Low-Latency HLS) orfmp4(plain HLS)segmentCount(integer, default 7) — how many segments the playlist keeps, no fewer than three target durationssegmentDuration(string, default1s) — minimum segment duration; a segment ends on a keyframe, so in practice it equals the camera's keyframe interval when that is longerpartDuration(string, default200ms) — Low-Latency HLS part durationmuxerCloseAfter(string, default60s) — how long without requests before a camera's HLS (and its 720p step) stopstranscodeMaxConcurrent(integer, default 2) — how many 720p steps a node may encode at once;0offers none
Playback
Archive recordings over HTTP, playable from any moment. In detail — Archive Playback.
enabled(boolean, defaultfalse) — enable the playback serveraddress(string, default:9996) — listen address and portallowOrigin(string, not set by default) — value of theAccess-Control-Allow-Originheader.*is any origin, otherwise one or several separated by commas (https://a.example, https://b.example); a value that is not an origin is ignored with a warning in the log
Metrics
Prometheus metrics. In detail — Monitoring.
enabled(boolean, defaultfalse) — enable the metrics endpointaddress(string, default:9998) — listen address and portallowOrigin(string, not set by default) — value of theAccess-Control-Allow-Originheader.*is any origin, otherwise one or several separated by commas (https://a.example, https://b.example); a value that is not an origin is ignored with a warning in the log
Logging
The service log. In detail — Monitoring.
directory(string, default/var/log/forgevis) — log file directorylevel(string, defaultinfo) —error,warn,info,debug,tracemaxSizeMb(integer, default 100) — log file size after which a new one startsmaxBackups(integer, default 10) — how many old files to keepconsole(boolean, defaulttrue) — also write the log to the console
Database
Cameras and recording metadata from PostgreSQL. In detail — Shared Database.
enabled(boolean, defaultfalse) — connect to the databaseurl(string) — connection string,postgres://user:pass@host:5432/dbwatch(boolean, defaulttrue) — apply camera changes in the database on the fly
Cluster
Several nodes with cameras moved on failure. In detail — Clustering & HA.
enabled(boolean, defaultfalse) — enable cluster modenode_id(integer) — the node's identifier in the clusterrpc_addr(string) — address nodes use to talk to each otherdataDir(string, default/var/lib/forgevis/data) — where the node keeps cluster state; an absolute pathtls.caCert,tls.cert,tls.key(strings) — certificates for node-to-node traffic
nodeName
Node name at the top level of the config (string, defaults to the system hostname). A node picks its cameras from the database by it (cameras.node_name), so two nodes must not share one.
Cameras
Map of camera configurations. Key is the camera identifier used in path templates and restream URLs.
source(string, required) — RTSP URL of the main stream, credentials includedsub(string, optional) — RTSP URL of the substreamsnapshot(string, optional) — HTTP URL of the camera's still image, served by/api/cameras/{id}/snapshot; the login is taken from this URL or, if it has none, fromsourcerecord(bool) — whether to record the cameraretention_days(number) — how many days of this camera's recordings to keep; see Archive retentionaudio(bool, defaultfalse) — record and serve audiortsp(bool, defaulttrue) — serve the camera through the RTSP serverhls(bool, defaulttrue) — serve the camera over HLSalwaysRemux(bool) — keep this camera's HLS ready at all times (overrideshls.alwaysRemux)ptz(bool, defaultfalse) — the camera has PTZ controlonvif.endpoint,onvif.username,onvif.password(strings) — ONVIF address and credentials for PTZ; by default the address is built from thesourcehost, and the login and password are taken fromsourceconnection_timeout_sec(number, default 5, 30 in a cluster) — how many seconds without subscribers before disconnecting from the cameranode_name(string) — which cluster node the camera should preferably run on
Any of these except source, sub and snapshot can be set for every camera in cameraDefaults.
Source URL Format
The source field must be a valid RTSP URL. Credentials are embedded in the URL:
rtsp://[username[:password]@]host[:port]/pathExamples:
yaml
# With authentication
source: "rtsp://admin:password123@192.168.1.100:554/stream1"
# Without authentication
source: "rtsp://192.168.1.101:554/stream1"
# Custom port
source: "rtsp://user:pass@camera.local:8554/live"Complete Example
yaml
# Global recording settings
record:
format: fmp4
buffer_size_mb: 4
segmentDuration: 900
# Default settings for all cameras
cameraDefaults:
record_path: "./recordings/%path/%Y-%m-%d_%H-%M-%S_%f.mp4"
record: true
audio: true # Enable audio by default
# RTSP server configuration
rtsp:
enabled: true
port: 8554
idle_timeout_sec: 300
# Camera configurations
cameras:
# HD camera with substream and audio
camera_001:
source: "rtsp://admin:secret@192.168.1.100:554/stream1"
sub: "rtsp://admin:secret@192.168.1.100:554/stream2"
audio: true # Record with audio
# Restream-only camera (recording disabled)
camera_002:
source: "rtsp://192.168.1.101:554/stream1"
record: false
# Camera with recording and audio
important_camera:
source: "rtsp://user:pass@192.168.1.102:554/h264"
record: true
audio: trueModular Configuration (conf.d)
For large deployments with many cameras, you can split camera configurations into separate files in the conf.d/ directory:
yaml
# conf.d/warehouse.yaml
warehouse_cam_01:
source: "rtsp://user:pass@192.168.1.200:554/stream1"
warehouse_cam_02:
source: "rtsp://user:pass@192.168.1.201:554/stream1"yaml
# conf.d/parking.yaml
parking_entrance:
source: "rtsp://user:pass@192.168.1.210:554/stream1"
sub: "rtsp://user:pass@192.168.1.210:554/stream2"Benefits:
- Modularity: One file per camera or location
- Organization: Group cameras by building, floor, or zone
- Scalability: Easy to add/remove cameras without editing main config
- Team workflow: Different people can manage different camera configs
- Security: Keep credentials separate (optional .gitignore)
Loading order:
- Cameras from
config.yamlare loaded first - Cameras from
conf.d/*.yamlare loaded alphabetically - If a camera ID appears in both,
conf.dversion takes precedence (with warning)
Best Practices
Storage
fmp4format is recommended (supports playback while recording)- Use hierarchical path structure:
/rpool/%path/%Y/%m/%d/stream-%path_%Y-%m-%d_%H-%M-%S.mp4 - Adjust segment duration based on use case:
- 60s for live monitoring
- 300-900s for archival storage
Cameras
- Use descriptive identifiers (e.g.,
front_door,parking_north) - Disable recording per camera with
record: false - Test RTSP URLs with ffprobe before adding to config
RTSP Server
- Default port 8554 is standard for RTSP
- Change port if conflicts occur
- Firewall: Allow TCP port for RTSP server
Hot Reload
ForgeVis automatically watches for configuration changes and applies them without restart.
Monitored Files
config.yaml- Main configuration fileconf.d/*.yaml- Individual camera configuration files
What Can Be Changed
- Add cameras - Create new YAML files in
conf.d/or add toconfig.yaml - Remove cameras - Delete camera files or remove from config
- Update camera sources - Change RTSP URL for existing cameras
- Enable/disable recording - Toggle
recordoption per camera
How It Works
- Configuration files are monitored for changes
- Changes are debounced (500ms) to handle rapid edits
- New configuration is loaded and validated
- Running cameras are compared with new configuration:
- Removed cameras are stopped and cleaned up
- Changed sources trigger camera restart
- New cameras are started automatically
- RTSP/HLS streams are updated immediately
Example Workflow
bash
# 1. Add new camera
echo "camera_003:
source: rtsp://admin:password@192.168.1.103:554/stream
sub: rtsp://admin:password@192.168.1.103:554/substream
" > conf.d/camera_003.yaml
# Configuration reloads automatically, camera_003 starts recording
# 2. Update camera source
# Edit conf.d/camera_002.yaml and save
# Camera restarts with new source automatically
# 3. Remove camera
rm conf.d/camera_001.yaml
# Camera stops, streams cleaned up, no longer accessibleStream Management
When a camera is removed:
- Recording stops gracefully
- RTSP clients are disconnected
- HLS muxers are cleaned up
- Stream becomes unavailable (returns 404)
- No restart required
Restreaming
Access camera streams via RTSP server:
bash
# Format
rtsp://localhost:{port}/{camera_id}
# Examples
ffplay -rtsp_transport tcp rtsp://localhost:8554/front_door
vlc rtsp://localhost:8554/parkingOperating Modes
The same binary can run in three distinct modes by flipping record and the rtsp server on/off.
Mode 1: Record only (no RTSP restreaming)
yaml
record:
format: fmp4
buffer_size_mb: 4
segmentDuration: 900
cameraDefaults:
record: true
rtsp:
enabled: false # restreaming disabled
cameras:
camera_001:
source: "rtsp://..."Mode 2: Restream only (no recording)
yaml
cameraDefaults:
record: false # disable recording for all cameras
record:
format: fmp4
buffer_size_mb: 4
segmentDuration: 900
rtsp:
enabled: true
port: 8554
cameras:
camera_001:
source: "rtsp://..."Mode 3: Record + restream
yaml
record:
format: fmp4
buffer_size_mb: 4
segmentDuration: 900
cameraDefaults:
record: true
rtsp:
enabled: true
port: 8554
cameras:
camera_001:
source: "rtsp://..."Validating the Configuration
ForgeVis validates the configuration on startup:
bash
./forgevis config.yamlWatch the logs for errors, including:
- Invalid RTSP URLs
- Inaccessible recording paths
- Duplicate
camera_identries - Malformed YAML