API Reference

circuitpython_rv565

Play RV565 video files on CircuitPython displays.

RV565 (https://github.com/elixpo/rv565, arXiv:2610.35935) stores video as independently zlib-compressed frames whose decoded bytes are big-endian RGB565, the native pixel format of ST7789-class SPI panels. This module reads version 6 files and shows them with displayio.

This is the easy route, not the fast one: the C library plays 24 FPS with dual-core decoding, while this pure-Python reader inflates with CircuitPython’s zlib and draws through displayio, so expect a few frames per second. Frames that fall behind are skipped, so a clip still plays at its own speed.

  • Author: Ayushman Bhattacharya

Implementation Notes

Hardware:

  • Any CircuitPython board whose build includes zlib (most ESP32-S3 and RP2350 boards), with an RGB565 display such as an ST7789.

Software and Dependencies:

class circuitpython_rv565.Clip(source, decompress=None)

An RV565 file: header, a lazily built frame index, and frame decoding.

Use open() rather than constructing this directly. Only the header is read on open; the frame index is built on first use by one pass over the length fields (4 bytes of RAM per frame), which also validates the whole container: every length is nonzero and inside the file, and no bytes follow the last frame.

Parameters:
  • source – an internal byte source (see open())

  • decompress – function inflating a zlib stream to bytes; defaults to the built-in zlib.decompress

build_index()

Scan the length fields once and validate the container.

Called automatically by frame(), payload() and payload_sizes(); call it yourself to fail early on a damaged file. Raises RV565Error if the container is invalid.

close()

Close the underlying file, if open() opened it.

property duration

Length in seconds.

frame(i)

Frame i as big-endian RGB565 bytes (2 * width * height).

Raises RV565Error if the payload does not inflate to exactly that many bytes.

property indexed

True once the frame index has been built.

payload(i)

The zlib payload of frame i (bytes or a memoryview).

payload_sizes()

Compressed length of every frame, in order.

verify()

Inflate every frame; raise RV565Error on the first bad one.

Returns:

the number of frames checked

class circuitpython_rv565.DirectPlayer(clip, bus, panel_width, panel_height, x_offset=0, y_offset=0, display=None, clock=None, sleep=None, madctl=None, restore_madctl=MADCTL_ADAFRUIT_ST7789)

Experimental: send frames straight to an ST7789 over its display bus.

RV565 frames are already in the byte order an ST7789 expects on SPI, so this skips displayio entirely: it sets the address window and sends the inflated bytes with one bus.send() per frame. It is much faster than Player but bypasses displayio, so the display must be initialised first (for example by adafruit_st7789.ST7789) and then left alone: this class sets root_group = None and turns off auto_refresh.

displayio rotation is a software transform: it never reaches the controller, whose RAM stays 240 columns x 320 rows. So for a landscape panel (panel_width > panel_height) play() first sends MADCTL madctl (default MADCTL_LANDSCAPE, 0x60, as the RV565 benchmarks do), which makes the controller’s columns run along the long side; the address window is then in landscape coordinates. When play ends, MADCTL is set back to restore_madctl so displayio can draw again. A portrait panel sends no MADCTL unless madctl is given.

Parameters:
  • clip – a Clip

  • bus – the fourwire.FourWire (CircuitPython 9+) or displayio.FourWire bus the display was created on

  • panel_width (int) – width of the panel as the clip should appear

  • panel_height (int) – height of the panel as the clip should appear

  • x_offset (int) – controller column offset for this module and MADCTL (0 for 320x240 panels in the benchmarks; 40 or 52/53 for 240x135 ones, depending on orientation)

  • y_offset (int) – controller row offset, likewise

  • display – the display object, so it can be quieted while playing

  • madctl – MADCTL byte to send before playing; None picks MADCTL_LANDSCAPE for a landscape panel and nothing for a portrait one. Use 0xA0 to turn a landscape picture upside down.

  • restore_madctl – MADCTL byte to send after playing, or None to leave it; default MADCTL_ADAFRUIT_ST7789

begin()

Take the panel from displayio and set MADCTL for streaming.

end()

Put MADCTL back to restore_madctl (if MADCTL was changed).

play(loops=1, start=0, on_frame=None)

Play at the clip’s rate, skipping late frames.

Sends MADCTL before the first frame and restores it afterwards, also when playing stops with an exception.

show(i)

Decode frame i and send it to the panel.

Called on its own, outside play(), it does not send MADCTL; call begin() first.

class circuitpython_rv565.PlayStats(fps)

What Player.play() achieved.

Variables:
  • shown – frames decoded and drawn

  • skipped – frames dropped to keep up with the clip’s rate

  • elapsed – wall time in seconds

  • decode_time – seconds spent inflating

  • draw_time – seconds spent filling the bitmap and refreshing

property fps

Frames actually shown per second.

class circuitpython_rv565.Player(clip, display=None, scale=1, x=None, y=None, clock=None, sleep=None)

Show an RV565 clip through displayio.

Frames are inflated into a displayio.Bitmap with 65536 values and drawn through a ColorConverter whose input colorspace is RGB565_SWAPPED. RV565 stores pixels big-endian; copying those bytes into the bitmap’s little-endian 16-bit storage gives byte-swapped values, which is what RGB565_SWAPPED describes, so no per-pixel conversion happens in Python.

Parameters:
  • clip – a Clip

  • display – a display object (board.DISPLAY, busdisplay.BusDisplay or adafruit_st7789.ST7789), or None to only build group

  • scale (int) – integer zoom for the group (1 = native size)

  • x – left edge on the display; centred when omitted

  • y – top edge on the display; centred when omitted

  • clock – function returning seconds (for tests)

  • sleep – function sleeping for seconds (for tests)

blit(frame)

Copy one decoded frame (big-endian RGB565 bytes) into bitmap.

play(loops=1, start=0, on_frame=None)

Play at the clip’s rate, skipping late frames; see PlayStats.

auto_refresh is turned off while playing so each frame is pushed once, and restored afterwards.

show(i)

Decode frame i, draw it, and refresh the display.

Returns:

(inflate_seconds, draw_seconds)

exception circuitpython_rv565.RV565Error

The data is not a valid RV565 file, or a frame does not decode.

circuitpython_rv565.open(source, decompress=None)

Open an RV565 clip.

Parameters:
  • source – a path such as "/clip.rv565" or "/sd/clip.rv565", a file object opened in binary mode, or bytes/bytearray

  • decompress – optional replacement for zlib.decompress

Returns:

a Clip

A path is opened and read frame by frame, so the clip does not have to fit in RAM; Clip.close() (or a with block) closes it again.

circuitpython_rv565.parse_header(data)

Parse and range-check the 12-byte header.

Parameters:

data – at least the first 12 bytes of the file

Returns:

(width, height, fps, flags, frames)