Single-file HTML/JS admin page seeded into /core/data alongside
whep-player.html. Lets an operator log in with the API_AUTH_USERNAME
+ API_AUTH_PASSWORD creds, list every process, and toggle webrtc.enabled
per process with a single button. WHEP URL displayed for enabled
processes with a one-click "open in WHEP player" link.
Closes the v0.1 GUI gap: the upstream Restreamer UI we ship doesn't
know about Core's webrtc config block, so toggling WebRTC required
direct API calls. This page is the user-friendly path. Reachable at
/wilddragon-webrtc.html on any deploy.
No build step — drops in via the existing seed-data.sh flow.
Closes the v0.1 observability gap. Eleven new metrics in the
dragonfork_webrtc_* namespace (RED-method on the WHEP surface plus
state gauges from the WebRTC subsystem), Prom + Grafana containers
added to deploy/truenas/core/, four pre-loaded alert rules, one
pre-provisioned dashboard.
Hybrid instrumentation: direct client_golang in app/webrtc/ for
hot-path counters and histograms; snapshot collector in
prometheus/webrtc.go for slow-changing gauges. Rationale and
trade-offs against the upstream monitor/metric bus pattern documented
in the Approach section.
Targets v0.2.0-dragonfork.
The compose file's environment: block only forwarded the variables it
explicitly referenced — CORE_ADDRESS, CORE_API_AUTH_*, CORE_WEBRTC_*,
CORE_LOG_LEVEL. Everything else got the upstream Core defaults
regardless of what was in .env. So 'CORE_RTMP_ADDRESS=:1937' in .env
was silently ignored and Core kept binding 1935.
Hit on the live TrueNAS host where another datarhei/restreamer
container was already on 1935 with active stream state — couldn't
just stop it. Adding explicit env passthrough for the four common
collision points (RTMP, RTMPS, SRT, TLS) so an operator can remap
each individually without editing this file:
CORE_RTMP_ADDRESS=:1937
CORE_RTMP_ADDRESS_TLS=:1938
CORE_SRT_ADDRESS=:6002
CORE_TLS_ADDRESS=:8183
Defaults are unchanged — empty .env keeps :1935/:1936/:6000/:8181.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Layers Wild Dragon branding on top of upstream restreamer-ui v1.14.0
without forking the whole repo — keeps upstream UI updates flowing in
when we bump RESTREAMER_UI_REF.
Overlay (deploy/truenas/core/ui-overlay/):
public/index.html Wild Dragon title, theme color #0d0e12
public/manifest.json PWA name/short_name/colors
public/favicon.ico multi-res ICO (16/32/64) generated from
a 'WD' monogram in orange #ff6633 on dark
public/logo192.png Apple touch icon
public/logo512.png PWA install icon
src/misc/Logo/images/ rs-logo.svg (square mark, used in the
Header) and logo.svg (wordmark, used in
the Footer) — both Wild-Dragon-themed
src/misc/Logo/{index,rsLogo}.js
link the logos to forge.wilddragon.net
instead of datarhei.com
apply-overlay.sh runs in the Docker ui-builder stage just after the
upstream git clone and just before yarn install. Two phases:
1. rsync the overlay's public/ and src/ on top of the cloned
upstream tree
2. Targeted in-place patches for one-line UI strings (header
title, two welcome captions). Each patch is anchored to a
unique surrounding context and the script fails loudly if the
anchor isn't present — so a future upstream rename surfaces
immediately rather than silently shipping un-rebranded UI.
Image size: ~+50KB (the overlay assets), no measurable build-time
delta. PWA installs and OS bookmarks now show Wild Dragon. The
remaining 'Restreamer'/'datarhei' references in views/Welcome.js,
views/Login.js, views/Settings.js, etc. are deeper-page strings
that aren't worth a one-off overlay; they'll go away when we fork
the UI repo properly for the WebRTC tab milestone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The Restreamer UI bundle includes subdirectories (_player,
_playersite, static, locales) and the Dockerfile copies the whole
tree into /core/static. seed-data.sh on first boot was using flat
'cp -p' which errors on directories with 'omitting directory ...';
set -e then exits, the container restarts forever in a crash loop,
and Core never starts.
Fix: 'cp -Rp' so directories are copied as trees. The no-clobber
check on the top-level name still keeps operator-edited content
safe — if /core/data/_player exists we don't replace it, even if
its internals diverge from the bundled version.
Also defends against dotfiles via the second glob.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the placeholder Dragon Fork landing page at / with the real
React SPA — the same UI that ships in upstream's datarhei/restreamer
image. Operators get the full process management dashboard, log
viewer, restream config, and so on.
Implementation: a new Docker stage 'ui-builder' (node:21-alpine3.20)
clones datarhei/restreamer-ui at a pinned tag (v1.14.0), runs
'yarn install + yarn build' with PUBLIC_URL="./" so all asset
references are relative, and the runtime stage pulls /ui/build into
/core/static. The existing seed-data.sh script then copies it into
/core/data on first boot.
Stacking order in /core/static:
1. UI bundle from ui-builder — provides index.html, the SPA bundle
and assets, _player, _playersite, etc.
2. Dragon Fork deploy/static/* — currently only whep-player.html;
the placeholder index.html was removed so the UI's wins.
Pinned to v1.14.0 (the most recent tagged restreamer-ui release)
rather than 'main' for reproducible builds. Bumping the pin is a
one-line ARG override.
Image size: ~+25MB compressed (Restreamer UI bundle is ~3MB
gzipped, plus the build-stage layer overhead until pruned).
UI-side configuration: the SPA defaults to talking to the
same-origin /api endpoints, which is exactly what we want when
serving from Core. No '?address=' query string needed on the URL.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
A clean post-merge deploy showed an unintended UX wart: hitting
http://<host>:<port>/ in a browser returned 404 'File not found'
because Core's static-disk handler serves /core/data and we never
put anything there. Functionally fine — the API and Swagger are
reachable on /api and /api/swagger — but a confusing first
impression for a brand-new operator.
Fix is deploy-side, not code-side: ship a small landing page +
the existing test/whep-player.html as default content for the data
volume.
Pieces:
deploy/truenas/core/static/
index.html — Dragon Fork-branded landing page; links
to Swagger and the WHEP player; live
/api status panel.
whep-player.html — same self-contained Pion subscriber that
lives at test/whep-player.html.
deploy/truenas/core/seed-data.sh
First-boot script. Copies /core/static/* into /core/data/
only when the destination filename doesn't already exist —
operator-supplied content is never clobbered, so this is a
safe addition that respects upstream's contract that
/core/data is operator-owned.
deploy/truenas/core/Dockerfile
COPYs the static dir and seed script into the runtime image,
wraps the entrypoint as 'seed-data.sh && exec run.sh' (run.sh
itself is unchanged from upstream).
Image size impact: ~15KB.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Conflict resolution: keep M3's full handler.go rewrite (per-stream
index, error matrix, PATCH, CORS, auto-cleanup) and re-apply the
swagger annotations from #7 onto the new function declarations,
including a fresh annotation for the M3-introduced Trickle endpoint.
Swagger docs regenerated to pick up all three.
Race-clean: go test -race ./app/webrtc/... green.
M5 / final M2-stack work. The fork now identifies itself unambiguously
in logs, the API, and the README without changing the Go module path
(internal imports stay at github.com/datarhei/core/v16 — see NOTES.md
for the rationale).
Identity surfaces:
- app/version.go gains Variant ('dragonfork') and Fork ('Datarhei —
Dragon Fork') as vars (overridable via -ldflags for downstream
re-packagers).
- api.About + the /api endpoint expose 'variant' and 'fork' fields;
Swagger docs regenerated.
- Startup banner logs 'variant' + 'fork' alongside the existing
application + version fields, so a TrueNAS sysadmin tail-following
/var/log can tell at a glance which fork is running.
Documentation:
- README.md rewritten with a Dragon Fork header and Quick start; the
upstream feature surface is summarised in 'From upstream Datarhei'
with a clear additivity statement. Sample process JSON, multi-input
pipeline guidance, link to the design + testing docs.
- NOTICE: Apache 2.0 §4(d) attribution to upstream datarhei Core,
Pion, Echo, FFmpeg.
- CREDITS: enumerated dependency list with licenses.
- CHANGELOG.md prepended with a 'Datarhei — Dragon Fork' section
starting at v0.1.0-dragonfork; upstream's '# Core' history preserved
below.
Module path stays github.com/datarhei/core/v16 by design — the fork is
distinguished by repo location and branch history, not import path.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds an end-to-end RTP-arrival latency probe that runs as a dedicated
CI job and asserts p95 < 50ms.
Implementation
--------------
A build-tagged test (-tags latency, off by default) sends 1000
synthetic RTP packets at 60Hz into corewebrtc.Source and reads them
back via a Pion subscriber's track.ReadRTP(). Each packet's payload
starts with the publisher's UnixNano send time; the subscriber diffs
against time.Now() at arrival and accumulates p50/p95/p99.
This exercises every link of the egress hop: Source UDP read,
subscriber fan-out, forwardRTPSplit, Pion's TrackLocalStaticRTP
write, DTLS-SRTP encrypt, ICE socket write, decrypt at the
subscriber, RTP unmarshal at ReadRTP. Pure server-side; no FFmpeg
or codecs involved.
Why not glass-to-glass
----------------------
The design's §7 calls for FFmpeg drawtext frame counters + decode-
side pixel sampling, p95<300ms RTMP / <200ms SRT. Implementing that
in pure Go needs a cgo H.264 decoder or an FFmpeg sidecar pipe — a
significantly bigger lift for a marginal regression-detection win
(encode/decode latency is roughly fixed by the codec stack and
isn't moved by Core code changes). The server-hop measurement
captures everything Core code can actually regress.
Threshold
---------
50ms p95. Locally observed on a quiet host:
p50=110µs, p95=237µs, p99=318µs.
The 50ms gate is ~200x headroom — generous enough to absorb CI
runner noise without false alarms, tight enough to catch a real
slowdown.
Race-clean: latencySamples uses a sync.Mutex around the slice append
(initial draft had a slice racing with the receive goroutine; vet
caught it).
Documented in test/TESTING.md and wired to .forgejo/workflows/test.yml
as the latency-gate job (depends on lint-and-vet, parallel with test
and webrtc-smoke).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three artifacts that close out the easier half of the M4 milestone:
1. .forgejo/workflows/test.yml — CI on every push and PR. Three jobs:
- lint-and-vet: go vet + go build (~30s)
- test: go test -race -short ./... + a no-race coverage
pass that uploads coverage.out as an artifact
- webrtc-smoke: TestIntegration_FiveViewerFanout and the rest of
the WebRTC subsystem tests in isolation, so a
failure on the egress path stays readable in the
log.
Pinned to Go 1.24 to match go.mod. The forge has a
forgejo-runner sibling container; this YAML uses GitHub Actions
syntax which Forgejo Actions accepts unchanged.
2. test/whep-player.html — self-contained browser WHEP subscriber for
manual smoke testing. RTCPeerConnection (recvonly V+A) + fetch()
POST/DELETE/PATCH against /api/v3/whep/:id, ICE/PC state pills,
inbound-bitrate sampling at 1 Hz, codec hint pulled from the answer
SDP, JWT token field, ?url=&token= shareable query string. No
external deps; works from file:// or any static host.
3. test/TESTING.md — short doc that ties together the in-process race
tests, the browser player, and the existing Pion CLI helper at
test/whep-client/. Notes the latency p95 gate as a follow-up.
Latency gate (FFmpeg drawtext frame counter + decode-side pixel
sampling, p95 < 300ms RTMP / < 200ms SRT) is queued for a separate
PR — it's a several-hundred-line addition in its own right and
shouldn't block CI from landing.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The WHEP routes were mounted by http/server.go via the app/webrtc
Handler.Register(), but Subscribe and Unsubscribe carried no swag
annotations. The Swagger UI at /api/swagger/index.html therefore
didn't list /api/v3/whep/* — programmatic API consumers and humans
browsing the docs couldn't discover the endpoints.
Adds the standard upstream-shaped @Summary / @Tags / @ID / @Router
annotations on Subscribe and Unsubscribe (matching the rtmp.go and
srt.go pattern) and regenerates docs/{docs.go,swagger.json,swagger.yaml}
via 'make swagger'. Verified: swagger.json now contains both paths,
swagger UI renders them under the v16.16.0 tag.
Closes#3.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
BuildArgs hardcoded -map 0✌️0 / -map 0🅰️0 for the two RTP legs.
Correct for production RTMP/SRT publishers (single combined input),
but breaks any process whose audio lives on a different input index
— multi-input lavfi test scaffolds, multi-camera pipelines, SDI +
file-audio mixes, etc.
Adds VideoMap and AudioMap fields to ConfigWebRTC (and the API DTO),
defaulting to the prior literals so existing deployments are
unaffected. BuildArgs reads them.
Tests:
- TestBuildArgs_DefaultMaps locks the empty-string default behavior
- TestBuildArgs_CustomMaps drives the multi-input override path
- TestProcessConfigWebRTCMapsRoundtrip extends the DTO roundtrip
Closes#2.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
TestIntegration_FiveViewerFanout drives the M3 acceptance criterion
in the wide direction: spin up the subsystem, register one process,
attach 5 Pion subscribers in parallel via the real Echo handler,
spray synthetic RTP at the allocated UDP ports, and assert each
subscriber's video + audio track receive at least one packet inside
a 15s window. After onProcessStop, the per-stream peer index must
drain to zero within 3s.
TestSubsystem_TeardownHookFiresOnProcessStop is the unit-level
counterpart — confirms the callback registered via
SetTeardownHook actually fires when a process is torn down, even
without a full Pion handshake.
Together these cover the acceptance language: '5 concurrent viewers,
all error paths correct, clean teardown'.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Covers each new code path that the design's §6 table requires:
- Subscribe -> 406 on non-H264 / non-Opus offer (TestHandler_Subscribe_406OnCodecMismatch)
- Subscribe -> 503 when total cap exhausted (TestHandler_Subscribe_503OnTotalCap)
- Subscribe -> 503 when per-stream cap exhausted (TestHandler_Subscribe_503OnPerStreamCap)
- Trickle -> 404 on unknown resource (TestHandler_Trickle_404WhenUnknown)
- preflight -> 204 + CORS headers (TestHandler_PreflightCORS)
- Register installs all 5 routes (TestHandler_RegisterMountsAllRoutes)
- Close drains the index without panicking (TestHandler_Close_DrainsPeers)
- requireH264AndOpus table-driven (TestRequireH264AndOpus)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Major Handler rewrite implementing the design's M3 acceptance
criteria ('5 concurrent viewers, all error paths correct, clean
teardown'):
Multi-viewer correctness:
- streamID -> resourceID -> Peer two-level index (was flat)
- per-stream peer cap alongside total cap, defaults match the
design's '5–8 viewer' target (8/stream, total from corewebrtc)
- per-peer awaitPeerClose goroutine watches Peer.Done() so ICE
failures yank the index entry + decrement the counter (no leaks)
- tearDownStreamPeers callback (registered with Subsystem in
NewHandler) drives all peer closes when the source process stops
Error matrix from design §6:
- 406 on codec mismatch (offer missing H264 or Opus rtpmap)
- 504 on ICE gathering timeout (passthrough from CreatePeerFromSources)
- 204 on DELETE unknown resource (idempotent per WHEP spec; was 404)
- 503 on per-stream cap reached (separate body from total-cap 503)
- 400 on missing/empty body (unchanged)
- 404 on unknown stream (unchanged)
WHEP spec compatibility:
- PATCH /whep/:id/:resource for trickle-ICE
- OPTIONS preflight on every WHEP path
- CORS Allow-Origin/Methods/Headers + Expose-Headers (Location, ETag)
- ETag header on Subscribe response
Defensive nil-peer guards in tearDown / Close paths so a partial
state doesn't panic.
Refactor: 134 -> 341 lines on handler.go but the surface is the
same (NewHandler/Register/Subscribe/Unsubscribe/Close); existing
callers continue to work. Pre-M3 test 'Unsubscribe_404WhenUnknown'
renamed and updated to the new 204 expectation.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Subsystem.SetTeardownHook installs a callback the subsystem invokes
just before closing per-stream Sources in onProcessStop. Used by the
WHEP Handler in M3 to drain its per-stream peer index before the
underlying Sources go away — closes the 'subscribers fan out into a
closed channel' race the design's §6 error matrix calls out as
'Publisher disconnects / FFmpeg exits'.
Single consumer by design (one subsystem, one handler). Calling
SetTeardownHook again replaces the previous callback; nil detaches.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two small additions to support the M3 handler:
- Peer.Done() — read-only view of the existing 'done' channel,
closed on Close(). Lets external indexes (Handler, admin API)
await peer teardown without polling.
- Peer.AddICECandidate — passthrough so the WHEP PATCH handler
can forward trickle-ICE candidates without reaching into the
PeerConnection directly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The M2 WHEP route lives under /api/v3 and inherits Core's JWT auth.
The M1 test client was written for the unauth'd PoC port; without
this flag it's useless against the real Core build.
- Subscribe() and postOffer() take a token string; empty means no
Authorization header (M1 behavior preserved).
- main.go gains a -token flag.
- main_test.go pass empty token (existing tests run against an
in-process unauth'd handler).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ProcessConfig in http/api/process.go shipped without a WebRTC field, so
JSON arriving at POST /api/v3/process was silently stripped of
"webrtc":{"enabled":true}. Marshal() handed restream a zero
ConfigWebRTC, the OnProcessStart hook no-op'd, and every WHEP request
returned 404 — even with a running webrtc-enabled process.
Caught on the M2 TrueNAS deploy at acceptance time: GET /process/{id}/config
came back without the webrtc block, despite the inbound JSON having it.
This is the API-layer twin of the earlier 'fix(config): preserve WebRTC
section in Config.Clone()' — same class of bug (drop-on-copy), different
struct.
- Add ProcessConfigWebRTC mirroring app.ConfigWebRTC.
- Marshal: copy DTO -> app.Config.WebRTC.
- Unmarshal: copy app.Config.WebRTC -> DTO.
- Regression tests cover both the JSON->DTO->Config path and the
default (no webrtc block) case.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Config.Clone() copied every top-level Data section except WebRTC.
Because api.go receives a clone (not the original), cfg.WebRTC.Enable
was always the zero value at runtime, the subsystem was skipped, and
the WHEP route was never mounted — regardless of CORE_WEBRTC_ENABLE.
Caught on the first live M2 TrueNAS deploy: env said enable=true,
container listened fine, but /api/v3/whep/:id returned Echo's default
JSON 404 (from router) instead of the handler's plain-text
'webrtc: stream not found' (which it would return for an unknown id).
- Add data.WebRTC = d.WebRTC in the struct-copy block.
- Deep-copy NAT1To1IPs alongside the other []string sections.
- Regression test TestConfigCopyWebRTC covers both.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a dedicated deploy bundle under deploy/truenas/core/ so the
real root Core binary — with the M2 WebRTC subsystem wired in —
can replace the M1 webrtc-poc stack on the TrueNAS host.
- Dockerfile: two-stage build on golang:1.24-alpine3.20 + alpine:3.20
runtime. FFmpeg is bundled so restream processes have their
subprocess path ready. Copies the core binary from core/core
(Go places the output file inside the core/ package directory
because it can't overwrite a directory with a file) plus import
and ffmigrate from the repo root.
- docker-compose.yml: host-networked Core service, env-driven
config (CORE_ADDRESS, CORE_API_AUTH_*, CORE_WEBRTC_ENABLE,
CORE_WEBRTC_PUBLIC_IP), with config/ and data/ bind mounts.
- README.md: M1→M2 cutover notes, one-time setup, JWT smoke test
against /api/v3/whep/:id, and teardown.
Verified: make release + make import + make ffmigrate all
cross-compile cleanly for linux/amd64; go build ./... and
go test ./... pass on the branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
End-to-end exercise of the M2 pipeline — subsystem hook, port
allocation, two-track forwarding, WHEP handshake — without
spinning up a full Core HTTP server:
- Fire onProcessStart directly to get the two RTP legs back
- Parse video + audio UDP ports out of the leg addresses,
assert adjacency
- Mount the Handler on an Echo httptest server
- Build a Pion PeerConnection (recvonly video + audio), POST
its offer, feed the answer back in
- Spray synthetic RTP packets at both loopback sockets
- Assert both OnTrack callbacks fire and each delivers at least
one RTP packet within 10s
- DELETE via the returned Location header to confirm teardown
Passes cleanly under -race in ~1s. Catches regressions across
the whole M2 wiring from a single fixture.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Installs the WebRTC egress subsystem at Core boot when
cfg.WebRTC.Enable is true and the subsystem constructs cleanly:
- http.Config gains an optional WebRTC *appwebrtc.Handler field;
server.setRoutesV3 mounts its WHEP routes on the JWT-protected
/api/v3 group.
- api.start() constructs the Subsystem, registers its ProcessHooks
with the restreamer, and builds a Handler. A construction failure
is logged and Core continues without WebRTC — consistent with
disabling the subsystem outright.
- api.stop() closes the Handler (tearing down active peers) before
closing the Subsystem (releasing per-process UDP sockets), mirroring
the RTMP/SRT teardown pattern.
Verified: go build ./... clean; go test ./app/webrtc/...
./core/webrtc/... ./restream/... ./http/... all pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Introduces the HTTP surface the browser (or OBS WebRTC clients)
target when subscribing to a process's egress:
POST /whep/:id -> answer SDP + Location header
DELETE /whep/:id/:resource -> tear down a specific peer
The handler looks up the per-process stream pair via the Subsystem,
validates SDP offer shape, and delegates peer creation to the core
PeerFactory's CreatePeerFromSources (two-source forwarding).
WHEP routes are left unauthenticated in M2 — browsers and OBS don't
carry the Core JWT, and per-process signed-URL tokens are an M3
enhancement. Deployments should place the endpoint behind an
authenticated reverse-proxy for now.
Tests cover:
- 404 for POSTs against unregistered streams
- 400 for empty/invalid SDP offers once a stream is registered
- 404 for DELETE against unknown resource ids
Introduces the subsystem layer that sits alongside api.API and wires
the M1 core/webrtc primitives into the per-process restream lifecycle.
app/webrtc/subsystem.go:
- Subsystem struct holding the global WebRTC config, core PeerFactory,
per-process stream map, and logger
- New(config.DataWebRTC, logger) constructor
- Enabled(), Hooks(), Close(), lookup() methods
app/webrtc/lifecycle.go:
- onProcessStart: allocates an adjacent UDP port pair, binds two
Pion Sources (video on V, audio on V+1), registers them under the
process id, and returns the two RTP output legs to append to the
FFmpeg command.
- onProcessStop: tears down the pair.
- allocAdjacentPair: retries up to 10 times to find a free (V, V+1)
pair since the kernel's ephemeral picker can hand us an odd port.
- splitRTPLegs: converts BuildArgs' flat []string into two ConfigIO
entries by splitting on the second -map token.
core/webrtc/peer.go + forward.go:
- Adds PeerFactory.CreatePeerFromSources for the M2 two-source
forwarding mode (video and audio on separate UDP ports, no
payload-type sniffing). Leaves CreatePeer intact for the M1 PoC.
- Adds forwardRTPSplit companion goroutine.
config/data.go:
- Promote anonymous WebRTC struct to named type DataWebRTC so
app/webrtc can accept it by value.
Adds a pair of lifecycle callbacks the app/webrtc subsystem installs
via SetHooks:
- OnStart fires synchronously just before ffmpeg.Start(). It receives
the task config and may return []ConfigIO extras to append to the
output list. When extras are appended, startProcess rebuilds the
FFmpeg command and the underlying process.Process before starting.
A non-nil error aborts the start.
- OnStop fires synchronously just after ffmpeg.Stop() so subsystems
can tear down per-process state.
Hooks run with the restream write lock held; they must not call back
into Restreamer methods or they will deadlock. This is the pattern
app/webrtc uses to inject per-process RTP output legs without having
to reach into restream internals from outside.
Adds Alloc(), the ephemeral loopback UDP port grabber the subsystem
uses to pick the RTP port it will hand to FFmpeg and then re-bind with
core/webrtc.NewSourceOn. Covered by a 100x rebind test.
Adds BuildArgs(), which emits the -f rtp output fragments (video on
the passed port, audio on port+1) with copy codecs by default and an
H.264 baseline / libopus re-encode leg when ForceTranscode is set.
Covered by three unit tests.
Adds webrtc.enable, webrtc.public_ip, webrtc.nat_1_to_1_ips, and
webrtc.udp_mux_port to the Core Data struct and registers each via
the existing vars system. Default is disabled; no behavior change
without explicit opt-in.
Adds the per-process WebRTC egress toggle + codec/payload-type knobs
described in the M2 spec. Clone() carries it forward. No behavior
change yet \u2014 the subsystem wiring comes later in M2.