Commit graph

368 commits

Author SHA1 Message Date
4d2f11d836 feat(app/webrtc): M3 robustness — error matrix, per-stream index, PATCH, CORS
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>
2026-05-03 11:23:55 +00:00
3abd4d8fd1 feat(app/webrtc): broadcast process-stop via SetTeardownHook
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>
2026-05-03 11:23:55 +00:00
4f84c72c85 feat(core/webrtc): expose Peer.Done() channel + AddICECandidate
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>
2026-05-03 11:23:55 +00:00
0417aff3b1 test(whep-client): add -token flag for JWT-gated /api/v3/whep endpoints
Some checks failed
tests / build (push) Failing after 2s
CodeQL / Analyze (pull_request) Failing after 2s
tests / build (pull_request) Failing after 1s
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>
2026-05-03 04:59:08 +00:00
f6d36bfa66 fix(http/api): carry process WebRTC config through the API DTO
Some checks failed
tests / build (push) Failing after 3s
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>
2026-05-03 04:53:25 +00:00
2d29dc9c4a fix(config): preserve WebRTC section in Config.Clone()
Some checks failed
tests / build (push) Failing after 3s
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>
2026-04-17 15:26:11 -04:00
d96aa70c27 deploy(truenas): Core image + compose for M2 WebRTC rollout
Some checks failed
tests / build (push) Failing after 3s
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>
2026-04-17 14:59:49 -04:00
b030102611 test(webrtc): add M2 integration smoke test
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>
2026-04-17 10:11:34 -04:00
83eaa28601 feat(webrtc): wire app/webrtc subsystem into Core lifecycle
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>
2026-04-17 10:08:54 -04:00
f6d5b3378a feat(webrtc): add Echo WHEP handler for app/webrtc subsystem
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
2026-04-17 10:03:24 -04:00
9d38e9ccdb feat(webrtc): add app/webrtc subsystem + lifecycle hooks
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.
2026-04-17 10:02:00 -04:00
46531bb479 feat(restream): add ProcessHooks for WebRTC subsystem integration
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.
2026-04-17 09:57:14 -04:00
16ae17d2a1 feat(app/webrtc): port allocator + FFmpeg arg builder
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.
2026-04-17 09:52:09 -04:00
80db028281 feat(config): add webrtc global config block
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.
2026-04-17 09:51:02 -04:00
eaeefee753 feat(restream): add ConfigWebRTC per-process field
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.
2026-04-17 09:50:28 -04:00
c38036de94 docs(m2): implementation plan 2026-04-17 09:49:20 -04:00
86bae816c1 docs(m2): WebRTC into Core proper — design spec
M2 promotes the M1 standalone PoC into the datarhei Core binary so
WebRTC becomes a first-class output alongside RTMP/SRT/HLS, surfaced
in the core-ui dashboard.

Architecture: new app/webrtc sibling subsystem + two small hooks on
restream (ProcessHooks + AppendOutput), reusing the untouched M1
core/webrtc package. WHEP served under /api/v3/process/{id}/whep,
inheriting JWT auth. A new "Live (WebRTC)" tab on the process detail
view provides the embedded browser player.

Covers: purpose, architecture diagram, decision table, components,
data flow (enable/subscribe/stop/disable/restart), error handling,
testing strategy (unit/integration/e2e), acceptance criteria,
rollback, and a seven-milestone sanity breakdown.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-17 09:42:16 -04:00
9e3f031f95 feat(webrtc): add -rtp-host flag + TrueNAS Docker deploy
Some checks failed
tests / build (push) Failing after 3s
CodeQL / Analyze (pull_request) Failing after 3s
tests / build (pull_request) Failing after 3s
- core/webrtc: NewSourceOn(streamID, host, port) allows binding the
  RTP UDP socket on something other than 127.0.0.1, required when the
  PoC runs in a container and must accept RTP from LAN publishers.
  NewSource(streamID, port) stays as a convenience wrapper on
  127.0.0.1 for existing tests and tight local tests.

- cmd/webrtc-poc: new -rtp-host flag (default 127.0.0.1 for safety).

- deploy/docker/Dockerfile: two-stage build, scratch runtime, ~14 MB.

- deploy/truenas/docker-compose.yml: host-networked stack template
  driven by a .env file. Host networking is required for WebRTC ICE
  to work without NAT rewriting per-candidate.

- deploy/truenas/README.md: operator runbook with port picking,
  bring-up, verification curls, and security notes.
2026-04-17 09:05:37 -04:00
413d0f24b6 test(webrtc): add Pion WHEP subscriber client + e2e test
Some checks failed
tests / build (push) Failing after 13s
CodeQL / Analyze (pull_request) Failing after 2s
tests / build (pull_request) Failing after 2s
whep-client/main.go: minimal Pion subscriber that POSTs a recvonly
offer, applies the answer, and waits for one RTP packet on each of
the video and audio tracks. Used as M1's end-to-end verifier.

