Skip to content

Commit ccc66b3

Browse files
mcuelenaereclaude
andcommitted
vnc: cycle codec to H.264 when VNC is enabled mid-session
If a browser tab negotiated H.265 before VNC was toggled on, the capture pipeline keeps emitting H.265 NALs. resolveCodec only forces H.264 for *new* WebRTC sessions; the in-flight one is not disturbed. So when a TigerVNC client subsequently connected, it received H.265 NALs marked as OpenH264 (encoding 50), which the spec defines as H.264-only — the client's decoder failed silently and the user kept seeing the (already-rendered) placeholder. After enabling VNC via JSON-RPC, check the running codec; if it is not H.264, cycle the pipeline (VideoStop → SetCodecType(0) → VideoStart). The active WebRTC session will see frames briefly disappear and then need to refresh — the warning log says so. Also adds trace-level diagnostics so the user can see what is happening with `JETKVM_LOG_TRACE=vnc`: - codec value at the moment a VNC client connects - dropped non-IDR frames during the keyframe wait - rect emissions (OpenH264 size/flags or Raw placeholder) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1d41410 commit ccc66b3

2 files changed

Lines changed: 38 additions & 0 deletions

File tree

vnc.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,42 @@ func rpcSetVNCConfig(cfg VNCConfig) error {
7474
vncLogger.Error().Err(err).Msg("failed to start VNC server after config change")
7575
return err
7676
}
77+
// Force the encoder to H.264 if a stream is already running
78+
// at H.265. Without this, a browser tab that negotiated H.265
79+
// before VNC was enabled would keep the pipeline at H.265,
80+
// and any VNC client would receive H.265 NALs marked as
81+
// OpenH264 (encoding 50, H.264-only) and silently fail to
82+
// decode. Cycling the stream briefly interrupts the active
83+
// WebRTC session — the user will need to refresh the browser.
84+
forceVideoCodecForVNC()
7785
}
7886
return nil
7987
}
8088

89+
// forceVideoCodecForVNC makes sure the running capture pipeline emits
90+
// H.264 once VNC is enabled. If no consumers are running, the codec
91+
// gets pinned but the pipeline stays stopped — the next acquirer's
92+
// VideoStart will pick it up.
93+
func forceVideoCodecForVNC() {
94+
curr, err := nativeInstance.VideoGetCodecType()
95+
if err != nil {
96+
vncLogger.Warn().Err(err).Msg("could not read current video codec; skipping coercion")
97+
return
98+
}
99+
if curr == 0 {
100+
return // already H.264
101+
}
102+
if !videoStreamHasConsumers() {
103+
// Pipeline is idle. Just pin the codec; next start picks it up.
104+
_ = nativeInstance.VideoSetCodecType(0)
105+
return
106+
}
107+
vncLogger.Warn().Int("from", curr).Msg("cycling capture pipeline to H.264 for VNC; refresh active browser tabs")
108+
_ = nativeInstance.VideoStop()
109+
_ = nativeInstance.VideoSetCodecType(0)
110+
_ = nativeInstance.VideoStart()
111+
}
112+
81113
// StartVNCServer starts the VNC TCP listener if VNC is enabled.
82114
// Idempotent: a second call while running is a no-op.
83115
func StartVNCServer() error {
@@ -222,6 +254,9 @@ func (s *VNCServer) addClient(c *vncConn) (sps, pps []byte) {
222254
if first {
223255
acquireVideoStreamWithCodec(vncVideoConsumer, 0) // 0 = H.264
224256
}
257+
if curr, err := nativeInstance.VideoGetCodecType(); err == nil {
258+
c.l.Trace().Int("codec", curr).Msg("video codec at VNC client connect (0=H.264, 1=H.265)")
259+
}
225260
s.paramsMu.Lock()
226261
defer s.paramsMu.Unlock()
227262
return append([]byte(nil), s.sps...), append([]byte(nil), s.pps...)

vnc_conn.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ func (c *vncConn) dispatchLoop() {
175175
}
176176
c.stateMu.Unlock()
177177
if drop {
178+
c.l.Trace().Int("size", len(pkt.data)).Msg("dropping non-IDR frame while waiting for keyframe")
178179
select {
179180
case c.updateNeeded <- struct{}{}:
180181
default:
@@ -274,6 +275,7 @@ func (c *vncConn) writeUpdate(pkt vncFramePacket) error {
274275
payload = pkt.data
275276
}
276277

278+
c.l.Trace().Uint16("w", w).Uint16("h", h).Int("size", len(payload)).Uint32("flags", flags).Bool("primed", c.primed).Msg("emitting OpenH264 rect")
277279
if err := c.conn.WriteOpenH264Rect(
278280
rfb.Rect{X: 0, Y: 0, W: w, H: h, Encoding: rfb.EncodingOpenH264},
279281
flags, payload,
@@ -282,6 +284,7 @@ func (c *vncConn) writeUpdate(pkt vncFramePacket) error {
282284
}
283285
} else if !hasH264 {
284286
pixels := rfb.PlaceholderImage(int(w), int(h))
287+
c.l.Trace().Uint16("w", w).Uint16("h", h).Msg("emitting Raw placeholder rect (client did not advertise OpenH264)")
285288
if err := c.conn.WriteRawRect(
286289
rfb.Rect{X: 0, Y: 0, W: w, H: h, Encoding: rfb.EncodingRaw},
287290
pixels,

0 commit comments

Comments
 (0)