Complete configuration
Start from a maximal module composition or inspect every field accepted by the current binary.
How to use this page
The maximal composition is a practical deployment template. It enables every module that can run together and leaves configured outbound push targets empty because cascade input and static push-all targets cannot share one node yet.
The field reference below names every accepted configuration field. Commented alternatives are mutually exclusive with the active line beside them. Remove modules and optional values that the deployment does not need.
See the MOQT output and relay guide for the media profile, exact-track routing contract, capacity ownership, and operational surfaces behind the configuration fields.
Private alpha: match this page to the binary version you deploy. Unknown fields fail startup, and configuration fields may still change before a stable release.
Draft with Map
Use Map to draw a topology and export per-node configuration drafts for Server v0.8.0. Review each draft against the binary and environment you deploy. Map does not run or deploy a cluster.
Open Map
Connect media nodes, inspect their settings and export configuration drafts.
Maximal composition
This template combines SRT push, managed pull, demand-driven cascade input, HLS, LL-HLS, WebRTC, native MOQT publication, local thumbnails, recording, S3 publication, cascade output, push control, transcode repair and webhooks. Set every referenced environment value and provide the external services before starting the process. This composition requires a valid full license and an FFmpeg-capable Server image; see licensing and image variants.
{
"license": {"file": "/run/secrets/dispergo-license.json"},
"server": {
"operational": {"host": "0.0.0.0", "port": 9091},
"shutdownTimeout": "10s"
},
"input": {
"mediaTimeout": "15s",
"srt": {
"push": {
"enabled": true,
"listener": {
"host": "0.0.0.0",
"port": 6001,
"passphrase": "${INGEST_SRT_PASSPHRASE}"
},
"peerIdleTimeout": "5s"
},
"pull": {
"api": {"enabled": true},
"static": [{
"streamId": "preview",
"source": "source.internal:7001",
"srtStreamId": "preview",
"passphrase": "${SOURCE_SRT_PASSPHRASE}"
}],
"maxActivePulls": 8,
"maxTombstones": 100
},
"cascade": {
"enabled": true,
"staticEndpoint": "upstream.internal:6102",
"passphrase": "${CASCADE_UPSTREAM_PASSPHRASE}",
"viewerIdleTimeout": "1m",
"maxOnDemandStreams": 64
}
}
},
"output": {
"root": "/var/lib/dispergo",
"lowWaterMarkBytes": 5368709120,
"http": {
"listener": {
"host": "0.0.0.0",
"port": 8080,
"maxConnections": 2048
},
"hls": {"enabled": true, "segmentDuration": "6s"},
"llhls": {"enabled": true, "windowDuration": "5m"},
"thumbnails": {"enabled": true},
"webrtc": {
"enabled": true,
"network": {
"udpPort": 50000,
"tcpPort": 50001,
"ipFamilies": ["ipv4"],
"candidateMode": "direct"
},
"security": {
"mode": "bearer",
"credentials": [{
"principal": "browser-viewers",
"token": "${WHEP_TOKEN}",
"streams": ["*"]
}],
"allowedOrigins": ["https://player.example.com"]
},
"ice": {
"urls": ["turns:turn.example.com:5349?transport=tcp"],
"turnSharedSecret": "${TURN_SHARED_SECRET}",
"internetReady": true
},
"timeouts": {"bootstrap": "2s"},
"admission": {
"maxSessions": 256,
"maxSessionsPerStream": 64,
"maxBootstrap": 64,
"maxBootstrapPerStream": 16
},
"rates": {
"bootstrapGlobal": {"perSecond": 64, "burst": 128},
"bootstrapScoped": {"perSecond": 4, "burst": 8}
},
"tracing": {"sampleRate": 0.005},
"audio": {"ffmpegPath": "ffmpeg", "maxWorkers": 4}
}
},
"moqt": {
"enabled": true,
"native": {"host": "0.0.0.0", "port": 4443},
"tls": {
"certificateFile": "/run/secrets/moqt-cert.pem",
"privateKeyFile": "/run/secrets/moqt-key.pem"
},
"authorization": {
"mode": "bearer",
"bearerTokenType": 0,
"credentials": [{
"principal": "moqt-viewers",
"token": "${MOQT_TOKEN}",
"permissions": [
{"operation": "subscribe", "namespacePrefix": ["live"]},
{"operation": "fetch", "namespacePrefix": ["live"]}
]
}]
},
"routes": {
"localPeerId": "origin.london.internal",
"publishers": [{
"streamId": "preview",
"namespace": ["live", "preview"],
"catalogNamespace": "live.preview",
"videoBitrate": 2000000,
"videoFrameRate": 25,
"audioBitrate": 128000
}]
},
"format": {"profile": "moqt-21"}
},
"srt": {
"cascade": {
"enabled": true,
"listener": {
"host": "0.0.0.0",
"port": 6002,
"passphrase": "${CASCADE_DOWNSTREAM_PASSPHRASE}"
},
"registry": {
"redisAddress": "redis.internal:6379",
"advertiseEndpoint": "node-a.internal:6002",
"keyPrefix": "dispergo:provide:"
}
},
"push": {
"api": {"enabled": true},
"maxActivePushes": 8,
"maxTombstones": 100
}
},
"thumbnails": {
"enabled": true,
"publish": true,
"interval": "10s",
"resize": {"width": 400, "height": 225},
"sessionMaxBytes": 1073741824,
"sessionMaxArtifacts": 10000,
"retention": "0s",
"ffmpegPath": "ffmpeg",
"maxWorkers": 4
},
"recording": {
"enabled": true,
"chunkDuration": "6s",
"retention": "0s",
"publish": true
},
"s3": {
"enabled": true,
"bucket": "${S3_BUCKET}",
"prefix": "media",
"region": "${AWS_REGION}",
"maxConcurrentUploads": 64,
"maxConcurrentThumbnailUploads": 8
}
},
"transcode": {
"enabled": true,
"hevcInput": false,
"videoTarget": {
"schemaVersion": 1,
"codec": "h264",
"h264": {
"profile": "constrained-baseline",
"level": "3.1",
"pixelFormat": "yuv420p",
"scan": "progressive",
"scaling": "preserve-no-upscale",
"maxWidth": 1280,
"maxHeight": 720,
"maxFrameRate": 30,
"maxBFrames": 0,
"maxKeyframeInterval": "2s"
}
},
"encoderProfile": "cpu",
"ffmpegPath": "ffmpeg",
"maxWorkers": 1,
"maxAddressSpaceBytes": 4294967296,
"queueSize": 0,
"policy": {
"defaultMode": "auto",
"streams": {"tenant:legacy": "always"}
}
},
"webhook": {
"enabled": true,
"url": "${WEBHOOK_URL}",
"authToken": "${WEBHOOK_TOKEN}",
"requestTimeout": "5s",
"maxAttempts": 5,
"queueSize": 256,
"maxConcurrentDeliveries": 4,
"shutdownFlushTimeout": "2s",
"streamState": {"enabled": true},
"llHlsState": {"enabled": true},
"thumbnailAvailable": {"enabled": true}
}
}
All fields
This JSONC document makes every compatible field explicit. Values are illustrative bounds or current defaults; measure worker capacity on the deployment hardware. The canonical target example allows one worker and a 4 GiB address-space ceiling, not a throughput guarantee. The commented alternatives name fields that cannot be active beside the selected form. The full output.s3 object is also accepted as the legacy output.recording.s3 alias, but the two paths cannot coexist. That alias is recording-only and cannot enable thumbnail publication.
{
"license": {"file": "/run/secrets/dispergo-license.json"},
"server": {
"operational": {
"host": "0.0.0.0",
"port": 9091
},
"shutdownTimeout": "10s"
},
"input": {
"mediaTimeout": "15s",
"srt": {
"push": {
"enabled": true,
"listener": {
"host": "0.0.0.0",
"port": 6001,
"passphrase": "${INGEST_SRT_PASSPHRASE}"
// Alternative after removing passphrase:
// "allowAnonymous": true
},
"peerIdleTimeout": "5s"
},
"pull": {
"defaults": {
"connectionTimeout": "5s",
"backoffBase": "1s",
"backoffMultiplier": 2,
"backoffCap": "1m",
"retryDeadline": "5m"
},
"api": {
"enabled": true
},
"static": [{
"streamId": "preview",
"source": "source.internal:7001",
"srtStreamId": "preview",
"passphrase": "${SOURCE_SRT_PASSPHRASE}",
"connectionTimeout": "5s",
"backoffBase": "2s",
"backoffMultiplier": 2,
"backoffCap": "1m",
"retryDeadline": "5m"
}],
"maxActivePulls": 8,
"maxTombstones": 100
},
"cascade": {
"enabled": true,
"staticEndpoint": "upstream.internal:6102",
"passphrase": "${CASCADE_UPSTREAM_PASSPHRASE}",
"viewerIdleTimeout": "1m",
"maxOnDemandStreams": 64
// Alternative to staticEndpoint:
// "registry": {
// "redisAddress": "redis.internal:6379",
// "keyPrefix": "dispergo:resolve:"
// }
}
}
},
"output": {
"root": "/var/lib/dispergo",
"lowWaterMarkBytes": 5368709120,
"http": {
"listener": {
"host": "0.0.0.0",
"port": 8080,
"maxConnections": 2048
},
"hls": {
"enabled": true,
"segmentDuration": "6s",
"maxSegmentBytes": 67108864,
"maxSegments": 7,
"minSegments": 3,
"maxTotalBytes": 1073741824,
"bandwidth": 10000000
},
"llhls": {
"enabled": true,
"cascadePublicEnabled": true,
"segmentTargetDuration": "1s",
"segmentHardDuration": "2s",
"partTargetDuration": "200ms",
"partHardDuration": "400ms",
"partPublishedTargetDuration": "340ms",
"partHoldBack": "680ms",
"windowDuration": "5m",
"maxSegments": 300,
"minSegments": 3,
"partWindowDuration": "15s",
"maxSegmentBytes": 67108864,
"maxPartBytes": 8388608,
"maxTotalBytes": 1073741824,
"bandwidth": 10000000,
"blockingReloadMaxWait": "8s",
"initialPlaylistMaxWait": "8s"
},
"thumbnails": {"enabled": true},
"webrtc": {
"enabled": true,
"network": {
"udpPort": 50000,
"tcpPort": 50001,
"udpBindIPs": ["10.0.0.10"],
"tcpBindIPs": ["10.0.0.10"],
"ipFamilies": ["ipv4"],
"interfaces": ["eth0"],
"localCIDRs": ["10.0.0.0/24"],
"candidateMode": "rewrite",
"addressRewrites": [{
"localCIDR": "10.0.0.0/24",
"externalIP": "198.51.100.10"
}],
"disconnectedTimeout": "5s",
"failedTimeout": "25s",
"keepAliveInterval": "5s",
"candidateGatherTimeout": "3s",
"tcpFirstStunTimeout": "5s",
"tcpUnknownUfragTimeout": "30s",
"viewerDrainTimeout": "5s",
"certificateRotationLead": "24h"
},
"security": {
"mode": "bearer",
"credentials": [{
"principal": "browser-viewers",
"token": "${WHEP_TOKEN}",
"streams": ["*"]
}],
"allowedOrigins": ["https://player.example.com"],
"trustedProxyCIDRs": ["10.0.0.0/8"]
},
"timeouts": {
"setup": "10s",
"bootstrap": "2s",
"body": "2s",
"patch": "2s",
"delete": "2s",
"idle": "30s",
"maxLifetime": "6h"
},
"admission": {
"maxSessions": 256,
"maxSessionsPerStream": 64,
"maxSessionsPerPrincipal": 32,
"maxSessionsPerAnonymous": 8,
"maxHalfOpen": 64,
"maxHalfOpenPerStream": 16,
"maxHalfOpenPerPrincipal": 8,
"maxHalfOpenPerAnonymous": 4,
"maxBootstrap": 64,
"maxBootstrapPerStream": 16,
"maxBootstrapPerPrincipal": 8,
"maxBootstrapPerAnonymous": 4
},
"rates": {
"postGlobal": {"perSecond": 64, "burst": 128},
"postScoped": {"perSecond": 4, "burst": 8},
"bootstrapGlobal": {"perSecond": 64, "burst": 128},
"bootstrapScoped": {"perSecond": 4, "burst": 8},
"patchGlobal": {"perSecond": 512, "burst": 1024},
"patchScoped": {"perSecond": 40, "burst": 80},
"patchSession": {"perSecond": 10, "burst": 20},
"deleteGlobal": {"perSecond": 256, "burst": 512},
"deleteScoped": {"perSecond": 20, "burst": 40},
"maxKeyedBuckets": 4096,
"bucketIdle": "10m"
},
"ice": {
"urls": ["turns:turn.example.com:5349?transport=tcp"],
"turnSharedSecret": "${TURN_SHARED_SECRET}",
"credentialTTL": "7h",
"allowedClockSkew": "30s",
"internetReady": true
},
"audio": {
"ffmpegPath": "ffmpeg",
"maxWorkers": 4,
"maxStartups": 2,
"gracePeriod": "10s",
"startupTimeout": "5s",
"restartBudget": 3,
"restartWindow": "1m",
"backoffBase": "250ms",
"backoffCap": "5s",
"maxSubscribers": 256,
"subscriberFrames": 8,
"inputQueueSamples": 8,
"inputQueueBytes": 262144,
"stallTimeout": "5s"
},
"tracing": {
"sampleRate": 0.005
}
}
},
"moqt": {
"enabled": true,
"native": {
"host": "0.0.0.0",
"port": 4443
},
"webTransport": {
"listener": {
"host": "0.0.0.0",
"port": 4444
},
"path": "/moqt"
},
"tls": {
"certificateFile": "/run/secrets/moqt-cert.pem",
"privateKeyFile": "/run/secrets/moqt-key.pem",
"clientCAFile": "/run/secrets/moqt-client-ca.pem"
},
"authorization": {
// Closed-network alternative: replace mode and credentials with
// "mode": "anonymous",
// "anonymousPermissions": [
// {"operation": "subscribe", "namespacePrefix": ["live"]},
// {"operation": "fetch", "namespacePrefix": ["live"]}
// ]
"mode": "bearer",
"bearerTokenType": 0,
"credentials": [{
"principal": "moqt-viewers",
"token": "${MOQT_TOKEN}",
"permissions": [
{"operation": "subscribe", "namespacePrefix": ["live"]},
{"operation": "fetch", "namespacePrefix": ["live"]},
{"operation": "subscribe", "namespacePrefix": ["relay"]},
{"operation": "fetch", "namespacePrefix": ["relay"]}
]
}]
},
"capacity": {
"maxSessions": 256,
"maxSessionsPerPeer": 16,
"admissionRatePerSecond": 64,
"admissionBurst": 16,
"maxConcurrentRequests": 128,
"maxConcurrentRequestsPerPeer": 32,
"requestRatePerSecondGlobal": 512,
"requestBurstGlobal": 128,
"requestRatePerSecondPerPeer": 64,
"requestBurstPerPeer": 16,
"maxTrackedRequestPeers": 1024
},
"routes": {
"localPeerId": "edge.paris.internal",
"topologyVersion": 1,
"publishers": [{
"streamId": "preview",
"namespace": ["live", "preview"],
"catalogNamespace": "live.preview",
"videoBitrate": 10000000,
"videoFrameRate": 25,
"audioBitrate": 128000
}],
"peers": [{
"id": "origin.internal",
"nativeAddress": "origin.internal:4443",
"serverName": "origin.internal",
"caFile": "/run/secrets/moqt-origin-ca.pem",
"certificateFile": "/run/secrets/moqt-relay-cert.pem",
"privateKeyFile": "/run/secrets/moqt-relay-key.pem"
}],
"relays": [{
"id": "relay-demo-catalog",
"presentationId": "demo",
"role": "catalog",
"peerId": "edge.paris.internal",
"upstreamPeerId": "origin.internal",
"namespace": ["relay", "demo"],
"trackName": "catalog",
"followCatalog": true,
"catalogNamespace": "relay.demo",
"token": "${MOQT_UPSTREAM_TOKEN}"
}],
"retry": {
"maxAttempts": 0,
"attemptTimeout": "10s",
"backoffBase": "100ms",
"backoffCap": "30s",
"multiplier": 2
}
},
"cache": {
"maxPublications": 256,
"maxRoutes": 256,
"maxAttachmentsPerRoute": 1024,
"maxRetainedObjects": 256,
"maxRetainedBytes": 33554432,
"maxQueuedObjects": 256,
"maxQueuedBytes": 33554432,
"maxHistoryEntries": 256,
"maxHistoryBytes": 262144,
"maxCodecBytes": 1048576,
"maxPreparedBatches": 8
},
"timeouts": {
"setup": "5s",
"idle": "1m",
"data": "10s",
"shutdown": "10s"
},
"format": {
"profile": "moqt-21"
}
},
"srt": {
"cascade": {
"enabled": true,
"listener": {
"host": "0.0.0.0",
"port": 6002,
"passphrase": "${CASCADE_DOWNSTREAM_PASSPHRASE}"
// Alternative after removing passphrase:
// "allowAnonymous": true
},
"registry": {
"redisAddress": "redis.internal:6379",
"advertiseEndpoint": "node-a.internal:6002",
"keyPrefix": "dispergo:provide:"
}
},
"push": {
"defaults": {
"connectionTimeout": "5s",
"backoffBase": "1s",
"backoffMultiplier": 2,
"backoffCap": "1m",
"retryDeadline": "5m",
"payloadSize": 1316,
"peerLatency": "120ms",
"receiverLatency": "120ms",
"sendBufferSize": 1048576,
"flowControlWindow": 8192
},
"api": {
"enabled": true
},
"static": [
// Remove input.srt.cascade before enabling this target.
// {
// "name": "archive",
// "address": "archive.internal:7001",
// "srtStreamId": "{streamId}",
// "passphrase": "${ARCHIVE_SRT_PASSPHRASE}",
// "connectionTimeout": "5s",
// "backoffBase": "2s",
// "backoffMultiplier": 2,
// "backoffCap": "1m",
// "retryDeadline": "5m",
// "payloadSize": 1316,
// "peerLatency": "120ms",
// "receiverLatency": "120ms",
// "sendBufferSize": 1048576,
// "flowControlWindow": 8192
// }
],
"maxActivePushes": 8,
"maxTombstones": 100
}
},
"thumbnails": {
"enabled": true,
"publish": true,
"interval": "10s",
"resize": {
"width": 400,
"height": 225
},
"sessionMaxBytes": 1073741824,
"sessionMaxArtifacts": 10000,
"retention": "0s",
"ffmpegPath": "ffmpeg",
"maxWorkers": 4
},
"recording": {
"enabled": true,
"chunkDuration": "6s",
"maxChunkBytes": 67108864,
"sessionMaxBytes": 10737418240,
"sessionMaxArtifacts": 10000,
"retention": "0s",
"publish": true
},
"s3": {
"enabled": true,
"bucket": "${S3_BUCKET}",
"prefix": "media",
"region": "${AWS_REGION}",
"endpoint": "https://s3.example.net",
"forcePathStyle": false,
"requestTimeout": "2m",
"multipartAttemptTimeout": "5m",
"multipartThresholdBytes": 134217728,
"multipartPartSizeBytes": 16777216,
"maxConcurrentUploads": 64,
"maxConcurrentThumbnailUploads": 8
}
},
"transcode": {
"enabled": true,
"hevcInput": false,
"videoTarget": {
"schemaVersion": 1,
"codec": "h264",
"h264": {
"profile": "constrained-baseline",
"level": "3.1",
"pixelFormat": "yuv420p",
"scan": "progressive",
"scaling": "preserve-no-upscale",
"maxWidth": 1280,
"maxHeight": 720,
"maxFrameRate": 30,
"maxBFrames": 0,
"maxKeyframeInterval": "2s"
}
},
"encoderProfile": "cpu",
"ffmpegPath": "ffmpeg",
"gracePeriod": "10s",
"maxWorkers": 1,
"queueSize": 0,
"stallTimeout": "5s",
"noMediaTimeout": "8s",
"maxRuntime": "12h",
"restartBudget": 5,
"restartWindow": "5m",
"backoffBase": "1s",
"backoffCap": "1m",
"sourceBufferBytes": 262144,
"maxAddressSpaceBytes": 4294967296,
"maxOpenFiles": 256,
"policy": {
"defaultMode": "auto",
"streams": {
"tenant:legacy": "always",
"tenant:trusted": "never"
}
}
},
"webhook": {
"enabled": true,
"url": "${WEBHOOK_URL}",
"authToken": "${WEBHOOK_TOKEN}",
"requestTimeout": "5s",
"maxAttempts": 5,
"queueSize": 256,
"maxConcurrentDeliveries": 4,
"shutdownFlushTimeout": "2s",
"streamState": {"enabled": true},
"llHlsState": {"enabled": true},
"thumbnailAvailable": {"enabled": true}
}
}
Composition constraints
- Use either
passphraseorallowAnonymouson one SRT listener, never both. - Use either
input.srt.cascade.staticEndpointor itsregistry, never both. - Give cascade input and output registries distinct
keyPrefixvalues when both run in one process. - Do not combine cascade input with configured
output.srt.push.statictargets. The push API remains available. - Keep
transcode.stallTimeoutandtranscode.noMediaTimeoutbelowinput.mediaTimeout. - Enabling
output.s3requires at least one ofoutput.recording.publishoroutput.thumbnails.publish. Both flags default tofalseand require their own module plus the shared S3 destination to be enabled. S3 does not require recording when thumbnails publish. - Do not combine
output.s3with the legacyoutput.recording.s3object. An enabled legacy alias implies recording publication only; it does not enable thumbnail publication. - Thumbnail publication requires local push or managed pull input. Cascade-only publication is a startup error; combining local and cascade inputs is valid.
- Enabling local HLS, LL-HLS, recording, thumbnails or
input.srt.cascaderequiresoutput.root. WebRTC with local input and MOQT do not require storage by themselves. Cascade-backed WHEP still requires the root for internal media storage, even withllhls.enabled: falseandcascadePublicEnabled: false. - Every node configuration requires
output.http.listener, including a MOQT-only relay. MOQT transport still uses its own UDP listeners. A MOQT relay without a local publisher can omit SRT input. The operational listener cannot use a nonzeromaxConnections; only the media listener has that configurable connection bound. - WebRTC requires
network.udpPort. Its UDP and optional TCP muxes must not overlap HTTP, operational, SRT ingest or cascade-output listeners on a shared bind address. - Anonymous WHEP is a closed-network mode. It cannot contain bearer credentials or TURN routes. Use bearer mode and explicit
allowedOriginswhen untrusted browser clients can reach the listener. - A TURN URL requires
turnSharedSecret.internetReadyalso requires aturns:route, andcredentialTTLmust outlive bootstrap, setup, maximum session lifetime and allowed clock skew. - MOQT requires at least one dedicated native QUIC or WebTransport UDP listener, TLS certificate and key files, explicit authorization, and one publisher or relay route. Its listeners must not overlap each other or another configured listener.
- MOQT defaults to
moqt-21, negotiating draft-21/20. Setformat.profiletomoqt-22to offer draft-22/21/20 on both carriers and outbound relay connections. Bearer tokens must resolve to 32 to 512 printable ASCII bytes. Anonymous permissions should be limited to a closed network and namespace prefix. - MOQT relay routes require a nonzero
topologyVersion, an operator-controlledpresentationId, and acatalogortrackrole. Each peer presentation requires exactly one catalog route. Catalog routes follow advertised media and timeline tracks by default;followCatalog: falseretains static exact-track mode. OptionalcatalogNamespacepermits an exact explicit MSF namespace mapping only when catalog following is enabled. Readiness requires every currently required route, including discovered tracks. - The verified upstream certificate identity must equal
upstreamPeerId. Cache values can reduce hard ceilings but cannot raise them. - MOQT publisher metadata requires positive
videoBitrateandvideoFrameRatefor video, plusaudioBitratefor AAC audio. Match the source; these values do not configure an encoder. - MOQT relay retry defaults to continuous recovery with capped backoff. Omit
routes.retry.maxAttemptsor use0; positive values from 1 through 64 select a finite unstable sequence. A stable active interval of at leastbackoffCapresets that sequence. transcode.videoTargetis an explicit versioned H.264 target. Deployment-widepolicy.defaultMode: "always"requires it;autoretains measured bypass.hevcInputandvideoTargetcannot be enabled together.- JPEG generation does not enable HTTP serving. Opt into
output.http.thumbnails.enabledseparately; it requires enabled thumbnail generation and local push or managed pull. output.http.llhls.cascadePublicEnableddefaults to enabled when omitted. Set it tofalseto hide cascade LL-HLS playback while preserving internal media ownership for other outputs such as WHEP.- Webhook stream state, LL-HLS readiness and thumbnail availability have independent opt-ins:
streamState.enabled,llHlsState.enabledandthumbnailAvailable.enabled.