Skip to content

Commit e70e272

Browse files
rgarciacursoragent
andcommitted
feat: lazy-start Neko connection on first /display/webrtc request
The relay no longer connects to Neko eagerly on server startup. Instead, the Neko WebRTC connection is established on-demand when the first client hits /display/webrtc, avoiding the cost of an idle WebRTC session when no one is consuming the screen stream. - Move reconnect loop into Relay.ensureRunning() behind sync.Once - HandleWebSocket triggers ensureRunning() then waits up to 15s for the relay to become ready - Simplify main.go: no background goroutine, just register endpoint Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 3505943 commit e70e272

File tree

2 files changed

+47
-19
lines changed

2 files changed

+47
-19
lines changed

server/cmd/api/main.go

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,10 @@ func main() {
138138

139139
// WebRTC relay: connects to Neko as a headless viewer and re-serves
140140
// the VP8 video stream to external WebRTC clients via a single
141-
// WebSocket signaling endpoint.
141+
// WebSocket signaling endpoint. The Neko connection is lazy —
142+
// it only starts when the first client connects.
142143
if config.WebRTCRelayEnabled {
143-
relay, err := webrtcscreen.NewRelay(webrtcscreen.RelayConfig{
144+
relay, err := webrtcscreen.NewRelay(ctx, webrtcscreen.RelayConfig{
144145
NekoBaseURL: "http://127.0.0.1:8080",
145146
NekoUser: "admin",
146147
NekoPass: adminPassword,
@@ -150,24 +151,9 @@ func main() {
150151
slogger.Error("failed to create webrtc relay", "err", err)
151152
os.Exit(1)
152153
}
153-
go func() {
154-
defer relay.Close()
155-
for {
156-
err := relay.Start(ctx)
157-
if ctx.Err() != nil {
158-
return
159-
}
160-
slogger.Warn("webrtc relay disconnected, reconnecting in 3s", "err", err)
161-
select {
162-
case <-ctx.Done():
163-
return
164-
case <-time.After(3 * time.Second):
165-
}
166-
}
167-
}()
168154

169155
r.Get("/display/webrtc", relay.HandleWebSocket)
170-
slogger.Info("webrtc relay endpoint enabled at /display/webrtc")
156+
slogger.Info("webrtc relay endpoint registered at /display/webrtc (lazy start)")
171157
}
172158

173159
srv := &http.Server{

server/lib/webrtcscreen/relay.go

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,21 @@ import (
2020
// WebRTC clients. The API server mounts HandleWebSocket on a
2121
// single endpoint (e.g., /display/webrtc) — that's all external
2222
// clients need to connect and receive the live screen.
23+
//
24+
// The Neko connection is lazy: it is only established when the
25+
// first client connects via HandleWebSocket.
2326
type Relay struct {
2427
logger *slog.Logger
2528
cfg RelayConfig
29+
ctx context.Context
2630

2731
mu sync.RWMutex
2832
localTrack *webrtc.TrackLocalStaticRTP
2933
nekoPC *webrtc.PeerConnection
3034
nekoWS *cws.Conn
3135
ready chan struct{} // closed when localTrack is receiving data
36+
37+
startOnce sync.Once
3238
}
3339

3440
type RelayConfig struct {
@@ -38,7 +44,7 @@ type RelayConfig struct {
3844
Logger *slog.Logger
3945
}
4046

41-
func NewRelay(cfg RelayConfig) (*Relay, error) {
47+
func NewRelay(ctx context.Context, cfg RelayConfig) (*Relay, error) {
4248
if cfg.Logger == nil {
4349
cfg.Logger = slog.Default()
4450
}
@@ -54,11 +60,35 @@ func NewRelay(cfg RelayConfig) (*Relay, error) {
5460
return &Relay{
5561
logger: cfg.Logger.With("component", "webrtc-relay"),
5662
cfg: cfg,
63+
ctx: ctx,
5764
localTrack: localTrack,
5865
ready: make(chan struct{}),
5966
}, nil
6067
}
6168

69+
// ensureRunning starts the Neko connection loop in the background
70+
// on the first call. Subsequent calls are no-ops.
71+
func (r *Relay) ensureRunning() {
72+
r.startOnce.Do(func() {
73+
r.logger.Info("first client request, starting neko connection")
74+
go func() {
75+
defer r.Close()
76+
for {
77+
err := r.Start(r.ctx)
78+
if r.ctx.Err() != nil {
79+
return
80+
}
81+
r.logger.Warn("webrtc relay disconnected, reconnecting in 3s", "err", err)
82+
select {
83+
case <-r.ctx.Done():
84+
return
85+
case <-time.After(3 * time.Second):
86+
}
87+
}
88+
}()
89+
})
90+
}
91+
6292
// Start connects to Neko and begins relaying video. It blocks until
6393
// the Neko connection drops or ctx is cancelled. Callers should call
6494
// Start in a loop for automatic reconnection.
@@ -229,6 +259,18 @@ func (r *Relay) Start(ctx context.Context) error {
229259
// After the exchange, WebRTC media flows directly. The WebSocket
230260
// can be closed.
231261
func (r *Relay) HandleWebSocket(w http.ResponseWriter, req *http.Request) {
262+
r.ensureRunning()
263+
264+
// Wait for the relay to be connected to Neko before accepting the client.
265+
select {
266+
case <-r.Ready():
267+
case <-time.After(15 * time.Second):
268+
http.Error(w, "relay not ready", http.StatusServiceUnavailable)
269+
return
270+
case <-req.Context().Done():
271+
return
272+
}
273+
232274
ws, err := cws.Accept(w, req, nil)
233275
if err != nil {
234276
r.logger.Error("websocket accept failed", "error", err)

0 commit comments

Comments
 (0)