API Reference

circuitpython_rv565

Play RV565 video files on CircuitPython displays.

RV565 (https://github.com/elixpo/rv565, arXiv:2610.35935) stores video as frames whose decoded bytes are already in the display’s native pixel format. This module reads format version 7 (nine pixel formats, stored or zlib frames, keyframes, XOR deltas and repeats) and version 6 (big-endian RGB565, zlib, every frame a keyframe), and shows clips with displayio (Player) or straight over an ST7789’s bus (DirectPlayer).

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; clips with the stored codec need no zlib), with a 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 and checked on open; the frame index is built on first use by one pass over the record headers (4 bytes of RAM per frame, plus 1 per frame for the frame types in version 7), which also validates the rest of the container: every length is nonzero and inside the file, no bytes follow the last record, frame types are known, every K-th frame is a keyframe, repeats have no body and stored bodies are exactly one frame. Whether a zlib body inflates to one frame is checked when it is decoded.

Decoding keeps one frame buffer of frame_bytes bytes and returns it from frame(); it is overwritten by the next call.

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 record headers once and validate the container.

Called automatically by frame(), payload(), frame_type() 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.

content_index(i)

The frame whose picture frame i shows: i itself unless it is a repeat, else the latest earlier frame that is not one.

property duration

Length in seconds.

frame(i)

Frame i as bytes in the clip’s pixel format (frame_bytes).

The result is the clip’s own frame buffer: the next call to frame() overwrites it, so copy it with bytes() to keep it. Decoding the next frame applies one record; any other frame starts at the latest keyframe at or before it (at most K - 1 records back).

Raises RV565Error if a zlib body does not inflate to exactly frame_bytes bytes.

frame_type(i)

"key", "delta" or "repeat".

property indexed

True once the frame index has been built.

payload(i)

The encoded body of frame i (bytes or a memoryview; empty for a repeat).

payload_sizes()

Body length of every frame (0 for repeats), in order.

verify()

Decode 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, restore_colmod=COLMOD_RGB565)

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 decoded bytes with one bus.send() per frame. It plays the pixel formats in DIRECT_FORMATS: rgb565be as the panel is set up by adafruit_st7789, and rgb666 by switching the controller to 18-bit pixels (COLMOD 0x66) while playing and back to restore_colmod afterwards. Repeat frames are not decoded or sent during play(); the panel keeps showing the picture. 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

  • restore_colmod – COLMOD byte to send after playing an rgb666 clip; default COLMOD_RGB565, adafruit_st7789’s value

begin()

Take the panel from displayio and set MADCTL (and COLMOD for rgb666) for streaming.

end()

Put MADCTL and COLMOD back (where begin() changed them).

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 shown on time (repeats included)

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

  • repeated – frames shown at no cost because the screen already held their picture (repeat frames)

  • 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.

Shows the pixel formats in PLAYER_FORMATS; other formats raise RV565Error. Each frame is copied into a displayio.Bitmap with one bitmaptools.readinto() call, with no per-pixel work in Python:

  • rgb565be: 65536-value bitmap through a ColorConverter whose input colorspace is RGB565_SWAPPED. The file’s big-endian bytes, read into the bitmap’s little-endian 16-bit storage, are byte-swapped values, which is what RGB565_SWAPPED describes.

  • rgb565le: the same bitmap through Colorspace.RGB565.

  • gray8, gray4, mono: a 256-, 16- or 2-value bitmap through a grey displayio.Palette (mono: 0 black, 1 white).

Repeat frames are not decoded or drawn during play().

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 (bytes in the clip’s format) 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:

(decode_seconds, draw_seconds)

exception circuitpython_rv565.RV565Error

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

circuitpython_rv565.frame_size(pixel_format, width, height)

Decoded size in bytes of one frame.

Parameters:

pixel_format – a name from PIXEL_FORMATS or its index

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, size=None)

Parse and range-check a version 6 or 7 header.

Parameters:
  • data – the first 16 bytes of the file (12 are enough for version 6)

  • size – the file’s length, to check the header size against

Returns:

(width, height, fps, flags, frames, version, header_size, pixel_format, codec, keyframe_interval), the last three as indexes into PIXEL_FORMATS, CODECS and keyframe_interval 1 for version 6