Skip to content

Commit eeabe73

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: jetkvm#1455 (comment) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent e7bc932 commit eeabe73

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
@@ -122,22 +122,6 @@ func onRPCMessage(message webrtc.DataChannelMessage, session *Session) {
122122
return
123123
}
124124

125-
// pauseVideo / resumeVideo are session-bound notifications: they
126-
// toggle this session's slot in the video stream refcount (see
127-
// video.go). Handled inline because the generic dispatcher doesn't
128-
// pass *Session, and acting on currentSession instead of the
129-
// receiving session would let a stale data channel mis-target the
130-
// active one during the 1s handover overlap. Both helpers are
131-
// idempotent so rapid pause/resume bursts are safe.
132-
switch request.Method {
133-
case "pauseVideo":
134-
releaseVideoStream(session.videoConsumerKey())
135-
return
136-
case "resumeVideo":
137-
acquireVideoStream(session.videoConsumerKey())
138-
return
139-
}
140-
141125
handler, ok := rpcHandlers[request.Method]
142126
if !ok {
143127
errorResponse := JSONRPCResponse{
@@ -1317,6 +1301,8 @@ var rpcHandlers = map[string]RPCHandler{
13171301
"relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}},
13181302
"wheelReport": {Func: rpcWheelReport, Params: []string{"wheelY", "wheelX"}},
13191303
"getVideoState": {Func: rpcGetVideoState},
1304+
"pauseVideo": {Func: rpcPauseVideo, TakesSession: true, Synchronous: true},
1305+
"resumeVideo": {Func: rpcResumeVideo, TakesSession: true, Synchronous: true},
13201306
"getUSBState": {Func: rpcGetUSBState},
13211307
"unmountImage": {Func: rpcUnmountImage},
13221308
"rpcMountBuiltInImage": {Func: rpcMountBuiltInImage, Params: []string{"filename"}},

video.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,22 @@ func rpcGetVideoState() (native.VideoState, error) {
8585
return lastVideoState, nil
8686
}
8787

88+
// rpcPauseVideo releases this session's slot in the video stream
89+
// refcount. Registered with TakesSession + Synchronous so it acts on
90+
// the receiving session and can't be reordered relative to a
91+
// resumeVideo from the same source.
92+
func rpcPauseVideo(s *Session) error {
93+
releaseVideoStream(s.videoConsumerKey())
94+
return nil
95+
}
96+
97+
// rpcResumeVideo re-acquires this session's slot. See rpcPauseVideo for
98+
// the dispatcher flags this relies on.
99+
func rpcResumeVideo(s *Session) error {
100+
acquireVideoStream(s.videoConsumerKey())
101+
return nil
102+
}
103+
88104
type rpcVideoSleepModeResponse struct {
89105
Supported bool `json:"supported"`
90106
Enabled bool `json:"enabled"`

0 commit comments

Comments
 (0)