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>
This commit is contained in:
Zac Gaetano 2026-05-03 11:23:55 +00:00
parent 4f84c72c85
commit 3abd4d8fd1
2 changed files with 30 additions and 0 deletions

View file

@ -94,6 +94,7 @@ func (s *Subsystem) onProcessStart(id string, cfg *appcfg.Config) ([]appcfg.Conf
func (s *Subsystem) onProcessStop(id string) {
s.mu.Lock()
st, ok := s.streams[id]
teardown := s.teardown
if ok {
delete(s.streams, id)
}
@ -102,6 +103,16 @@ func (s *Subsystem) onProcessStop(id string) {
if !ok {
return
}
// Broadcast first, so any subscribed peers get torn down while
// the streamID is still meaningful. The handler's tearDownStreamPeers
// drives each Peer.Close() which in turn unsubscribes from the
// Sources we're about to shut down — preventing a "subscribers fan
// out into a closed channel" race.
if teardown != nil {
teardown(id)
}
if st.video != nil {
_ = st.video.Close()
}

View file

@ -31,6 +31,12 @@ type Subsystem struct {
mu sync.Mutex
streams map[string]*processStream // processID -> stream pair
// teardown is set by the Handler (or any other consumer) so the
// Subsystem can broadcast process-stop events. Called *before*
// the per-stream Sources are closed, so consumers can yank their
// own indexes while the stream id is still valid.
teardown func(streamID string)
}
// processStream captures the two Sources (video + audio) backing a
@ -110,6 +116,19 @@ func (s *Subsystem) Close() {
}
}
// SetTeardownHook registers a callback invoked just before a stream's
// Sources are closed in onProcessStop. The callback is expected to
// tear down any external resources keyed by streamID — most importantly
// the WHEP Handler's per-stream peer index.
//
// Calling SetTeardownHook again replaces the previous callback; pass
// nil to detach. Only one consumer is supported by design.
func (s *Subsystem) SetTeardownHook(fn func(streamID string)) {
s.mu.Lock()
defer s.mu.Unlock()
s.teardown = fn
}
// lookup returns the per-process stream pair for id, or nil, false.
// Used by the WHEP handler.
func (s *Subsystem) lookup(id string) (*processStream, bool) {