Skip to content

Commit 6a7d684

Browse files
mcuelenaereclaude
andcommitted
fix(video): make pause/resume race-free by routing through the new dispatcher
Replaces the inline switch in onRPCMessage with a pair of regular RPCHandler entries flagged TakesSession + Synchronous. The dispatcher runs them on the per-session rpcQueue pump goroutine — which is the single sequential consumer of the queue — so a rapid pause→resume pair can no longer be reordered by the Go scheduler. Previously each RPC was dispatched via "go onRPCMessage(...)", letting the scheduler interleave the two helpers so a release could land after the matching acquire and leave the encoder stopped while the tab was visible. Reported by Cursor Bugbot: #1455 (comment) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 0326d1e commit 6a7d684

2 files changed

Lines changed: 18 additions & 16 deletions

File tree

jsonrpc.go

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -126,22 +126,6 @@ func onRPCMessage(message webrtc.DataChannelMessage, session *Session) {
126126
return
127127
}
128128

129-
// pauseVideo / resumeVideo are session-bound notifications: they
130-
// toggle this session's slot in the video stream refcount (see
131-
// video.go). Handled inline because the generic dispatcher doesn't
132-
// pass *Session, and acting on currentSession instead of the
133-
// receiving session would let a stale data channel mis-target the
134-
// active one during the 1s handover overlap. Both helpers are
135-
// idempotent so rapid pause/resume bursts are safe.
136-
switch request.Method {
137-
case "pauseVideo":
138-
releaseVideoStream(session.videoConsumerKey())
139-
return
140-
case "resumeVideo":
141-
acquireVideoStream(session.videoConsumerKey())
142-
return
143-
}
144-
145129
handler, ok := rpcHandlers[request.Method]
146130
if !ok {
147131
errorResponse := JSONRPCResponse{
@@ -1381,6 +1365,8 @@ var rpcHandlers = map[string]RPCHandler{
13811365
"wheelReport": {Func: rpcWheelReport, Params: []string{"wheelY", "wheelX"}},
13821366
"wakeHost": {Func: rpcWakeHost},
13831367
"getVideoState": {Func: rpcGetVideoState},
1368+
"pauseVideo": {Func: rpcPauseVideo, TakesSession: true, Synchronous: true},
1369+
"resumeVideo": {Func: rpcResumeVideo, TakesSession: true, Synchronous: true},
13841370
"getUSBState": {Func: rpcGetUSBState},
13851371
"unmountImage": {Func: rpcUnmountImage},
13861372
"rpcMountBuiltInImage": {Func: rpcMountBuiltInImage, Params: []string{"filename"}},

video.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,22 @@ func updateHostDisplayAdvertisement(reason string, force bool) error {
157157
return setHostDisplayAdvertisedLocked(shouldAdvertiseHostDisplayLocked(), reason, force)
158158
}
159159

160+
// rpcPauseVideo releases this session's slot in the video stream
161+
// refcount. Registered with TakesSession + Synchronous so it acts on
162+
// the receiving session and can't be reordered relative to a
163+
// resumeVideo from the same source.
164+
func rpcPauseVideo(s *Session) error {
165+
releaseVideoStream(s.videoConsumerKey())
166+
return nil
167+
}
168+
169+
// rpcResumeVideo re-acquires this session's slot. See rpcPauseVideo for
170+
// the dispatcher flags this relies on.
171+
func rpcResumeVideo(s *Session) error {
172+
acquireVideoStream(s.videoConsumerKey())
173+
return nil
174+
}
175+
160176
type rpcVideoSleepModeResponse struct {
161177
Supported bool `json:"supported"`
162178
Enabled bool `json:"enabled"`

0 commit comments

Comments
 (0)