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>
This commit is contained in:
Zac Gaetano 2026-04-17 10:08:54 -04:00
parent f6d5b3378a
commit 83eaa28601
2 changed files with 44 additions and 0 deletions

View file

@ -16,6 +16,7 @@ import (
"time"
"github.com/datarhei/core/v16/app"
appwebrtc "github.com/datarhei/core/v16/app/webrtc"
"github.com/datarhei/core/v16/config"
configstore "github.com/datarhei/core/v16/config/store"
configvars "github.com/datarhei/core/v16/config/vars"
@ -73,6 +74,8 @@ type api struct {
s3fs map[string]fs.Filesystem
rtmpserver rtmp.Server
srtserver srt.Server
webrtcsub *appwebrtc.Subsystem
webrtchandler *appwebrtc.Handler
metrics monitor.HistoryMonitor
prom prometheus.Metrics
service service.Service
@ -617,6 +620,22 @@ func (a *api) start() error {
a.restream = restream
// Build the WebRTC egress subsystem if the operator enabled it.
// Failure to construct the subsystem (e.g., invalid NAT1To1 IP)
// is logged and the subsystem declines to install hooks — Core
// starts normally without WebRTC support, consistent with how
// disabling the subsystem at runtime is handled.
if cfg.WebRTC.Enable {
webrtcSub, werr := appwebrtc.New(cfg.WebRTC, a.log.logger.core)
if werr != nil {
a.log.logger.core.Warn().WithError(werr).Log("WebRTC subsystem disabled: construction failed")
} else {
a.restream.SetHooks(webrtcSub.Hooks())
a.webrtcsub = webrtcSub
a.webrtchandler = appwebrtc.NewHandler(webrtcSub, 0)
}
}
var httpjwt jwt.JWT
if cfg.API.Auth.Enable {
@ -1014,6 +1033,7 @@ func (a *api) start() error {
},
RTMP: a.rtmpserver,
SRT: a.srtserver,
WebRTC: a.webrtchandler,
JWT: a.httpjwt,
Config: a.config.store,
Sessions: a.sessions,
@ -1354,6 +1374,17 @@ func (a *api) stop() {
a.srtserver = nil
}
// Tear down the WebRTC subsystem: close any active WHEP peers
// first, then release all per-process UDP sockets.
if a.webrtchandler != nil {
a.webrtchandler.Close()
a.webrtchandler = nil
}
if a.webrtcsub != nil {
a.webrtcsub.Close()
a.webrtcsub = nil
}
// Stop the RTMP server
if a.rtmpserver != nil {
a.log.logger.rtmp.Info().Log("Stopping ...")

View file

@ -33,6 +33,7 @@ import (
"net/http"
"strings"
appwebrtc "github.com/datarhei/core/v16/app/webrtc"
cfgstore "github.com/datarhei/core/v16/config/store"
"github.com/datarhei/core/v16/http/cache"
"github.com/datarhei/core/v16/http/errorhandler"
@ -86,6 +87,7 @@ type Config struct {
Cors CorsConfig
RTMP rtmp.Server
SRT srt.Server
WebRTC *appwebrtc.Handler
JWT jwt.JWT
Config cfgstore.Store
Cache cache.Cacher
@ -124,6 +126,7 @@ type server struct {
session *api.SessionHandler
widget *api.WidgetHandler
resources *api.MetricsHandler
webrtc *appwebrtc.Handler
}
middleware struct {
@ -238,6 +241,10 @@ func NewServer(config Config) (Server, error) {
)
}
if config.WebRTC != nil {
s.v3handler.webrtc = config.WebRTC
}
if config.Prometheus != nil {
s.handler.prometheus = handler.NewPrometheus(
config.Prometheus.HTTPHandler(),
@ -545,6 +552,12 @@ func (s *server) setRoutesV3(v3 *echo.Group) {
s.router.GET("/api/v3/widget/process/:id", s.v3handler.widget.Get)
}
// v3 WebRTC (WHEP egress). Mounted on the v3 group so JWT auth
// covers it in M2; public embed tokens will ship in M3.
if s.v3handler.webrtc != nil {
s.v3handler.webrtc.Register(v3)
}
// v3 Restreamer
if s.v3handler.restream != nil {
v3.GET("/skills", s.v3handler.restream.Skills)