mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-19 15:19:55 +00:00
Honor QualityLevel and CompressLevel pseudo-encodings
This commit is contained in:
@@ -69,6 +69,15 @@ const (
|
|||||||
pseudoEncDesktopName = -307
|
pseudoEncDesktopName = -307
|
||||||
pseudoEncExtendedDesktopSize = -308
|
pseudoEncExtendedDesktopSize = -308
|
||||||
|
|
||||||
|
// Quality/Compression level pseudo-encodings (TightVNC extension). The
|
||||||
|
// client picks one value from each range to tune JPEG quality and zlib
|
||||||
|
// effort. 0 is lowest quality / fastest, 9 is highest quality / best
|
||||||
|
// compression.
|
||||||
|
pseudoEncQualityLevelMin = -32
|
||||||
|
pseudoEncQualityLevelMax = -23
|
||||||
|
pseudoEncCompressLevelMin = -256
|
||||||
|
pseudoEncCompressLevelMax = -247
|
||||||
|
|
||||||
// Tight compression-control byte top nibble. Stream-reset bits 0-3
|
// Tight compression-control byte top nibble. Stream-reset bits 0-3
|
||||||
// (one per zlib stream) are unused while we run a single stream.
|
// (one per zlib stream) are unused while we run a single stream.
|
||||||
tightFillSubenc = 0x80
|
tightFillSubenc = 0x80
|
||||||
@@ -427,16 +436,65 @@ type tightState struct {
|
|||||||
// colorSeen is reused by sampledColorCount per rect; cleared via the Go
|
// colorSeen is reused by sampledColorCount per rect; cleared via the Go
|
||||||
// runtime's map-clear fast path to avoid a fresh allocation each call.
|
// runtime's map-clear fast path to avoid a fresh allocation each call.
|
||||||
colorSeen map[uint32]struct{}
|
colorSeen map[uint32]struct{}
|
||||||
|
// jpegQualityOverride forces a fixed JPEG quality on every rect when
|
||||||
|
// non-zero (set from the client's QualityLevel pseudo-encoding). Zero
|
||||||
|
// falls back to the area-based tiers in tightQualityFor.
|
||||||
|
jpegQualityOverride int
|
||||||
|
// qualityLevel and compressLevel are the 0..9 levels currently applied,
|
||||||
|
// or -1 if the client did not express a preference. Used to decide
|
||||||
|
// whether a SetEncodings refresh needs to recreate the tight state.
|
||||||
|
qualityLevel int
|
||||||
|
compressLevel int
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTightState() *tightState {
|
func newTightState() *tightState {
|
||||||
|
return newTightStateWithLevels(-1, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// newTightStateWithLevels builds a tightState whose zlib stream and JPEG
|
||||||
|
// quality reflect the client's QualityLevel / CompressLevel pseudo-encodings.
|
||||||
|
// Pass -1 for either level to keep our defaults (BestSpeed zlib and the
|
||||||
|
// area-tiered JPEG quality in tightQualityFor).
|
||||||
|
func newTightStateWithLevels(qualityLevel, compressLevel int) *tightState {
|
||||||
return &tightState{
|
return &tightState{
|
||||||
jpegBuf: &bytes.Buffer{},
|
jpegBuf: &bytes.Buffer{},
|
||||||
zlib: newZlibState(),
|
zlib: newZlibStateLevel(zlibLevelFor(compressLevel)),
|
||||||
colorSeen: make(map[uint32]struct{}, 64),
|
colorSeen: make(map[uint32]struct{}, 64),
|
||||||
|
jpegQualityOverride: jpegQualityForLevel(qualityLevel),
|
||||||
|
qualityLevel: qualityLevel,
|
||||||
|
compressLevel: compressLevel,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
func jpegQualityForLevel(level int) int {
|
||||||
|
if level < 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if level > 9 {
|
||||||
|
level = 9
|
||||||
|
}
|
||||||
|
// 0 -> 30, 9 -> 93. Linear so adjacent steps are perceptually similar.
|
||||||
|
return 30 + level*7
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// BestSpeed: matches the historical default before the pseudo-encoding
|
||||||
|
// was honoured.
|
||||||
|
func zlibLevelFor(level int) int {
|
||||||
|
if level < 1 {
|
||||||
|
return zlib.BestSpeed
|
||||||
|
}
|
||||||
|
if level > zlib.BestCompression {
|
||||||
|
return zlib.BestCompression
|
||||||
|
}
|
||||||
|
return level
|
||||||
|
}
|
||||||
|
|
||||||
// encodeTightRect emits a single Tight-encoded rect. Picks Fill for uniform
|
// encodeTightRect emits a single Tight-encoded rect. Picks Fill for uniform
|
||||||
// content, JPEG for photo-like rects above a size and color-count threshold,
|
// content, JPEG for photo-like rects above a size and color-count threshold,
|
||||||
// and Basic+zlib otherwise. Returns the rect header + Tight body (no
|
// and Basic+zlib otherwise. Returns the rect header + Tight body (no
|
||||||
@@ -496,7 +554,11 @@ func encodeTightFill(x, y, w, h int, r, g, b byte) []byte {
|
|||||||
func encodeTightJPEG(img *image.RGBA, x, y, w, h int, t *tightState) ([]byte, bool) {
|
func encodeTightJPEG(img *image.RGBA, x, y, w, h int, t *tightState) ([]byte, bool) {
|
||||||
t.jpegBuf.Reset()
|
t.jpegBuf.Reset()
|
||||||
sub := img.SubImage(image.Rect(img.Rect.Min.X+x, img.Rect.Min.Y+y, img.Rect.Min.X+x+w, img.Rect.Min.Y+y+h))
|
sub := img.SubImage(image.Rect(img.Rect.Min.X+x, img.Rect.Min.Y+y, img.Rect.Min.X+x+w, img.Rect.Min.Y+y+h))
|
||||||
if err := jpeg.Encode(t.jpegBuf, sub, &jpeg.Options{Quality: tightQualityFor(w * h)}); err != nil {
|
q := t.jpegQualityOverride
|
||||||
|
if q == 0 {
|
||||||
|
q = tightQualityFor(w * h)
|
||||||
|
}
|
||||||
|
if err := jpeg.Encode(t.jpegBuf, sub, &jpeg.Options{Quality: q}); err != nil {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
jpegBytes := t.jpegBuf.Bytes()
|
jpegBytes := t.jpegBuf.Bytes()
|
||||||
@@ -610,8 +672,12 @@ type zlibState struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newZlibState() *zlibState {
|
func newZlibState() *zlibState {
|
||||||
|
return newZlibStateLevel(zlib.BestSpeed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newZlibStateLevel(level int) *zlibState {
|
||||||
buf := &bytes.Buffer{}
|
buf := &bytes.Buffer{}
|
||||||
w, _ := zlib.NewWriterLevel(buf, zlib.BestSpeed)
|
w, _ := zlib.NewWriterLevel(buf, level)
|
||||||
return &zlibState{buf: buf, w: w}
|
return &zlibState{buf: buf, w: w}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,12 @@ type session struct {
|
|||||||
clientSupportsQEMUKey bool
|
clientSupportsQEMUKey bool
|
||||||
clientSupportsExtClipboard bool
|
clientSupportsExtClipboard bool
|
||||||
extClipCapsSent bool
|
extClipCapsSent bool
|
||||||
|
// clientJPEGQuality and clientZlibLevel hold the 0..9 levels the client
|
||||||
|
// advertised via the QualityLevel / CompressLevel pseudo-encodings, or
|
||||||
|
// -1 when the client has not expressed a preference. Applied to the
|
||||||
|
// tight encoder state after every SetEncodings.
|
||||||
|
clientJPEGQuality int
|
||||||
|
clientZlibLevel int
|
||||||
// prevFrame, curFrame and idleFrames live on the encoder goroutine and
|
// prevFrame, curFrame and idleFrames live on the encoder goroutine and
|
||||||
// must not be touched elsewhere. curFrame holds a session-owned copy of
|
// must not be touched elsewhere. curFrame holds a session-owned copy of
|
||||||
// the capturer's latest frame so the encoder works on a stable buffer
|
// the capturer's latest frame so the encoder works on a stable buffer
|
||||||
@@ -92,6 +98,8 @@ func (s *session) addr() string { return s.conn.RemoteAddr().String() }
|
|||||||
func (s *session) serve() {
|
func (s *session) serve() {
|
||||||
defer s.conn.Close()
|
defer s.conn.Close()
|
||||||
s.pf = defaultClientPixelFormat()
|
s.pf = defaultClientPixelFormat()
|
||||||
|
s.clientJPEGQuality = -1
|
||||||
|
s.clientZlibLevel = -1
|
||||||
s.encodeCh = make(chan fbRequest, 1)
|
s.encodeCh = make(chan fbRequest, 1)
|
||||||
|
|
||||||
if err := s.handshake(); err != nil {
|
if err := s.handshake(); err != nil {
|
||||||
@@ -335,11 +343,21 @@ func (s *session) handleSetEncodings() error {
|
|||||||
encs = append(encs, "ext-clipboard")
|
encs = append(encs, "ext-clipboard")
|
||||||
case encTight:
|
case encTight:
|
||||||
s.useTight = true
|
s.useTight = true
|
||||||
if s.tight == nil {
|
|
||||||
s.tight = newTightState()
|
|
||||||
}
|
|
||||||
encs = append(encs, "tight")
|
encs = append(encs, "tight")
|
||||||
}
|
}
|
||||||
|
switch {
|
||||||
|
case enc >= pseudoEncQualityLevelMin && enc <= pseudoEncQualityLevelMax:
|
||||||
|
s.clientJPEGQuality = int(enc - pseudoEncQualityLevelMin)
|
||||||
|
encs = append(encs, fmt.Sprintf("quality=%d", s.clientJPEGQuality))
|
||||||
|
case enc >= pseudoEncCompressLevelMin && enc <= pseudoEncCompressLevelMax:
|
||||||
|
s.clientZlibLevel = int(enc - pseudoEncCompressLevelMin)
|
||||||
|
encs = append(encs, fmt.Sprintf("compress=%d", s.clientZlibLevel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s.useTight && (s.tight == nil ||
|
||||||
|
s.tight.qualityLevel != s.clientJPEGQuality ||
|
||||||
|
s.tight.compressLevel != s.clientZlibLevel) {
|
||||||
|
s.tight = newTightStateWithLevels(s.clientJPEGQuality, s.clientZlibLevel)
|
||||||
}
|
}
|
||||||
sendExtClipCaps := s.clientSupportsExtClipboard && !s.extClipCapsSent
|
sendExtClipCaps := s.clientSupportsExtClipboard && !s.extClipCapsSent
|
||||||
if sendExtClipCaps {
|
if sendExtClipCaps {
|
||||||
@@ -877,7 +895,8 @@ func (s *session) handleExtCutText(payloadLen uint32) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
case extClipActionProvide:
|
case extClipActionProvide:
|
||||||
return s.handleExtClipProvide(flags, rest)
|
s.handleExtClipProvide(flags, rest)
|
||||||
|
return nil
|
||||||
default:
|
default:
|
||||||
s.log.Debugf("unknown ext clipboard action 0x%x", action)
|
s.log.Debugf("unknown ext clipboard action 0x%x", action)
|
||||||
return nil
|
return nil
|
||||||
@@ -885,22 +904,21 @@ func (s *session) handleExtCutText(payloadLen uint32) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handleExtClipProvide decodes a Provide payload and pushes the recovered
|
// handleExtClipProvide decodes a Provide payload and pushes the recovered
|
||||||
// text into the host clipboard. Errors and other unsupported formats (RTF,
|
// text into the host clipboard. Decode errors and unsupported formats (RTF,
|
||||||
// HTML, etc.) are swallowed so a malformed message doesn't tear down the
|
// HTML, etc.) are logged and dropped so a malformed message doesn't tear
|
||||||
// session.
|
// down the session.
|
||||||
func (s *session) handleExtClipProvide(flags uint32, payload []byte) error {
|
func (s *session) handleExtClipProvide(flags uint32, payload []byte) {
|
||||||
if len(payload) == 0 {
|
if len(payload) == 0 {
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
text, err := parseExtClipProvideText(flags, payload)
|
text, err := parseExtClipProvideText(flags, payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.log.Debugf("parse ext clipboard provide: %v", err)
|
s.log.Debugf("parse ext clipboard provide: %v", err)
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
if text != "" {
|
if text != "" {
|
||||||
s.injector.SetClipboard(text)
|
s.injector.SetClipboard(text)
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// sendExtClipProvideText answers an inbound Request(text) with the current
|
// sendExtClipProvideText answers an inbound Request(text) with the current
|
||||||
|
|||||||
Reference in New Issue
Block a user