From c4592095b2a366b6d828a921b9d67017c8635e60 Mon Sep 17 00:00:00 2001 From: Bill Gardner Date: Wed, 17 Dec 2025 13:42:31 -0500 Subject: [PATCH 1/7] remove duplicate sessionID in logs --- service/rtc/session.go | 8 ++++---- service/rtc/simulcast.go | 22 ++++++++++------------ 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/service/rtc/session.go b/service/rtc/session.go index 6af9ed3..fb68d57 100644 --- a/service/rtc/session.go +++ b/service/rtc/session.go @@ -232,7 +232,7 @@ func (s *session) getSourceRate(mimeType, rid string) int { rm := s.screenRateMonitors[getTrackIndex(mimeType, rid)] if rm == nil { - s.log.Warn("rate monitor should not be nil", mlog.String("sessionID", s.cfg.SessionID)) + s.log.Warn("rate monitor should not be nil") return -1 } @@ -372,7 +372,7 @@ func (s *session) sendOffer(sdpOutCh chan<- Message) error { } if err := s.sendMediaMapping(); err != nil { - s.log.Error("failed to send media mapping", mlog.Err(err), mlog.String("sessionID", s.cfg.SessionID)) + s.log.Error("failed to send media mapping", mlog.Err(err)) } select { @@ -636,13 +636,13 @@ func (s *session) sendMediaMapping() error { } track := trx.Sender().Track() if track == nil { - s.log.Warn("track is nil", mlog.String("sessionID", s.cfg.SessionID)) + s.log.Warn("track is nil") continue } trackID := track.ID() trackType := getTrackType(trackID) if trackType == "" { - s.log.Warn("track type is empty", mlog.String("sessionID", s.cfg.SessionID), mlog.String("trackID", trackID)) + s.log.Warn("track type is empty", mlog.String("trackID", trackID)) continue } mediaMap[trx.Mid()] = dc.TrackInfo{ diff --git a/service/rtc/simulcast.go b/service/rtc/simulcast.go index 99144fe..0e703cb 100644 --- a/service/rtc/simulcast.go +++ b/service/rtc/simulcast.go @@ -70,7 +70,7 @@ func (s *session) initBWEstimator(bwEstimator cc.BandwidthEstimator) { select { case rateCh <- rate: default: - s.log.Error("failed to send on rateCh", mlog.String("sessionID", s.cfg.SessionID)) + s.log.Error("failed to send on rateCh") } }) @@ -94,7 +94,6 @@ func (s *session) initBWEstimator(bwEstimator cc.BandwidthEstimator) { lastLossRate = lossRate s.log.Debug("sender bwe", - mlog.String("sessionID", s.cfg.SessionID), mlog.Int("delayRate", delayRate), mlog.Int("lossRate", lossRate), mlog.String("averageLoss", fmt.Sprintf("%.5f", averageLoss)), @@ -109,7 +108,7 @@ func (s *session) initBWEstimator(bwEstimator cc.BandwidthEstimator) { // high rate track and there was a drop in the estimated rate // in which case we want to act as quickly as possible. if time.Since(lastLevelChangeAt) < backoff && (currLevel == SimulcastLevelLow || rateDiff >= 0) { - s.log.Debug("skipping bitrate check due to backoff, no drop", mlog.String("sessionID", s.cfg.SessionID)) + s.log.Debug("skipping bitrate check due to backoff, no drop") return } @@ -140,7 +139,7 @@ func (s *session) initBWEstimator(bwEstimator cc.BandwidthEstimator) { select { case rate, ok := <-rateCh: if !ok { - s.log.Info("rateCh was closed, returning", mlog.String("sessionID", s.cfg.SessionID)) + s.log.Info("rateCh was closed, returning") return } rateChangeHandler(rate) @@ -169,7 +168,7 @@ func (s *session) handleSenderBitrateChange(downRate int, lossRate int) (bool, i currTrack := sender.Track() if currTrack == nil { - s.log.Error("track should not be nil", mlog.String("sessionID", s.cfg.SessionID)) + s.log.Error("track should not be nil") return false, 0, "" } @@ -181,14 +180,14 @@ func (s *session) handleSenderBitrateChange(downRate int, lossRate int) (bool, i localTrack, ok := currTrack.(*webrtc.TrackLocalStaticRTP) if !ok { - s.log.Error("track conversion failed", mlog.String("sessionID", s.cfg.SessionID)) + s.log.Error("track conversion failed") return false, 0, "" } mimeType := localTrack.Codec().MimeType currSourceRate := screenSession.getSourceRate(mimeType, currLevel) if currSourceRate <= 0 { - s.log.Warn("current source rate not available yet", mlog.String("sessionID", s.cfg.SessionID)) + s.log.Warn("current source rate not available yet") return false, 0, "" } @@ -201,7 +200,7 @@ func (s *session) handleSenderBitrateChange(downRate int, lossRate int) (bool, i // If the loss based rate estimation is greater than the source rate we avoid // potentially downgrading the level due to fluctuating delay rate estimation. if currLevel == SimulcastLevelHigh && lossRate > int(float32(currSourceRate)*rateTolerance) { - s.log.Debug("skipping level downgrade, no loss", mlog.String("sessionID", s.cfg.SessionID)) + s.log.Debug("skipping level downgrade, no loss") return false, 0, "" } @@ -213,12 +212,11 @@ func (s *session) handleSenderBitrateChange(downRate int, lossRate int) (bool, i sourceRate := screenSession.getSourceRate(mimeType, newLevel) if sourceRate <= 0 { - s.log.Warn("source rate not available", mlog.String("sessionID", s.cfg.SessionID)) + s.log.Warn("source rate not available") return false, 0, "" } s.log.Debug("switching simulcast level", - mlog.String("sessionID", s.cfg.SessionID), mlog.String("currLevel", currLevel), mlog.String("newLevel", newLevel), mlog.Int("downRate", downRate), @@ -229,14 +227,14 @@ func (s *session) handleSenderBitrateChange(downRate int, lossRate int) (bool, i select { case s.tracksCh <- trackActionContext{action: trackActionRemove, localTrack: currTrack}: default: - s.log.Error("failed to send screen track: channel is full", mlog.String("sessionID", s.cfg.SessionID)) + s.log.Error("failed to send screen track: channel is full") return false, 0, "" } select { case s.tracksCh <- trackActionContext{action: trackActionAdd, localTrack: newTrack}: default: - s.log.Error("failed to send screen track: channel is full", mlog.String("sessionID", s.cfg.SessionID)) + s.log.Error("failed to send screen track: channel is full") return false, 0, "" } From 2cbe62f3c323472b726457bd62f3fa5229736316 Mon Sep 17 00:00:00 2001 From: Bill Gardner Date: Thu, 18 Dec 2025 17:08:05 -0500 Subject: [PATCH 2/7] update pion to latest, use bgardner8008 fork --- go.mod | 14 +++++++------- go.sum | 24 ++++++++++++------------ 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/go.mod b/go.mod index bcbdde8..963705f 100644 --- a/go.mod +++ b/go.mod @@ -8,25 +8,26 @@ require ( github.com/gorilla/websocket v1.5.3 github.com/grafana/pyroscope-go/godeltaprof v0.1.8 github.com/kelseyhightower/envconfig v1.4.0 + github.com/mattermost/logr/v2 v2.0.21 github.com/mattermost/mattermost/server/public v0.1.10 github.com/pborman/uuid v1.2.1 github.com/pion/ice/v4 v4.0.10 github.com/pion/interceptor v0.1.40 github.com/pion/logging v0.2.4 - github.com/pion/rtcp v1.2.15 - github.com/pion/rtp v1.8.21 + github.com/pion/rtcp v1.2.16 + github.com/pion/rtp v1.8.26 github.com/pion/stun/v3 v3.0.0 github.com/pion/webrtc/v4 v4.1.3 github.com/prometheus/client_golang v1.16.0 github.com/prometheus/procfs v0.11.0 - github.com/stretchr/testify v1.10.0 + github.com/stretchr/testify v1.11.1 github.com/vmihailenco/msgpack/v5 v5.4.1 golang.org/x/crypto v0.39.0 golang.org/x/sys v0.33.0 - golang.org/x/time v0.3.0 + golang.org/x/time v0.10.0 ) -replace github.com/pion/interceptor v0.1.40 => github.com/streamer45/interceptor v0.0.0-20250701195358-9e4ca8111c7a +replace github.com/pion/interceptor v0.1.40 => github.com/bgardner8008/interceptor v0.0.0-20251218215555-af6c9bfab967 require ( github.com/abcum/lcp v0.0.0-20201209214815-7a3f3840be81 // indirect @@ -49,7 +50,6 @@ require ( github.com/klauspost/compress v1.17.11 // indirect github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 // indirect github.com/mattermost/ldap v3.0.4+incompatible // indirect - github.com/mattermost/logr/v2 v2.0.21 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect @@ -63,7 +63,7 @@ require ( github.com/pion/sctp v1.8.39 // indirect github.com/pion/sdp/v3 v3.0.14 // indirect github.com/pion/srtp/v3 v3.0.6 // indirect - github.com/pion/transport/v3 v3.0.7 // indirect + github.com/pion/transport/v3 v3.1.1 // indirect github.com/pion/turn/v4 v4.0.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/plar/go-adaptive-radix-tree v1.0.4 // indirect diff --git a/go.sum b/go.sum index bb09e86..5678d2c 100644 --- a/go.sum +++ b/go.sum @@ -65,6 +65,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgardner8008/interceptor v0.0.0-20251218215555-af6c9bfab967 h1:nARMm5bWtaaovmoBwZDWChLyEPOSXD9HeaXmlREM9PA= +github.com/bgardner8008/interceptor v0.0.0-20251218215555-af6c9bfab967/go.mod h1:pSPR73X1vaG0G9Bw40HIeE6d4c+fos8MSpMnpz1fH7g= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= @@ -357,10 +359,10 @@ github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM= github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA= github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= -github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo= -github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0= -github.com/pion/rtp v1.8.21 h1:3yrOwmZFyUpcIosNcWRpQaU+UXIJ6yxLuJ8Bx0mw37Y= -github.com/pion/rtp v1.8.21/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk= +github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo= +github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo= +github.com/pion/rtp v1.8.26 h1:VB+ESQFQhBXFytD+Gk8cxB6dXeVf2WQzg4aORvAvAAc= +github.com/pion/rtp v1.8.26/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM= github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE= github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE= github.com/pion/sdp/v3 v3.0.14 h1:1h7gBr9FhOWH5GjWWY5lcw/U85MtdcibTyt/o6RxRUI= @@ -369,8 +371,8 @@ github.com/pion/srtp/v3 v3.0.6 h1:E2gyj1f5X10sB/qILUGIkL4C2CqK269Xq167PbGCc/4= github.com/pion/srtp/v3 v3.0.6/go.mod h1:BxvziG3v/armJHAaJ87euvkhHqWe9I7iiOy50K2QkhY= github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw= github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU= -github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= -github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= +github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM= +github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ= github.com/pion/turn/v4 v4.0.2 h1:ZqgQ3+MjP32ug30xAbD6Mn+/K4Sxi3SdNOTFf+7mpps= github.com/pion/turn/v4 v4.0.2/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs= github.com/pion/webrtc/v4 v4.1.3 h1:YZ67Boj9X/hk190jJZ8+HFGQ6DqSZ/fYP3sLAZv7c3c= @@ -464,8 +466,6 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= -github.com/streamer45/interceptor v0.0.0-20250701195358-9e4ca8111c7a h1:0UYiiY37zerYTtgDH5e1mkiNN2C5A2Yzq/wq7hOx6wg= -github.com/streamer45/interceptor v0.0.0-20250701195358-9e4ca8111c7a/go.mod h1:0TChgrdwvHL3iwybehNI1G9grC5zAN1j3K6c7APxW+0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= @@ -478,8 +478,8 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= github.com/tidwall/btree v0.4.2/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8= @@ -735,8 +735,8 @@ golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= +golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= From 4a0b96cf7907906e315fe3d0b851e20d016242a7 Mon Sep 17 00:00:00 2001 From: Bill Gardner Date: Mon, 22 Dec 2025 11:49:44 -0500 Subject: [PATCH 3/7] add configuration options for NACK responder --- config/config.sample.toml | 13 +++++++++++++ docs/env_config.md | 2 ++ service/config.go | 2 ++ service/rtc/config.go | 23 +++++++++++++++++++++++ service/rtc/config_test.go | 31 +++++++++++++++++++++++++++++++ service/rtc/sfu.go | 12 +++++++++--- 6 files changed, 80 insertions(+), 3 deletions(-) diff --git a/config/config.sample.toml b/config/config.sample.toml index 0e65a3f..7709825 100644 --- a/config/config.sample.toml +++ b/config/config.sample.toml @@ -102,6 +102,19 @@ turn.credentials_expiration_minutes = 1440 # network address will be open. # udp_sockets_count = +# nack_buffer_size specifies the number of packets to buffer for NACK retransmission +# per SSRC (video stream). A larger buffer allows clients to request older packets +# but increases memory usage. Default is 256 packets (~8.5 seconds at 30fps). +# Must be a power of 2. Valid values: 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192 +# nack_buffer_size = 256 + +# nack_disable_copy controls whether the NACK responder should store packet copies +# or just pointers. Setting this to true (disabling copy) improves performance but +# can cause crashes due to memory corruption when the ring buffer wraps around. +# Default is true for backward compatibility, but should be set to false for +# production stability to avoid memory corruption crashes. +# nack_disable_copy = true + [store] # A path to a directory the service will use to store persistent data such as registered client IDs and hashed credentials. data_source = "/tmp/rtcd_db" diff --git a/docs/env_config.md b/docs/env_config.md index b926949..de90f8e 100644 --- a/docs/env_config.md +++ b/docs/env_config.md @@ -21,6 +21,8 @@ RTCD_RTC_TURNCONFIG_STATICAUTHSECRET String RTCD_RTC_TURNCONFIG_CREDENTIALSEXPIRATIONMINUTES Integer RTCD_RTC_ENABLEIPV6 True or False RTCD_RTC_UDPSOCKETSCOUNT Integer +RTCD_RTC_NACKBUFFERSIZE Integer (power of 2: 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192) +RTCD_RTC_NACKDISABLECOPY True or False RTCD_STORE_DATASOURCE String RTCD_LOGGER_ENABLECONSOLE True or False RTCD_LOGGER_CONSOLEJSON True or False diff --git a/service/config.go b/service/config.go index 5f5ad39..85cd949 100644 --- a/service/config.go +++ b/service/config.go @@ -82,6 +82,8 @@ func (c *Config) SetDefaults() { c.RTC.ICEPortTCP = 8443 c.RTC.TURNConfig.CredentialsExpirationMinutes = 1440 c.RTC.UDPSocketsCount = rtc.GetDefaultUDPListeningSocketsCount() + c.RTC.NACKBufferSize = 256 // Default: 256 packets (~8.5 seconds at 30fps) + c.RTC.NACKDisableCopy = true // Default: true for backward compatibility (but not recommended) c.Store.DataSource = "/tmp/rtcd_db" c.Logger.EnableConsole = true c.Logger.ConsoleJSON = false diff --git a/service/rtc/config.go b/service/rtc/config.go index cb2ccfe..3a1c69b 100644 --- a/service/rtc/config.go +++ b/service/rtc/config.go @@ -38,6 +38,15 @@ type ServerConfig struct { // a constant multiplier of 100. E.g. On a 4 CPUs node, 400 sockets per local // network address will be open. UDPSocketsCount int `toml:"udp_sockets_count"` + // NACKBufferSize specifies the number of packets to buffer for NACK retransmission + // per SSRC. A larger buffer allows clients to request older packets but increases + // memory usage. Must be a power of 2. Default is 256 packets (~8.5 seconds at 30fps). + NACKBufferSize uint16 `toml:"nack_buffer_size"` + // NACKDisableCopy controls whether the NACK responder should store packet copies + // or just pointers. Disabling copy improves performance but can cause crashes due + // to memory corruption when the ring buffer wraps around. Default is true (disabled) + // for backward compatibility, but should be set to false for production stability. + NACKDisableCopy bool `toml:"nack_disable_copy"` } func (c ServerConfig) IsValid() error { @@ -73,6 +82,20 @@ func (c ServerConfig) IsValid() error { return fmt.Errorf("invalid UDPSocketsCount value: should be greater than 0") } + if c.NACKBufferSize < 32 { + return fmt.Errorf("invalid NACKBufferSize value: should be at least 32") + } + + if c.NACKBufferSize > 8192 { + return fmt.Errorf("invalid NACKBufferSize value: should not exceed 8192") + } + + // Buffer size must be a power of 2 (required by pion/interceptor ring buffer) + isPowerOfTwo := c.NACKBufferSize != 0 && (c.NACKBufferSize&(c.NACKBufferSize-1)) == 0 + if !isPowerOfTwo { + return fmt.Errorf("invalid NACKBufferSize value: must be a power of 2 (32, 64, 128, 256, 512, 1024, 2048, 4096, 8192)") + } + return nil } diff --git a/service/rtc/config_test.go b/service/rtc/config_test.go index c5ff47c..2c1d229 100644 --- a/service/rtc/config_test.go +++ b/service/rtc/config_test.go @@ -104,6 +104,36 @@ func TestServerConfigIsValid(t *testing.T) { require.EqualError(t, err, "invalid UDPSocketsCount value: should be greater than 0") }) + t.Run("invalid NACKBufferSize too small", func(t *testing.T) { + var cfg ServerConfig + cfg.ICEPortUDP = 8443 + cfg.ICEPortTCP = 8443 + cfg.UDPSocketsCount = 1 + cfg.NACKBufferSize = 16 + err := cfg.IsValid() + require.EqualError(t, err, "invalid NACKBufferSize value: should be at least 32") + }) + + t.Run("invalid NACKBufferSize too large", func(t *testing.T) { + var cfg ServerConfig + cfg.ICEPortUDP = 8443 + cfg.ICEPortTCP = 8443 + cfg.UDPSocketsCount = 1 + cfg.NACKBufferSize = 16384 + err := cfg.IsValid() + require.EqualError(t, err, "invalid NACKBufferSize value: should not exceed 8192") + }) + + t.Run("invalid NACKBufferSize not power of 2", func(t *testing.T) { + var cfg ServerConfig + cfg.ICEPortUDP = 8443 + cfg.ICEPortTCP = 8443 + cfg.UDPSocketsCount = 1 + cfg.NACKBufferSize = 100 + err := cfg.IsValid() + require.EqualError(t, err, "invalid NACKBufferSize value: must be a power of 2 (32, 64, 128, 256, 512, 1024, 2048, 4096, 8192)") + }) + t.Run("valid", func(t *testing.T) { var cfg ServerConfig cfg.ICEAddressUDP = "127.0.0.1" @@ -111,6 +141,7 @@ func TestServerConfigIsValid(t *testing.T) { cfg.ICEPortTCP = 8443 cfg.TURNConfig.CredentialsExpirationMinutes = 1440 cfg.UDPSocketsCount = 1 + cfg.NACKBufferSize = 256 err := cfg.IsValid() require.NoError(t, err) }) diff --git a/service/rtc/sfu.go b/service/rtc/sfu.go index e5a593c..c2d7aaf 100644 --- a/service/rtc/sfu.go +++ b/service/rtc/sfu.go @@ -156,7 +156,7 @@ func initMediaEngine() (*webrtc.MediaEngine, error) { return &m, nil } -func initInterceptors(m *webrtc.MediaEngine) (*interceptor.Registry, <-chan cc.BandwidthEstimator, error) { +func initInterceptors(m *webrtc.MediaEngine, cfg ServerConfig) (*interceptor.Registry, <-chan cc.BandwidthEstimator, error) { var i interceptor.Registry generator, err := nack.NewGeneratorInterceptor() if err != nil { @@ -164,7 +164,13 @@ func initInterceptors(m *webrtc.MediaEngine) (*interceptor.Registry, <-chan cc.B } // NACK - responder, err := nack.NewResponderInterceptor(nack.ResponderSize(nackResponderBufferSize), nack.DisableCopy()) + responderOpts := []nack.ResponderOption{ + nack.ResponderSize(cfg.NACKBufferSize), + } + if cfg.NACKDisableCopy { + responderOpts = append(responderOpts, nack.DisableCopy()) + } + responder, err := nack.NewResponderInterceptor(responderOpts...) if err != nil { return nil, nil, err } @@ -252,7 +258,7 @@ func (s *Server) InitSession(cfg SessionConfig, closeCb func() error) error { return fmt.Errorf("failed to init media engine: %w", err) } - iRegistry, bwEstimatorCh, err := initInterceptors(mEngine) + iRegistry, bwEstimatorCh, err := initInterceptors(mEngine, s.cfg) if err != nil { return fmt.Errorf("failed to init interceptors: %w", err) } From a8d7adf9c7f343ef30c97f8ae863c82c99d3d7a7 Mon Sep 17 00:00:00 2001 From: Bill Gardner Date: Mon, 22 Dec 2025 12:08:08 -0500 Subject: [PATCH 4/7] allow cfg.NACKBufferSize = 0 to mean default --- service/rtc/config.go | 24 ++++++++++++++---------- service/rtc/config_test.go | 12 ++++++++++++ 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/service/rtc/config.go b/service/rtc/config.go index 3a1c69b..48cb7c2 100644 --- a/service/rtc/config.go +++ b/service/rtc/config.go @@ -82,18 +82,22 @@ func (c ServerConfig) IsValid() error { return fmt.Errorf("invalid UDPSocketsCount value: should be greater than 0") } - if c.NACKBufferSize < 32 { - return fmt.Errorf("invalid NACKBufferSize value: should be at least 32") - } + // NACKBufferSize validation only applies if explicitly set (non-zero) + // A zero value is allowed and will use the default from SetDefaults() + if c.NACKBufferSize != 0 { + if c.NACKBufferSize < 32 { + return fmt.Errorf("invalid NACKBufferSize value: should be at least 32") + } - if c.NACKBufferSize > 8192 { - return fmt.Errorf("invalid NACKBufferSize value: should not exceed 8192") - } + if c.NACKBufferSize > 8192 { + return fmt.Errorf("invalid NACKBufferSize value: should not exceed 8192") + } - // Buffer size must be a power of 2 (required by pion/interceptor ring buffer) - isPowerOfTwo := c.NACKBufferSize != 0 && (c.NACKBufferSize&(c.NACKBufferSize-1)) == 0 - if !isPowerOfTwo { - return fmt.Errorf("invalid NACKBufferSize value: must be a power of 2 (32, 64, 128, 256, 512, 1024, 2048, 4096, 8192)") + // Buffer size must be a power of 2 (required by pion/interceptor ring buffer) + isPowerOfTwo := c.NACKBufferSize&(c.NACKBufferSize-1) == 0 + if !isPowerOfTwo { + return fmt.Errorf("invalid NACKBufferSize value: must be a power of 2 (32, 64, 128, 256, 512, 1024, 2048, 4096, 8192)") + } } return nil diff --git a/service/rtc/config_test.go b/service/rtc/config_test.go index 2c1d229..3e3602c 100644 --- a/service/rtc/config_test.go +++ b/service/rtc/config_test.go @@ -134,6 +134,18 @@ func TestServerConfigIsValid(t *testing.T) { require.EqualError(t, err, "invalid NACKBufferSize value: must be a power of 2 (32, 64, 128, 256, 512, 1024, 2048, 4096, 8192)") }) + t.Run("valid with NACKBufferSize zero (uses default)", func(t *testing.T) { + var cfg ServerConfig + cfg.ICEAddressUDP = "127.0.0.1" + cfg.ICEPortUDP = 8443 + cfg.ICEPortTCP = 8443 + cfg.TURNConfig.CredentialsExpirationMinutes = 1440 + cfg.UDPSocketsCount = 1 + cfg.NACKBufferSize = 0 // Zero is allowed, will use default + err := cfg.IsValid() + require.NoError(t, err) + }) + t.Run("valid", func(t *testing.T) { var cfg ServerConfig cfg.ICEAddressUDP = "127.0.0.1" From 492056e498b1a35a5b93a0a5e8f63676a4c05978 Mon Sep 17 00:00:00 2001 From: Bill Gardner Date: Mon, 22 Dec 2025 12:19:45 -0500 Subject: [PATCH 5/7] remove comments to pass lint --- service/config.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/service/config.go b/service/config.go index 85cd949..62c200e 100644 --- a/service/config.go +++ b/service/config.go @@ -82,8 +82,8 @@ func (c *Config) SetDefaults() { c.RTC.ICEPortTCP = 8443 c.RTC.TURNConfig.CredentialsExpirationMinutes = 1440 c.RTC.UDPSocketsCount = rtc.GetDefaultUDPListeningSocketsCount() - c.RTC.NACKBufferSize = 256 // Default: 256 packets (~8.5 seconds at 30fps) - c.RTC.NACKDisableCopy = true // Default: true for backward compatibility (but not recommended) + c.RTC.NACKBufferSize = 256 + c.RTC.NACKDisableCopy = true c.Store.DataSource = "/tmp/rtcd_db" c.Logger.EnableConsole = true c.Logger.ConsoleJSON = false From 19881aea4786da024a2d82834cba3ae5dbd62f65 Mon Sep 17 00:00:00 2001 From: Bill Gardner Date: Mon, 22 Dec 2025 13:04:49 -0500 Subject: [PATCH 6/7] allow NACK buffersize equal to 0, apply default when creating interceptor --- service/rtc/config.go | 2 -- service/rtc/config_test.go | 2 +- service/rtc/sfu.go | 6 +++++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/service/rtc/config.go b/service/rtc/config.go index 48cb7c2..93ffc2d 100644 --- a/service/rtc/config.go +++ b/service/rtc/config.go @@ -82,8 +82,6 @@ func (c ServerConfig) IsValid() error { return fmt.Errorf("invalid UDPSocketsCount value: should be greater than 0") } - // NACKBufferSize validation only applies if explicitly set (non-zero) - // A zero value is allowed and will use the default from SetDefaults() if c.NACKBufferSize != 0 { if c.NACKBufferSize < 32 { return fmt.Errorf("invalid NACKBufferSize value: should be at least 32") diff --git a/service/rtc/config_test.go b/service/rtc/config_test.go index 3e3602c..da61667 100644 --- a/service/rtc/config_test.go +++ b/service/rtc/config_test.go @@ -141,7 +141,7 @@ func TestServerConfigIsValid(t *testing.T) { cfg.ICEPortTCP = 8443 cfg.TURNConfig.CredentialsExpirationMinutes = 1440 cfg.UDPSocketsCount = 1 - cfg.NACKBufferSize = 0 // Zero is allowed, will use default + cfg.NACKBufferSize = 0 // Zero is allowed, default applied when creating interceptor err := cfg.IsValid() require.NoError(t, err) }) diff --git a/service/rtc/sfu.go b/service/rtc/sfu.go index c2d7aaf..d1e4045 100644 --- a/service/rtc/sfu.go +++ b/service/rtc/sfu.go @@ -164,8 +164,12 @@ func initInterceptors(m *webrtc.MediaEngine, cfg ServerConfig) (*interceptor.Reg } // NACK + bufferSize := cfg.NACKBufferSize + if bufferSize == 0 { + bufferSize = 256 + } responderOpts := []nack.ResponderOption{ - nack.ResponderSize(cfg.NACKBufferSize), + nack.ResponderSize(bufferSize), } if cfg.NACKDisableCopy { responderOpts = append(responderOpts, nack.DisableCopy()) From 2e66de859e4989630093f90075f8e8b7680f50cc Mon Sep 17 00:00:00 2001 From: Bill Gardner Date: Fri, 16 Jan 2026 13:45:02 -0500 Subject: [PATCH 7/7] add docker-login as dependency of sign makefile target --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 9ad7835..30058f3 100644 --- a/Makefile +++ b/Makefile @@ -159,7 +159,7 @@ release: build github-release ## to build and release artifacts package: docker-login docker-build ## to build, package and push the artifact to a container registry .PHONY: sign -sign: docker-sign docker-verify ## to sign the artifact and perform verification +sign: docker-login docker-sign docker-verify ## to sign the artifact and perform verification .PHONY: lint lint: go-lint docker-lint ## to lint