whep-client/main_test.go: in-process e2e wiring — stands up Source,
Registry, PeerFactory and WHEPHandler behind an httptest server,
injects synthetic PT=102/111 RTP on the Source's UDP port and calls
Subscribe. Validates the full egress pipeline without requiring
FFmpeg or external network. Skipped under -short.
2026-04-17 08:52:40 -04:00
e471bd02b2 test(webrtc): add FFmpeg publisher script for M1 PoC
Generates a synthetic testsrc2 video + sine audio and pushes H.264/Opus
RTP to the webrtc-poc's UDP port, using the hard-coded payload types
(102 video, 111 audio) the M1 forwarder dispatches on. Intended to be
run alongside test/whep-client (M1 Task 11) for end-to-end verification.
2026-04-17 08:51:22 -04:00
c24c96d022 feat(webrtc): add standalone webrtc-poc binary for M1 testing
Minimal egress-only server that wires Source, Registry, PeerFactory and
WHEPHandler together on a single stream id. Listens for RTP on a local
UDP port (default 127.0.0.1:10000) and serves WHEP on :8787.

Not part of the Core binary — will be demoted to an internal test helper
once M2 integrates WebRTC output into the process-graph.
2026-04-17 08:50:31 -04:00
f6ddae23c9 feat(webrtc): add WHEP POST handler (happy path) 2026-04-17 08:48:06 -04:00
b2a691186c feat(webrtc): add PeerFactory, Peer, and RTP forwarder 2026-04-17 08:47:27 -04:00
917c353e03 feat(webrtc): add ICE config helper (Configuration + SettingEngine)
Vendors github.com/pion/webrtc/v4 v4.2.11 and its transitive
dependencies (datachannel, dtls/v3, ice/v4, interceptor, logging,
mdns/v2, sctp, sdp/v3, srtp/v3, stun/v3, transport/v4, turn/v4).
2026-04-17 08:46:27 -04:00
1fdc29ace1 feat(webrtc): add Source with UDP RTP reader and subscriber fan-out
Adds github.com/pion/rtp v1.10.1 as a direct dependency (vendored).
2026-04-17 08:45:48 -04:00
3a17e543c5 feat(webrtc): add thread-safe Registry for stream_id -> SourceHandle 2026-04-17 08:44:59 -04:00
2250cb0a8f feat(webrtc): add Config with defaults and validation 2026-04-17 08:44:30 -04:00
7ea1844869 feat(webrtc): add package skeleton and typed errors 2026-04-17 08:43:57 -04:00
651a9a3eb5 chore(deps): bump Go 1.21→1.24 and resync vendor for Pion WebRTC v4 compat
Pion webrtc/v4 (v4.2.11) requires Go 1.24+. Upstream datarhei was at
go 1.21.0. Bumping to go 1.24.0 pulls minor bumps across testify,
golang.org/x/{crypto,net,sync,sys,text,time,tools,mod}; vendor/ is
regenerated via 'go mod vendor' to reflect the new versions.

No application code changes; pure dep bump to unblock M1.
2026-04-17 08:43:31 -04:00
262a393b8d docs: add Dragon Fork WebRTC egress design spec and M1 plan 2026-04-17 08:40:05 -04:00
Ingo Oppermann
0de97f4a6b
Add linux/arm/v8 build
Some checks failed
CodeQL / Analyze (push) Failing after 9s
tests / build (push) Failing after 19s
2026-03-16 09:28:04 +01:00
Ingo Oppermann
8b66753a27
Upgrade to CUDA 12.9.1 2026-02-25 14:03:16 +01:00
Ingo Oppermann
d93ab0e92b
Upgrade alpine to 3.23, golang to 1.26 2026-02-25 13:52:36 +01:00
Ingo Oppermann
74cd623377
Upgrade to ffmpeg 7.1.1 2025-12-29 08:59:28 +02:00
Ingo Oppermann
41c505ad43
Bump golang to 1.25 2025-12-09 15:10:55 +01:00
Ingo Oppermann
17a73c9f95
Build latest vod 2025-07-17 22:11:45 +02:00
Ingo Oppermann
5a533022f2
Build specific commit 2025-07-17 17:14:56 +02:00
Ingo Oppermann
a5c2e79253
Build specific commit 2025-07-17 16:46:30 +02:00
Ingo Oppermann
9a7f357e30
Update go version 2025-06-19 16:19:37 +02:00
Ingo Oppermann
07221a2f0b
Upgrade to alpine3.21 2025-01-29 12:20:56 +01:00
Ingo Oppermann
37b99e7752
Remove ubuntu22 build, remove cuda11 builds, bundle with ffmpeg7.1 2024-11-29 11:45:20 +01:00
Ingo Oppermann
6f3e2caf0f
Build with go1.23 2024-10-01 15:19:58 +02:00
Ingo Oppermann
2a8b01feac
Make cuda12 image latest 2024-09-26 15:03:59 +02:00
Ingo Oppermann
65beedd281
Upgrade to ubuntu noble, add cuda 12 bundle 2024-09-26 14:13:59 +02:00
Ingo Oppermann
ca6dba7259
Add ubuntu build for vod branch 2024-09-24 11:47:25 +02:00
Ingo Oppermann
ddba7bbf74
Upgrade base image to alpine3.20 2024-07-11 12:22:50 +02:00
Ingo Oppermann
6872ba0498
Merge branch 'main' into dev 2024-06-07 11:47:03 +02:00
Ingo Oppermann
69d3155176
Remove alpine3.16 builds for vod branch 2024-06-07 11:45:06 +02:00
Ingo Oppermann
18fc8abe62
Update changelog, bump version to 16.16.0 2024-06-07 11:37:25 +02:00
Ingo Oppermann
32b5a83fa9
Fix datarhei/restreamer#759 2024-06-04 17:51:49 +02:00