PyVNCServer is designed for projects that need more control than a black-box VNC server provides. The protocol stack, capture pipeline, encoding selection and connection lifecycle are all implemented in Python and can be extended or embedded directly.
Highlights:
doctor, config lifecycle, capture benchmark and local release checks.For the complete guides, configuration reference, architecture and troubleshooting documentation, see https://xulek.github.io/PyVNCServer/.
| Client / transport | Status | Notes |
|---|---|---|
| UltraVNC Viewer | ✅ Regression-covered | Tight/RRE fixes retained; Raw/RRE/Hextile/Zlib/ZRLE/Tight switching is covered by end-to-end tests |
| Standard RFB 3.8 clients | ✅ Supported | Client must advertise at least one encoding implemented by the server |
| noVNC / browser clients | ✅ Supported transport | noVNC is tracked as web/noVNC; enable WebSocket and configure an Origin allowlist |
| Raw TCP VNC | ✅ Supported | Default transport |
| VeNCrypt 0.2 | ✅ Protocol + real TLS tested | Security type 19; X509None/X509Vnc recommended |
| Legacy TLS-wrapped VNC | ✅ Optional | Pre-RFB TLS wrapper retained for compatibility |
[!NOTE]
Tightencoding andTight security/capability negotiationare different concepts. Tight capability negotiation does not provide transport encryption by itself.
Clone recursively to initialize the bundled noVNC submodule:
git clone --recurse-submodules https://github.com/xulek/PyVNCServer.git
cd PyVNCServer
python -m pip install -U pip
python -m pip install -e .
If you already cloned the repository without submodules:
git submodule update --init --recursive
For the faster optional capture stack:
python -m pip install -e ".[performance]"
Optional H.264 support:
python -m pip install -e ".[h264]"
Install both:
python -m pip install -e ".[performance,h264]"
pyvncserver serve
The packaged defaults listen on:
127.0.0.1:5900
You can also run the package directly:
python -m pyvncserver serve
Use a custom TOML configuration:
pyvncserver serve --config config/pyvncserver.toml
Override the configured log level:
pyvncserver serve --log-level DEBUG
For a local test:
127.0.0.1:5900.Auto, Tight, ZRLE or Hextile as the preferred encoding.PyVNCServer includes specific UltraVNC interoperability fixes:
Relevant compatibility switches:
[features]
tight_stream_reset_for_ultravnc = false
[limits]
tight_disable_for_ultravnc = false
Use these as troubleshooting switches rather than enabling them automatically for every client.
More details: UltraVNC documentation.
WebSocket support is disabled by default. Enable it explicitly:
[features]
enable_websocket = true
[websocket]
allowed_origins = [
"http://127.0.0.1:6080",
"http://localhost:6080",
]
The WebSocket implementation accepts the binary transport used by noVNC and enforces:
noVNC is tracked as the web/noVNC Git submodule. PyVNCServer provides the VNC WebSocket transport; serve the noVNC frontend with your preferred HTTP server or reverse proxy.
More details: noVNC & WebSocket documentation.
PyVNCServer 3.4 adds protocol support aimed especially at modern viewers and noVNC:
| Extension | ID / message | Status |
|---|---|---|
| ContinuousUpdates | -313 / client 150 |
✅ Push-style framebuffer updates |
| Fence | -312 / 248 |
✅ Negotiation probe + request/response |
| LastRect | -224 |
✅ Implemented, opt-in by default |
| ExtendedDesktopSize | -308 |
✅ Layout reporting and resize status |
| SetDesktopSize | client 251 |
✅ Parsed; host resize rejected safely by default |
ContinuousUpdates runs inside the existing per-client session thread, so Tight/Zlib encoder state remains ordered. ExtendedDesktopSize now uses the correct 16-byte SCREEN record and immediately advertises the current screen layout when negotiated.
| Encoding | ID | Implementation | Typical use |
|---|---|---|---|
| Raw | 0 |
✅ | Simple baseline, LAN/debugging |
| CopyRect | 1 |
✅ | Efficient screen movement/scroll-like updates |
| RRE | 2 |
✅ | Flat-color / simple regions |
| Hextile | 5 |
✅ | General compatibility with tiled updates |
| Zlib | 6 |
✅ | General-purpose compressed rectangles |
| Tight | 7 |
✅ | Strong VNC client compatibility and compression |
| ZRLE | 16 |
✅ | Efficient tiled zlib/RLE encoding |
| JPEG path | extension | ✅ Optional path | Image-like content where lossy compression is appropriate |
| H.264 path | extension | ⚙️ Optional | Requires av; extension/client support is required |
The server negotiates only encodings advertised by the client and can select different encodings for different update regions.
See the encoding reference for protocol and fallback details.
capture_backend = "auto" selects the best available backend.
| Backend | Platform | Notes |
|---|---|---|
| DXCam / DXGI | Windows | Preferred optional high-performance path |
| MSS | Cross-platform | Main portable capture backend |
Pillow ImageGrab |
Platform dependent | Fallback capture path |
Install performance extras on Windows to make DXCam available:
python -m pip install -e ".[performance]"
The server-wide CaptureProducer captures once and publishes generations of the framebuffer to client sessions. This avoids scaling capture work linearly with the number of connected clients.
[!IMPORTANT] On Windows with DXCam 0.3.0+, PyVNCServer can harvest native DXGI dirty/move rectangles. If metadata cannot be read safely, it falls back to the software change detector instead of assuming that the screen did not change.
Combined desktop capture is available through MSS:
[server]
capture_backend = "auto"
capture_all_monitors = true
monitor_index = 0
When enabled with capture_backend = "auto", PyVNCServer prefers MSS monitor 0, which represents the virtual desktop spanning all displays. Physical monitor coordinates are normalized to the RFB framebuffer origin and reported through ExtendedDesktopSize.
DXCam still captures one DXGI output per camera; use MSS/auto for a combined multi-monitor framebuffer.
See docs/MULTI_MONITOR.md.
PyVNCServer deliberately uses conservative defaults.
[server]
host = "127.0.0.1"
port = 5900
handshake_timeout = 5.0
max_connections = 10
max_connections_per_ip = 4
max_unauthenticated_connections = 4
[security]
password = ""
read_only_password = ""
allow_insecure_no_auth = false
The server refuses an unauthenticated non-loopback bind unless you explicitly opt into it.
Classic VNC authentication:
For untrusted networks, prefer VeNCrypt X509Vnc, SSH tunnelling or a VPN.
VeNCrypt is negotiated as normal RFB security type 19; TLS begins only after the client selects a VeNCrypt subtype.
Recommended password-authenticated configuration:
[security]
password = "secret"
vencrypt_enabled = true
vencrypt_subtypes = ["x509-vnc"]
tls_cert_file = "server.crt"
tls_key_file = "server.key"
tls_minimum_version = "1.2"
require_encrypted_transport = true
Supported v3.5 subtypes:
| VeNCrypt subtype | ID | User authentication | Server identity |
|---|---|---|---|
| X509None | 260 |
none | X.509 certificate |
| X509Vnc | 261 |
classic VNC auth inside TLS | X.509 certificate |
| TLSNone | 257 |
none | anonymous TLS |
| TLSVnc | 258 |
classic VNC auth inside TLS | anonymous TLS |
TLSNone / TLSVnc are legacy anonymous-TLS compatibility modes and are disabled unless vencrypt_allow_anonymous_tls = true.
If a VNC password exists, PyVNCServer will not offer a *None subtype unless vencrypt_allow_no_auth_with_password = true is explicitly set. This prevents accidentally turning encryption into an authentication bypass.
The old pre-RFB TLS wrapper remains available:
[security]
tls_enabled = true
tls_cert_file = "server.crt"
tls_key_file = "server.key"
tls_minimum_version = "1.2"
tls_enabled and vencrypt_enabled are intentionally mutually exclusive.
[security]
auth_max_failures = 5
auth_failure_window_seconds = 30.0
auth_backoff_max_seconds = 2.0
Additional protections include connection admission limits, handshake timeouts and WebSocket payload limits.
See the full security guide.
The default v4.1 performance policy favors fresh frames over queued frames. On LAN/localhost this means smaller TCP send queues, a faster shared capture producer, stale ContinuousUpdates frame dropping and fewer unnecessary copies.
[performance]
profile = "low-latency"
capture_producer_fps = 120
socket_send_buffer_bytes = 262144
socket_receive_buffer_bytes = 131072
framebuffer_send_coalesce_bytes = 131072
producer_conversion_cache_entries = 4
producer_conversion_cache_max_bytes = 67108864
drop_stale_continuous_frames = true
Measure CPU-side hot paths locally:
PYTHONPATH=src python benchmarks/benchmark_latency_pipeline.py --iterations 100
For meaningful end-to-end numbers, run benchmark_lan_latency.py against the actual Windows/DXGI host and viewer/network path.
The safe packaged configuration is stored in:
src/pyvncserver/default_config.toml
A project-level example is available at:
config/pyvncserver.toml
Important options:
[server]
host = "127.0.0.1"
port = 5900
frame_rate = 30
lan_frame_rate = 90
network_profile_override = "auto"
scale_factor = 1.0
capture_backend = "auto"
monitor_index = 0
capture_all_monitors = false
max_connections = 10
input_control_policy = "single-controller"
[features]
enable_region_detection = true
enable_metrics = true
enable_request_coalescing = true
enable_lan_adaptive_encoding = true
enable_websocket = false
enable_copyrect_encoding = true
enable_zrle_encoding = true
enable_tight_encoding = true
enable_jpeg_encoding = true
enable_h264_encoding = false
enable_parallel_encoding = true
enable_capture_producer = true
enable_dxgi_metadata = true
enable_continuous_updates = true
enable_fence = true
enable_last_rect = false
enable_extended_desktop_size = true
allow_client_resize = false
[adaptive]
enabled = true
min_fps = 12
target_utilization = 0.80
merge_regions = true
cache_enabled = true
reorder_encodings = true
adapt_tight_compression = true
[limits]
encoding_threads = 0
max_set_encodings = 1024
max_client_cut_text = 16777216
network_profile_override = "auto" allows the server to classify the connection instead of forcing LAN tuning for every client.
The complete option reference is available in the configuration documentation.
flowchart LR
C1[UltraVNC / RFB client]
C2[noVNC browser]
TCP[TCP transport]
WS[WebSocket transport]
AUTH[RFB handshake + security]
SESSION[Client session]
PIPE[Framebuffer pipeline]
ENC[Encoding manager]
CAP[Shared CaptureProducer]
BACKENDS[DXCam / MSS / Pillow]
INPUT[Keyboard / pointer / clipboard]
C1 --> TCP --> AUTH
C2 --> WS --> AUTH
AUTH --> SESSION
SESSION --> PIPE --> ENC
CAP --> PIPE
BACKENDS --> CAP
SESSION --> INPUT
src/pyvncserver/
├── app/
│ └── server.py listener, admission, auth, lifecycle
├── session/
│ ├── loop.py per-client RFB message loop
│ └── runtime.py framebuffer / encoding session helpers
├── platform/
│ └── producer.py shared framebuffer producer
├── runtime/
│ ├── adaptive.py per-client congestion control + encoded-region cache
│ ├── security.py per-IP limits and auth throttling
│ ├── connection_limiter.py
│ └── connection_registry.py
├── rfb/ structured RFB subpackage
├── observability/ metrics / logging / profiling facades
├── plugins/ capture / encoding / security plugin contracts
├── capture.py stable capture facade
├── encodings.py stable encoding facade
├── protocol.py stable protocol facade
├── security.py stable security facade
├── errors.py stable exception facade
├── config.py validated TOML configuration
├── cli.py command-line entry point
└── _core/ private implementation detail
Version 4.0 removes the old top-level vnc_lib package. Stable embedding imports are now grouped under:
from pyvncserver.capture import ScreenCapture
from pyvncserver.encodings import EncoderManager
from pyvncserver.protocol import RFBProtocol
from pyvncserver.security import VNCAuth, VeNCryptServer
from pyvncserver.errors import ProtocolError
from pyvncserver import PluginManager, VNCServerV3
Validate the architectural boundary with:
python scripts/check_architecture.py
PyVNCServer 3.7 adds deployment-oriented commands that do not require starting the RFB listener:
pyvncserver info
pyvncserver doctor --config config/pyvncserver.toml
pyvncserver config init pyvncserver.toml
pyvncserver config validate pyvncserver.toml
pyvncserver benchmark --config pyvncserver.toml
pyvncserver release check --root .
[observability]
prometheus_enabled = true
prometheus_host = "127.0.0.1"
prometheus_port = 9100
This exposes /metrics, /healthz, /readyz and /status. The observability listener is disabled by default.
[clipboard]
enabled = true
direction = "both"
max_bytes = 1048576
encoding = "latin-1"
utf-8 can be enabled explicitly when both endpoints agree on it; Latin-1 remains the compatibility default.
The server contains several latency and throughput optimizations:
Install development dependencies and run the complete suite:
python -m pip install -e ".[dev]"
python -m pytest -q
Current 4.0.0 verification result:
417 passed, 13 skipped
The skipped cases in the recorded verification environment are legacy/auth compatibility cases and do not affect Tight/RRE tests.
The suite includes coverage for:
python -m compileall -q src tests
Run the full suite:
python -m pip install -e ".[dev]"
python -m pytest -q
python -m compileall -q src tests
The v4.0 package retains the adaptive/TLS/RFB/operations coverage from 3.7 and adds architecture-boundary checks, public facade imports and real capture/encoding/security plugin integration tests.
The repository currently contains these workflow definitions:
| Workflow | Purpose |
|---|---|
CI |
Tests Python 3.11–3.13, includes a Windows test job, checks bytecode compilation, coverage and package builds |
Documentation |
Strictly builds MkDocs documentation on PRs and deploys GitHub Pages from main |
python -m venv .venv
Activate it:
Windows PowerShell
.\.venv\Scripts\Activate.ps1
Linux / macOS
source .venv/bin/activate
Install the project:
python -m pip install -U pip
python -m pip install -e ".[dev,performance]"
Run verification before committing:
python -m compileall -q src tests
python -m pytest -q
Build distributions:
python -m pip install build twine
python -m build
python -m twine check dist/*
allow_client_resize does not change the host display mode; v3.4 only accepts layout-only changes matching the current framebuffer size.SyncNext is parsed but not implemented as a deferred barrier.pyvncserver._core namespace is not a public API; use the documented 4.x facades instead.