Remote Frame Buffer
Project description
pdum.rfb — Remote Frame Buffer
Render a framebuffer in Python, view and interact with it in the browser.
pdum.rfb streams a server-rendered framebuffer to a browser over a WebSocket and
sends pointer/keyboard/resize events back. It targets scientific and interactive
visualization across the whole cadence range — from sparse, on-demand scenes
(render only when state changes) to high-frame-rate interactive streaming
(low-latency H.264/WebCodecs). You own the loop and pick the cadence; the library
never imposes a fixed game-engine tick. It is not a generic VNC clone.
Coming from
jupyter_rfb? Same idea — render in Python, view in the browser, events flow back, same renderview event vocabulary — so it slots underrendercanvas/pygfx/fastplotlib. What's different:
- Not tied to Jupyter. Frames travel over a plain WebSocket, not ipywidgets/kernel comms — the same server drives a standalone web page, a desktop webview, or a headless box, no notebook required.
- High frame rates. Alongside the per-frame image path (every frame a keyframe, à la
jupyter_rfb), a low-latency H.264/WebCodecs path with per-client backpressure and keyframe policy streams continuous, interactive framerates — not just occasional redraws.- Zero-copy on the GPU. When you render on CUDA, frames can go straight to NVENC (CUDA NV12 → H.264) with no host round-trip — see GPU zero-copy.
The repo ships two halves:
- a Python server — this package,
habemus-papadum-rfb(import pdum.rfb), Python 3.14+, UV-managed; - a browser client —
@habemus-papadum/rfb-widgets, a TypeScript package whose decoding runs entirely in a Web Worker (it owns the WebSocket, the decoder, and a transferredOffscreenCanvas).
A sibling native package, habemus-papadum-nvenc (import pdum.nvenc, under
packages/nvenc/), provides an optional PyAV-free GPU H.264
encoder.
📖 Full documentation: https://habemus-papadum.github.io/pdum_rfb/
How it works
The public API is push: you own your loop and publish frames into a shared
Display; the library fans each frame out to every connected viewer and lets you
drain input from all of them in one place.
import asyncio
import pdum.rfb as rfb
async def main():
display = await rfb.serve(1280, 720, port=8765) # WS server starts in the background
state = initial_state()
try:
while running(state):
for ev in display.poll_events(): # input from every viewer
state = update(state, ev)
display.publish(render(state)) # sync, latest-wins, fans out to all viewers
await asyncio.sleep(1 / 30) # or on-demand — you own the cadence
finally:
await display.aclose()
asyncio.run(main())
import { RemoteFramebufferView } from "@habemus-papadum/rfb-widgets";
const view = new RemoteFramebufferView(document.getElementById("stage")!, {
url: "ws://localhost:8765",
});
// later: view.dispose();
Each connecting browser negotiates the best shared transport: an image path (JPEG/PNG/WebP, every frame a keyframe; dependency-light) or an H.264 path (Annex B for the browser's WebCodecs decoder). For GPU-rendered scenes, three hardware NVENC routes are available — see the Installation guide.
Installation
pip install habemus-papadum-rfb # image path (numpy, pillow, websockets)
pip install 'habemus-papadum-rfb[h264]' # + CPU/software H.264 (PyAV/libx264)
pip install 'habemus-papadum-rfb[gpu-nvenc-sdk]' # + GPU H.264 (NVIDIA, Linux) — fastest
pip install 'habemus-papadum-rfb[anywidget]' # + Jupyter/marimo notebook widget
import pdum.rfb works without any extra. Not sure what your machine supports?
pip install 'habemus-papadum-rfb[cli]' then pdum-rfb doctor. The full matrix
(CPU vs the three GPU routes, platform limits) is in the
Installation guide.
Developing
The repo is a uv workspace (root habemus-papadum-rfb + packages/*) with the
browser client as a self-contained pnpm project under widgets/. The layout,
the uv/pnpm conventions, and the CI are documented in
Repository & Development.
Prerequisites
Install these yourself first — setup.sh detects them and tells you how to
install any that are missing, but it never installs them for you:
- uv —
curl -LsSf https://astral.sh/uv/install.sh | sh - Node.js 20+ and pnpm (for the browser client / e2e). The repo pins the tested
LTS in
.nvmrc(Node 22, which CI uses) —nvm use/fnm usepicks it up.corepack enable(ships with Node) ornpm i -g pnpmprovides pnpm. Optional if you only work on the Python side.setup.shrefuses to set up the browser client on Node < 20.
Bootstrap (all platforms)
git clone https://github.com/habemus-papadum/pdum_rfb.git
cd pdum_rfb
./scripts/setup.sh # idempotent — rerun after pulling dependency changes
One command sets up everything, the same way on macOS / Linux / Linux+GPU:
- Python —
uv sync --frozen(the committeduv.lockis authoritative). On a Linux box with an NVIDIA GPU and a CUDA toolkit it auto-adds the native NVENC SDK encoder — see Per-platform notes for theRFB_GPUknob. - Browser client —
pnpm install --frozen-lockfileplus the Playwright Chromium download used by the e2e suite (skipped, with a hint, if Node/pnpm are absent). - pre-commit hooks.
Per-platform notes
-
Linux without a GPU (and CI's default) — the bootstrap above is everything. The
devgroup already includes PyAV, so the image and CPU-H.264 paths and all their tests work. GPU tests detect no device and skip. -
Linux with an NVIDIA GPU —
setup.shdetects the GPU + CUDA toolkit and builds the PyAV-free NVENC SDK encoder (pdum.nvenc) as an editable install automatically. Override with theRFB_GPUenv var:RFB_GPU=auto ./scripts/setup.sh # default: build it iff Linux + GPU + CUDA toolkit present RFB_GPU=force ./scripts/setup.sh # build even if the CUDA major ≠ 13 (then swap CuPy yourself) RFB_GPU=0 ./scripts/setup.sh # CPU paths only
The
gpu-nvenc-sdkextra pinscupy-cuda13x; on a CUDA-12 toolkit useRFB_GPU=forceand swap tocupy-cuda12x. To confirm what lit up:uv run --group gpu-dev pdum-rfb doctor # which encode paths are available
For the PyAV-18 zero-copy route specifically,
./scripts/install-gpu.shbuilds a CUDA-enabled PyAV. See the GPU zero-copy guide. -
macOS — the image and CPU-H.264 paths work (PyAV publishes arm64 wheels). The NVENC/GPU paths are NVIDIA/Linux-only and are simply unavailable; everything else, including the full headless test suite for the CPU paths, runs normally.
Common commands
uv run pytest # Python tests
uv run ruff check . && uv run ruff format .
uv run python -m pdum.rfb.server --pattern bouncing_box --port 8765 # demo server
uv run mkdocs serve # docs at http://localhost:8000
pnpm -C widgets typecheck # browser client: types
pnpm -C widgets test # Vitest unit tests
pnpm -C widgets e2e # Playwright e2e (boots the Python server)
pnpm -C widgets dev # demo at http://localhost:5173
Releasing
Maintainers only. Releasing publishes to PyPI/npm, pushes tags, and creates public GitHub releases. Version numbers are human-managed — don't hand-edit them.
./scripts/release.sh <patch|minor|major> bumps the version across all four
version files in lockstep (pyproject.toml, src/pdum/rfb/__init__.py,
widgets/package.json, packages/nvenc/pyproject.toml), then — via an interactive
step selector — commits, tags, pushes, and publishes all three packages:
- PyPI —
scripts/publish.shpublishes bothhabemus-papadum-rfb(hatch) and the nativehabemus-papadum-nvencwheels. - npm —
@habemus-papadum/rfb-widgets. - GitHub Release — which triggers the docs site to redeploy.
Publishing is never done from CI. It's non-interactive when a git-ignored
.env at the repo root supplies credentials (loaded by the release/publish
scripts; pre-set env vars win):
# .env (git-ignored — never commit)
HATCH_INDEX_USER=__token__
HATCH_INDEX_AUTH=pypi-… # PyPI token (hatch does NOT read ~/.pypirc)
NPM_TOKEN=npm_… # npm *Automation* token (bypasses 2FA)
release.sh materializes NPM_TOKEN into a transient, outside-the-repo .npmrc
just for the pnpm publish call — there is no committed or persistent .npmrc, so
dev checkouts stay auth-config-free. Full mechanics and the CI overview are in
Repository & Development.
License
MIT License — see LICENSE for details.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file habemus_papadum_rfb-0.1.0.tar.gz.
File metadata
- Download URL: habemus_papadum_rfb-0.1.0.tar.gz
- Upload date:
- Size: 763.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: Hatch/1.17.0 {"ci":null,"cpu":"arm64","distro":{"name":"macOS","version":"26.5.1"},"implementation":{"name":"CPython","version":"3.14.0"},"installer":{"name":"hatch","version":"1.17.0"},"openssl_version":"OpenSSL 3.6.2 7 Apr 2026","python":"3.14.0","system":{"name":"Darwin","release":"25.5.0"}} HTTPX2/2.5.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dfd7a6113f7de820b1183ce88211f14bc439773b6146889e0e383f2b4b4c0eab
|
|
| MD5 |
cc620d2192a952c28014e100c3433c56
|
|
| BLAKE2b-256 |
07e6ba347375f5252f3f1b7ded5d03d28fbaee21a4f3d9d24c6aec89ff159287
|
File details
Details for the file habemus_papadum_rfb-0.1.0-py3-none-any.whl.
File metadata
- Download URL: habemus_papadum_rfb-0.1.0-py3-none-any.whl
- Upload date:
- Size: 201.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: Hatch/1.17.0 {"ci":null,"cpu":"arm64","distro":{"name":"macOS","version":"26.5.1"},"implementation":{"name":"CPython","version":"3.14.0"},"installer":{"name":"hatch","version":"1.17.0"},"openssl_version":"OpenSSL 3.6.2 7 Apr 2026","python":"3.14.0","system":{"name":"Darwin","release":"25.5.0"}} HTTPX2/2.5.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
813ae70e8901499c575628e43d946e4fc0b17fef3e18cd0ff6b016d2e2491256
|
|
| MD5 |
6fa5d8f09fcb63cfe063ee2704bc7b82
|
|
| BLAKE2b-256 |
03f97e4f9e543eaa06f816cad7d60679e5f40bd40d60b5900c4c8799c601f678
|