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:
CircuitPython firmware: https://circuitpython.org/downloads
zlib,displayioandbitmaptoolsbuilt-in modules
- 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()andpayload_sizes(); call it yourself to fail early on a damaged file. RaisesRV565Errorif the container is invalid.
- property duration
Length in seconds.
- frame(i)
Frame
ias big-endian RGB565 bytes (2 * width * height).Raises
RV565Errorif 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
RV565Erroron 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
displayioentirely: it sets the address window and sends the inflated bytes with onebus.send()per frame. It is much faster thanPlayerbut bypassesdisplayio, so the display must be initialised first (for example byadafruit_st7789.ST7789) and then left alone: this class setsroot_group = Noneand turns offauto_refresh.displayiorotation 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 MADCTLmadctl(defaultMADCTL_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 torestore_madctlsodisplayiocan draw again. A portrait panel sends no MADCTL unlessmadctlis given.- Parameters:
clip – a
Clipbus – the
fourwire.FourWire(CircuitPython 9+) ordisplayio.FourWirebus the display was created onpanel_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;
NonepicksMADCTL_LANDSCAPEfor a landscape panel and nothing for a portrait one. Use0xA0to turn a landscape picture upside down.restore_madctl – MADCTL byte to send after playing, or
Noneto leave it; defaultMADCTL_ADAFRUIT_ST7789
- begin()
Take the panel from
displayioand 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.
- 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.Bitmapwith 65536 values and drawn through aColorConverterwhose input colorspace isRGB565_SWAPPED. RV565 stores pixels big-endian; copying those bytes into the bitmap’s little-endian 16-bit storage gives byte-swapped values, which is whatRGB565_SWAPPEDdescribes, so no per-pixel conversion happens in Python.- Parameters:
clip – a
Clipdisplay – a display object (
board.DISPLAY,busdisplay.BusDisplayoradafruit_st7789.ST7789), orNoneto only buildgroupscale (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_refreshis 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, orbytes/bytearraydecompress – 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 awithblock) 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)