In rtcConnection.HandlePacket the backend->client reader goroutine was
spawned unconditionally on every inbound client packet, which leaked
goroutines under steady-state WHEP load (STUN keepalives + RTCP feedback).
Guard the spawn with sync.Once so it runs exactly once per connection.
In SRTConnection.handleHandshake apply the same sync.Once guard so a
client retry of handshake 2 (because our handshake 3 was dropped) does
not race a second reader on backendUDP.Read.
In both Run loops, hoist the 4096-byte receive buffer out of the read
loop; callers consume the slice synchronously and the kernel copies on
Write/sendto, so no caller retains the slice past the call. To keep this
safe for SRT, clone ExtraData in SRTHandshakePacket.UnmarshalBinary so
decoded handshakes do not alias the reused receive buffer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
srs_bench and other legacy clients post the SDP offer as
{"sdp":"v=0\r\n...","streamurl":"..."} to /rtc/v1/play/ (and
/rtc/v1/publish/). The proxy was passing that raw body straight into
ParseIceUfragPwd, whose [^\s]+ class did not stop at the literal "\"
characters of the JSON-escaped newlines, so the captured ufrag absorbed
the next attributes. The contaminated ufrag was stored in the LB while
the player's STUN binding carried the clean wire ufrag, so
LoadWebRTCByUfrag missed and playback never started.
Add unwrapSDPEnvelope to extract the sdp field when the body is a JSON
envelope (forwarded bytes and the candidate port rewrite still operate
on the raw envelope so the client sees a valid response), and tighten
ParseIceUfragPwd to stop at backslash as well as whitespace.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Refactor the Go proxy for dependency injection: every proxy server,
the bootstrap, the signal handler, the load balancers, and AMF0 now accept
functional-option seams (factories/closures) so tests can inject fakes
without binding real sockets, talking to real Redis, or racing on
package globals.
- Drop the package-global `lb.SrsLoadBalancer`. The bootstrap creates
the LB locally and threads it through every proxy server constructor. Two old
global indirections in `internal/signal` and `internal/rtmp/amf0` are
likewise replaced by per-instance fields.
- Rename `internal/server` → `internal/proxy` and rename the `lb` public
surface for clarity: `SRSLoadBalancer` is split into `OriginService` /
`HLSService` / `RTCService` and recomposed as `OriginLoadBalancer`;
`SRSServer` → `OriginServer`; all proxy server types gain a `Proxy`
qualifier (e.g. `RTMPServer` → `RTMPProxyServer`).
- Extract the Redis client behind a new `internal/redisclient` package
with a minimal `RedisClient` interface and a counterfeiter fake.
- Add counterfeiter fakes (`proxyfakes`, `lbfakes`, `redisclientfakes`)
and ~7.5k lines of unit tests covering bootstrap, memory + Redis LBs, all
five proxy servers, the signal handler, and AMF0.
- Add two new E2E flows — `proxy-e2e-srt-test.sh` (SRT publish through
proxy, verify SRT/RTMP/HTTP-FLV/HLS playback) and `proxy-e2e-whip-test.sh`
(WHIP publish, verify RTMP/HTTP-FLV/HLS via origin `rtc_to_rtmp`) — plus
`setup-ffmpeg-with-whip.sh`, a macOS builder for an ffmpeg with
openssl-DTLS WHIP and SRT support that the two scripts auto-invoke when needed.
- Workspace reorg: move `memory/` and `skills/` to the repo root so all
agent tools (Claude / Codex / Kiro / OpenClaw) share one source of truth via
symlinks. Sync `docs/proxy/proxy-load-balancer.md` and
`memory/srs-codebase-map.md` with the new names.
No protocol, log, HTTP API, or wire-format changes. Refactor only — all
externally observable proxy behavior is unchanged.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: chatgpt-codex-connector[bot] <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com>