pdum.rfb in Jupyter — renderer and viewer in one kernel¶
The smallest possible remote-framebuffer demo: one kernel hosts both the Python
renderer and the browser viewer. You draw plain NumPy frames and push them into a
Display; an anywidget connects over a WebSocket and shows
them live. No second process, no Node, no GPU — just CPU encoding and NumPy.
Two cells is all it takes: one starts the server and a render loop, the next drops in
the viewer. Install with uv add 'habemus-papadum-rfb[anywidget]'.
import itertools
import numpy as np
import pdum.rfb as rfb
from pdum.rfb.notebook import publish_loop
# Top-level await works in Jupyter — the kernel already runs an event loop.
# Plain CPU / image path: no GPU, no H.264 tuning, no frame-rate knobs. Just NumPy.
# stats_interval=1.0 → the server pushes per-second metrics the viewer folds into its Stats
# (fps / rtt / bitrate), so the batteries viewer's overlay shows live server-truth numbers.
display = await rfb.serve(320, 240, port=0, stats_interval=1.0)
counter = itertools.count()
def render():
"""One frame, drawn from scratch each call — a sliding orange block on a green pulse."""
t = next(counter)
frame = np.zeros((240, 320, 3), dtype=np.uint8)
frame[:, :, 1] = t % 256 # background: pulsing green
x = (t * 4) % 260 # block slides left -> right
frame[90:150, x : x + 60] = (240, 120, 40) # a moving orange block
return frame
# Push ~10 frames/sec on a background task; the cell returns immediately.
task = publish_loop(display, render, fps=10)
display.ws_url
Connect the viewer in another cell — same kernel, same process. Frames flow over the WebSocket, never through the notebook protocol:
display.widget()
Network caveat — where the frames actually travel¶
The viewer connects to a WebSocket that rfb.serve() opens on the machine running this
kernel — the pixels flow over that socket, not over the Jupyter comm channel. So your
browser has to be able to reach that host and port:
- Local notebook (browser and kernel on the same machine): it just works over
localhost. - Hosted / deployed (JupyterHub, Colab, Binder, a remote box): the raw WebSocket port
usually isn't directly reachable from your browser. You'd mount the ASGI hub
same-origin and pass
base_path=(see the notebook guide), or set up SSH / port forwarding. - Headless execution (e.g.
scripts/test_notebooks.sh): no browser connects at all — the cells simply wire everything up, and no live view appears. That's expected.
Stop the loop and close the server when you're done:
task.cancel()
await display.aclose()