Released 2026-08-05. Fixes land directly on this app's feature surface: HLS muxer recomputes AAC PTS in MPEG-TS segments (iOS playback precision, relevant since the app's config sets hlsVariant: mpegts), fixes a goroutine leak during HLS part rotation, fixes a Chrome WebRTC "packet lost" false-positive and non-deterministic WebRTC track ordering (WHIP ingest), and fixes OBS multitrack RTMP URL parsing. No breaking config changes; new opt-in features (native forwarding, MoQ draft support) don't affect this config (moq: no already). Matches the same bump on the podsteadr repo's docker-compose.yml.
165 lines
5.9 KiB
YAML
165 lines
5.9 KiB
YAML
app:
|
|
id: podsteadr-mediamtx
|
|
name: podsteadr MediaMTX
|
|
version: "1.20.0"
|
|
description: MediaMTX ingest/output backend for podsteadr — RTMP + WebRTC/WHIP ingest, HLS playback, stream recording.
|
|
category: media
|
|
|
|
# Hyphenated name matches the podsteadr repo's docker-compose container_name
|
|
# (podsteadr-mediamtx); alias `mediamtx` is the short hostname podsteadr's
|
|
# server reaches it by (MEDIAMTX_API_URL=http://mediamtx:9997) and the one
|
|
# baked into mediamtx.yml's authHTTPAddress callback below.
|
|
container_name: podsteadr-mediamtx
|
|
|
|
container:
|
|
image: docker.io/bluenviron/mediamtx:1.20.0
|
|
pull_policy: if-not-present
|
|
network: podsteadr-net
|
|
network_aliases: [mediamtx]
|
|
derived_env:
|
|
# Browsers need a reachable ICE host candidate for WebRTC/WHIP; without
|
|
# this, the offer only advertises container-internal addresses and
|
|
# publish/playback negotiation fails for anyone off-host.
|
|
- key: MTX_WEBRTCADDITIONALHOSTS
|
|
template: "{{HOST_MDNS}}"
|
|
|
|
dependencies:
|
|
- storage: 10Gi
|
|
|
|
resources:
|
|
cpu_limit: 1
|
|
memory_limit: 512Mi
|
|
disk_limit: 10Gi
|
|
|
|
security:
|
|
# Stock mediamtx image runs as container-root (no USER directive) but
|
|
# only ever writes to the bind-mounted /recordings — CHOWN/DAC_OVERRIDE
|
|
# cover the fresh-bind-dir-ownership gap the same way apps/botfights and
|
|
# apps/immich document (root uid inside the container does not
|
|
# automatically bypass DAC checks once cap-drop ALL applies). Unverified
|
|
# against a real install; check first-boot logs on initial deploy.
|
|
capabilities: [CHOWN, DAC_OVERRIDE]
|
|
readonly_root: true
|
|
no_new_privileges: true
|
|
network_policy: isolated
|
|
|
|
ports:
|
|
- host: 1935
|
|
container: 1935
|
|
protocol: tcp
|
|
auth: none
|
|
auth_rationale: >-
|
|
RTMP ingest (OBS). Not HTTP, so the node's session gate has no login
|
|
page to serve here; publish auth is delegated to podsteadr's own
|
|
HTTP auth webhook (authHTTPAddress below), which checks a per-stream
|
|
secret key never exposed in this port mapping.
|
|
- host: 8889
|
|
container: 8889
|
|
protocol: tcp
|
|
auth: none
|
|
auth_rationale: >-
|
|
WebRTC/WHIP ingest — browsers publish directly with a per-stream
|
|
bearer secret checked by podsteadr's auth webhook, the same
|
|
protocol-level auth as the RTMP port above.
|
|
- host: 8189
|
|
container: 8189
|
|
protocol: udp
|
|
auth: none
|
|
auth_rationale: >-
|
|
WebRTC ICE/UDP media transport. Raw UDP has no HTTP session concept
|
|
for the gate to enforce.
|
|
- host: 8890
|
|
container: 8888
|
|
protocol: tcp
|
|
auth: none
|
|
auth_rationale: >-
|
|
Public HLS playback URL, handed out to viewers and podcast/livestream
|
|
clients outside the node (zap.stream, third-party players). A login
|
|
page here would break every external viewer; playback is read-only.
|
|
|
|
volumes:
|
|
- type: bind
|
|
# Shared with apps/podsteadr (mounted read-only there) so the app can
|
|
# list and remux finished recordings for one-click episode publishing.
|
|
source: /var/lib/archipelago/podsteadr/recordings
|
|
target: /recordings
|
|
options: [rw]
|
|
- type: bind
|
|
source: /var/lib/archipelago/podsteadr-mediamtx/config/mediamtx.yml
|
|
target: /mediamtx.yml
|
|
options: [ro]
|
|
|
|
environment: []
|
|
|
|
files:
|
|
- path: /var/lib/archipelago/podsteadr-mediamtx/config/mediamtx.yml
|
|
overwrite: true
|
|
content: |
|
|
# MediaMTX configuration for podsteadr.
|
|
# Ingest: RTMP (OBS) + WebRTC/WHIP (browser). Output: HLS. Publish auth is
|
|
# delegated to podsteadr via HTTP; stream status is polled from the API.
|
|
|
|
logLevel: info
|
|
|
|
api: yes
|
|
apiAddress: :9997
|
|
|
|
# ---- authentication ------------------------------------------------------
|
|
authMethod: http
|
|
authHTTPAddress: http://podsteadr-app:8095/api/mediamtx/auth
|
|
authHTTPExclude:
|
|
- action: api
|
|
- action: metrics
|
|
- action: pprof
|
|
|
|
# ---- protocols -----------------------------------------------------------
|
|
rtsp: no
|
|
srt: no
|
|
moq: no
|
|
|
|
rtmp: yes
|
|
rtmpAddress: :1935
|
|
|
|
hls: yes
|
|
hlsAddress: :8888
|
|
# Standard HLS, not lowLatency: LL-HLS's small per-part buffering window has very little
|
|
# tolerance for B-frame reordering (common in most OBS encoder presets), and a real test
|
|
# stream crashed the muxer twice in ~2 minutes with "too many reordered frames" / "unable to
|
|
# extract DTS" once frame timing got even slightly irregular. Standard HLS buffers a full
|
|
# segment before finalizing, which absorbs that jitter — a few extra seconds of latency
|
|
# instead of intermittent muxer crashes / viewer buffering.
|
|
hlsVariant: mpegts
|
|
hlsAlwaysRemux: yes
|
|
hlsAllowOrigins: ["*"]
|
|
|
|
webrtc: yes
|
|
webrtcAddress: :8889
|
|
webrtcLocalUDPAddress: :8189
|
|
webrtcAllowOrigins: ["*"]
|
|
|
|
# ---- recording -----------------------------------------------------------
|
|
pathDefaults:
|
|
record: yes
|
|
recordPath: /recordings/%path/%Y-%m-%d_%H-%M-%S-%f
|
|
recordFormat: fmp4
|
|
recordPartDuration: 1s
|
|
recordSegmentDuration: 1h
|
|
recordDeleteAfter: 168h
|
|
|
|
paths:
|
|
# Streams live at live/<streamId>; publish requires the stream secret,
|
|
# which podsteadr checks in the auth webhook.
|
|
"~^live/[A-Za-z0-9]+$": {}
|
|
|
|
health_check:
|
|
# Stock mediamtx image has no shell, so an in-container HTTP probe of the
|
|
# API isn't meaningfully cheaper than TCP; RTMP liveness is enough (same
|
|
# polling-not-hooks rationale as podsteadr's own status poller, which
|
|
# exists precisely because runOn*-style shell hooks aren't available on
|
|
# this image).
|
|
type: tcp
|
|
endpoint: localhost:1935
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|