From 5543404188578fe9489ac19018c0b8122a6962e1 Mon Sep 17 00:00:00 2001 From: Viktor Liu Date: Mon, 18 May 2026 14:07:26 +0200 Subject: [PATCH] Cap honored VNC client JPEG quality at 50 --- client/vnc/server/extclipboard.go | 2 +- client/vnc/server/extclipboard_test.go | 2 +- client/vnc/server/rfb.go | 20 ++++++++++++++++---- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/client/vnc/server/extclipboard.go b/client/vnc/server/extclipboard.go index 38234b007..ba7dba386 100644 --- a/client/vnc/server/extclipboard.go +++ b/client/vnc/server/extclipboard.go @@ -16,7 +16,7 @@ import ( // - UTF-8 text format (legacy is Latin-1). // - Pull-based: a Notify announces "I have new content", the peer fetches // via Request only when it actually needs the data. Saves bandwidth on -// a high-latency relay path versus pushing every change. +// high-latency transports versus pushing every change. // - zlib-compressed payloads. // - Caps negotiation so each side knows the other's per-format max size. // diff --git a/client/vnc/server/extclipboard_test.go b/client/vnc/server/extclipboard_test.go index 43c278bc3..70a106af9 100644 --- a/client/vnc/server/extclipboard_test.go +++ b/client/vnc/server/extclipboard_test.go @@ -16,7 +16,7 @@ func TestBuildExtClipCaps(t *testing.T) { require.Len(t, payload, 8, "Caps with one format should be 4 bytes flags + 4 bytes size") flags := binary.BigEndian.Uint32(payload[0:4]) - // noVNC checks individual action bits in our Caps to decide whether to + // Clients check individual action bits in our Caps to decide whether to // auto-Request on Notify, so all supported actions must be advertised. assert.NotZero(t, flags&extClipActionCaps, "Caps action bit must be set") assert.NotZero(t, flags&extClipActionRequest, "Request action bit must be set") diff --git a/client/vnc/server/rfb.go b/client/vnc/server/rfb.go index 93e71d84f..bf25d7632 100644 --- a/client/vnc/server/rfb.go +++ b/client/vnc/server/rfb.go @@ -472,8 +472,11 @@ func newTightStateWithLevels(qualityLevel, compressLevel int) *tightState { } // jpegQualityForLevel maps a 0..9 client preference to a JPEG quality value. -// Returns 0 when no preference is set (-1), letting the encoder fall back to -// the area-based tiers. +// Returns 0 when no preference is set (-1), letting the encoder fall back +// to the area-based tiers. The output is capped at jpegQualityClientCap +// so a client asking for the highest quality does not push per-frame JPEG +// byte counts into a regime that overwhelms bandwidth-constrained +// transports. Within the cap the mapping is still linear. func jpegQualityForLevel(level int) int { if level < 0 { return 0 @@ -481,10 +484,19 @@ func jpegQualityForLevel(level int) int { if level > 9 { level = 9 } - // 0 -> 30, 9 -> 93. Linear so adjacent steps are perceptually similar. - return 30 + level*7 + q := 30 + level*7 + if q > jpegQualityClientCap { + q = jpegQualityClientCap + } + return q } +// jpegQualityClientCap upper-bounds the JPEG quality we honour from the +// client's QualityLevel pseudo-encoding. 50 keeps full-screen JPEGs in +// the same byte range as the area-tiered defaults used when the client +// does not express a preference. +const jpegQualityClientCap = 50 + // zlibLevelFor maps a 0..9 client preference to a zlib compression level. // Level 0 ("no compression") would emit larger output than input on most // rects, so we floor to BestSpeed (1). -1 (no preference) also picks