From 02c649d97e12fcc0723afefa6ea69a3147772def Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Wed, 10 Jun 2026 14:39:07 -0700 Subject: [PATCH] viewers: count WHEP sessions only once connected MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WebRTCPlayback2 incremented the viewer count as soon as the SDP answer was minted, so handshakes that never established — the web player churns through several peer connections while loading or before falling back to HLS — each counted as a viewer until ICE failure detection fired seconds later. That's the "count jumps +4-5 then settles" spike. Count on PeerConnectionStateConnected instead, with the increment/decrement paired under a mutex so a connect racing teardown can't leak a count. Also point ViewerDec's per-protocol branch at ViewersTotal — it was updating/deleting the per-streamer gauge with a protocol label. Co-Authored-By: Claude Fable 5 --- pkg/media/webrtc_playback2.go | 36 ++++++++++++++++++++++++++++++++--- pkg/spmetrics/spmetrics.go | 4 ++-- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/pkg/media/webrtc_playback2.go b/pkg/media/webrtc_playback2.go index 1bbc1b71..b17a217e 100644 --- a/pkg/media/webrtc_playback2.go +++ b/pkg/media/webrtc_playback2.go @@ -3,6 +3,7 @@ package media import ( "context" "fmt" + "sync" "time" "github.com/google/uuid" @@ -83,9 +84,37 @@ func (mm *MediaManager) WebRTCPlayback2(ctx context.Context, user string, rendit // Setup complete! Now we boot up streaming in the background while returning the SDP offer to the user. + // The session only counts as a viewer once the peer connection actually + // establishes — counting at SDP-answer time inflated the count with + // handshakes that never connected (each lingering until ICE failure + // detection). The mutex pairs the increment with exactly one decrement + // even if a connect races session teardown. + var viewerMu sync.Mutex + viewerCounted := false + viewerDone := false + markConnected := func() { + viewerMu.Lock() + defer viewerMu.Unlock() + if viewerDone || viewerCounted { + return + } + viewerCounted = true + mm.IncrementViewerCount(user, "webrtc") + } + markDone := func() { + viewerMu.Lock() + defer viewerMu.Unlock() + viewerDone = true + if viewerCounted { + viewerCounted = false + mm.DecrementViewerCount(user, "webrtc") + } + } + go func() { ctx, cancel := context.WithCancel(ctx) defer cancel() + defer markDone() latency := time.Duration(0) @@ -194,9 +223,6 @@ func (mm *MediaManager) WebRTCPlayback2(ctx context.Context, user string, rendit } }() - mm.IncrementViewerCount(user, "webrtc") - defer mm.DecrementViewerCount(user, "webrtc") - if !audioOnly { go func() { rtcpBuf := make([]byte, 1500) @@ -228,6 +254,10 @@ func (mm *MediaManager) WebRTCPlayback2(ctx context.Context, user string, rendit peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) { log.Log(ctx, "Peer Connection State has changed", "state", s.String()) + if s == webrtc.PeerConnectionStateConnected { + markConnected() + } + if s == webrtc.PeerConnectionStateFailed || s == webrtc.PeerConnectionStateClosed || s == webrtc.PeerConnectionStateDisconnected { // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. diff --git a/pkg/spmetrics/spmetrics.go b/pkg/spmetrics/spmetrics.go index 9c25f351..b62673ba 100644 --- a/pkg/spmetrics/spmetrics.go +++ b/pkg/spmetrics/spmetrics.go @@ -237,9 +237,9 @@ func ViewerDec(user string, protocol string) { } viewersByProtocol[protocol]-- if viewersByProtocol[protocol] == 0 { - Viewers.DeleteLabelValues(protocol) + ViewersTotal.DeleteLabelValues(protocol) } else { - Viewers.WithLabelValues(protocol).Set(float64(viewersByProtocol[protocol])) + ViewersTotal.WithLabelValues(protocol).Set(float64(viewersByProtocol[protocol])) } }() } -- 2.51.2