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 nozlib), with a 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 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, everyK-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_bytesbytes and returns it fromframe(); 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()andpayload_sizes(); call it yourself to fail early on a damaged file. RaisesRV565Errorif the container is invalid.
- content_index(i)
The frame whose picture frame
ishows:iitself unless it is a repeat, else the latest earlier frame that is not one.
- property duration
Length in seconds.
- frame(i)
Frame
ias 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 withbytes()to keep it. Decoding the next frame applies one record; any other frame starts at the latest keyframe at or before it (at mostK - 1records back).Raises
RV565Errorif a zlib body does not inflate to exactlyframe_bytesbytes.
- 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
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, 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
displayioentirely: it sets the address window and sends the decoded bytes with onebus.send()per frame. It plays the pixel formats inDIRECT_FORMATS:rgb565beas the panel is set up byadafruit_st7789, andrgb666by switching the controller to 18-bit pixels (COLMOD0x66) while playing and back torestore_colmodafterwards. Repeat frames are not decoded or sent duringplay(); the panel keeps showing the picture. 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_ST7789restore_colmod – COLMOD byte to send after playing an
rgb666clip; defaultCOLMOD_RGB565,adafruit_st7789’s value
- begin()
Take the panel from
displayioand set MADCTL (and COLMOD forrgb666) for streaming.
- 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 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 raiseRV565Error. Each frame is copied into adisplayio.Bitmapwith onebitmaptools.readinto()call, with no per-pixel work in Python:rgb565be: 65536-value bitmap through aColorConverterwhose input colorspace isRGB565_SWAPPED. The file’s big-endian bytes, read into the bitmap’s little-endian 16-bit storage, are byte-swapped values, which is whatRGB565_SWAPPEDdescribes.rgb565le: the same bitmap throughColorspace.RGB565.gray8,gray4,mono: a 256-, 16- or 2-value bitmap through a greydisplayio.Palette(mono: 0 black, 1 white).
Repeat frames are not decoded or drawn during
play().- 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 (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_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:
(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_FORMATSor 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, 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, 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 intoPIXEL_FORMATS,CODECSandkeyframe_interval1 for version 6