Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
6e91542
videocore: size the rendered rect when play starts
mikeysklar Oct 31, 2026
30acdaf
videocore: draw a moved plain buffer at its new position
mikeysklar Oct 31, 2026
82b66a2
videocore: let a source report which part of a frame changed
mikeysklar Oct 31, 2026
4ec36a1
videocore: check a strided frame fits in the framebuffer
mikeysklar Oct 31, 2026
76ff34a
gifio: factor out GIF file opening
mikeysklar Oct 29, 2026
bdb44e9
videocore: let a source report end of stream
mikeysklar Oct 31, 2026
91d7471
videogif: add a GIF video source
mikeysklar Oct 31, 2026
a4d411c
Merge branch 'main' into video-gif
mikeysklar Nov 3, 2026
cb9dd9d
Merge adafruit/main into video-gif
mikeysklar Nov 3, 2026
ddcf2b1
Update circuitpython.pot after merging main
mikeysklar Nov 3, 2026
351b7ea
Merge branch 'video-gif' of the fork into video-gif
mikeysklar Nov 3, 2026
be3b506
videogif: take only a filename and leave gifio unchanged
mikeysklar Nov 4, 2026
8b663e8
videocore: rename on_eos to on_stream_end
mikeysklar Nov 4, 2026
c5c9284
videocore: report changed areas as displayio areas
mikeysklar Nov 4, 2026
63e26e8
videogif: stop instead of replaying when moved or shown again
mikeysklar Nov 4, 2026
2cc6ae2
videogif: take the byte order from the display, not an argument
mikeysklar Nov 4, 2026
b2d84b9
Merge remote-tracking branch 'adafruit/main' into video-gif
mikeysklar Nov 4, 2026
2262545
videogif: catch up on late frames; say where play() starts
mikeysklar Nov 4, 2026
ec4d5a9
Merge remote-tracking branch 'adafruit/main' into video-gif
mikeysklar Nov 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion locale/circuitpython.pot
Original file line number Diff line number Diff line change
Expand Up @@ -1553,7 +1553,7 @@ msgstr ""

#: ports/espressif/common-hal/audioio/AudioOut.c
#: ports/espressif/common-hal/mipicsi/Camera.c
#: shared-module/videocore/VideoBuffer.c
#: shared-module/videocore/VideoBuffer.c shared-module/videogif/GifDecoder.c
msgid "already playing"
msgstr ""

Expand Down Expand Up @@ -2555,6 +2555,7 @@ msgstr ""

#: py/argcheck.c shared-bindings/gifio/GifWriter.c
#: shared-module/_airlift/__init__.c shared-module/gifio/OnDiskGif.c
#: shared-module/videogif/GifDecoder.c
msgid "%q must be <= %d"
msgstr ""

Expand Down
9 changes: 9 additions & 0 deletions main.c
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@
#include "shared-module/usb_host_audio/USBIn.h"
#endif

#if CIRCUITPY_VIDEOGIF
#include "shared-module/videogif/GifDecoder.h"
#endif

#if CIRCUITPY_MEMORYMONITOR
#include "shared-module/memorymonitor/__init__.h"
#endif
Expand Down Expand Up @@ -392,6 +396,11 @@ static void cleanup_after_vm(mp_obj_t exception) {
picogame_reset();
#endif

// Stop GIF playback before the displays and their framebuffers go away.
#if CIRCUITPY_VIDEOGIF
videogif_reset();
#endif

// Turn off the display and flush the filesystem before the heap disappears.
#if CIRCUITPY_DISPLAYIO
reset_displays();
Expand Down
4 changes: 2 additions & 2 deletions ports/espressif/common-hal/mipicsi/Camera.c
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ static IRAM_ATTR bool mipicsi_on_trans_finished(
// Report where the frame actually landed: after a destination change
// (or a pause) the frame that was in flight lands at the previous one.
if (self->dest_base != NULL && self->dest_on_frame != NULL) {
self->dest_on_frame(self->dest_ctx, trans->buffer);
self->dest_on_frame(self->dest_ctx, trans->buffer, NULL);
}
return false;
}
Expand Down Expand Up @@ -406,7 +406,7 @@ bool common_hal_mipicsi_camera_set_destination(
mipicsi_camera_obj_t *self,
uint8_t *base,
size_t stride,
void (*on_frame)(void *ctx, const void *frame),
videocore_on_frame_fun on_frame,
void *ctx) {
const size_t row_bytes = ((size_t)self->width * mipicsi_bits_per_pixel_for(self->output_format) + 7) / 8;
const size_t frame_bytes = (size_t)self->height * row_bytes;
Expand Down
7 changes: 4 additions & 3 deletions ports/espressif/common-hal/mipicsi/Camera.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#endif

#include "shared-bindings/mipicsi/__init__.h"
#include "shared-module/videocore/__init__.h"

typedef struct mipicsi_camera_obj mipicsi_camera_obj_t;

Expand Down Expand Up @@ -56,10 +57,10 @@ struct mipicsi_camera_obj {
// Streaming destination, set by videocore.VideoBuffer. The CSI controller
// only runs while dest_base is set. Frames are DMA'd contiguously into
// dest_base, which must be cache-line aligned. dest_on_frame(dest_ctx,
// frame) is called from the ISR with where each frame landed; a frame in
// flight when the destination changes lands at the previous one.
// frame, NULL) is called from the ISR with where each frame landed; a
// frame in flight when the destination changes lands at the previous one.
volatile void *dest_base;
void (*dest_on_frame)(void *ctx, const void *frame);
videocore_on_frame_fun dest_on_frame;
void *dest_ctx;
bool ctlr_started;
// True between pause and resume: only the CSI controller is stopped.
Expand Down
5 changes: 5 additions & 0 deletions py/circuitpy_defns.mk
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,9 @@ endif
ifeq ($(CIRCUITPY_VIDEOCORE),1)
SRC_PATTERNS += videocore/%
endif
ifeq ($(CIRCUITPY_VIDEOGIF),1)
SRC_PATTERNS += videogif/%
endif
ifeq ($(CIRCUITPY_WARNINGS),1)
SRC_PATTERNS += warnings/%
endif
Expand Down Expand Up @@ -719,6 +722,7 @@ $(filter $(SRC_PATTERNS), \
supervisor/RunReason.c \
supervisor/Runtime.c \
supervisor/StatusBar.c \
videogif/__init__.c \
wifi/AuthMode.c \
wifi/Packet.c \
wifi/PowerManagement.c \
Expand Down Expand Up @@ -926,6 +930,7 @@ SRC_SHARED_MODULE_ALL = \
vectorio/__init__.c \
videocore/VideoBuffer.c \
videocore/__init__.c \
videogif/GifDecoder.c \
warnings/__init__.c \
watchdog/__init__.c \
zlib/__init__.c \
Expand Down
4 changes: 4 additions & 0 deletions py/circuitpy_mpconfig.mk
Original file line number Diff line number Diff line change
Expand Up @@ -860,6 +860,10 @@ CIRCUITPY_ULAB_OPTIMIZE_SIZE ?= 0
CIRCUITPY_VIDEOCORE ?= 0
CFLAGS += -DCIRCUITPY_VIDEOCORE=$(CIRCUITPY_VIDEOCORE)

# Animated GIF files as a videocore video source. Shares gifio's decoder.
CIRCUITPY_VIDEOGIF ?= $(call enable-if-all,$(CIRCUITPY_VIDEOCORE) $(CIRCUITPY_GIFIO))
CFLAGS += -DCIRCUITPY_VIDEOGIF=$(CIRCUITPY_VIDEOGIF)

CIRCUITPY_WARNINGS ?= $(CIRCUITPY_FULL_BUILD)
CFLAGS += -DCIRCUITPY_WARNINGS=$(CIRCUITPY_WARNINGS)

Expand Down
2 changes: 1 addition & 1 deletion shared-bindings/mipicsi/Camera.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ bool common_hal_mipicsi_camera_set_destination(
mipicsi_camera_obj_t *self,
uint8_t *base,
size_t stride,
void (*on_frame)(void *ctx, const void *frame),
videocore_on_frame_fun on_frame,
void *ctx);

mipicsi_pixel_format_t common_hal_mipicsi_camera_get_pixel_format(mipicsi_camera_obj_t *self);
Expand Down
8 changes: 6 additions & 2 deletions shared-bindings/videocore/VideoBuffer.c
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ static mp_obj_t videocore_videobuffer_make_new(const mp_obj_type_t *type, size_t
//| whole frame.
//|
//| :param ~circuitpython_typing.VideoSource source: The video source to
//| play, such as `mipicsi.Camera`
//| play, such as `mipicsi.Camera` or `videogif.GifDecoder`
//| """
//|
static mp_obj_t videocore_videobuffer_play(mp_obj_t self_in, mp_obj_t source) {
Expand All @@ -103,7 +103,11 @@ static mp_obj_t videocore_videobuffer_stop(mp_obj_t self_in) {
static MP_DEFINE_CONST_FUN_OBJ_1(videocore_videobuffer_stop_obj, videocore_videobuffer_stop);

//| playing: bool
//| """True when a video source is being played, False otherwise. (read-only)"""
//| """True when a video source is being played, False otherwise. It also
//| becomes False when the source has no more frames, such as a GIF that
//| has finished its last loop. The last frame then stays shown until
//| ``stop()`` or the next ``play()``. A framebuffer target also drops it
//| when moved or hidden. (read-only)"""
//|
static mp_obj_t videocore_videobuffer_get_playing(mp_obj_t self_in) {
videocore_videobuffer_obj_t *self = MP_OBJ_TO_PTR(self_in);
Expand Down
7 changes: 4 additions & 3 deletions shared-bindings/videocore/__init__.c
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@
//| This module provides the video pipeline shared between video sources and
//| video outputs. It is the video equivalent of the `audiocore` module.
//|
//| Video sources, such as `mipicsi.Camera`, implement the internal videosource
//| protocol. A `VideoBuffer` tells a video source where to put its frames in a
//| framebuffer, such as `mipidsi.Display`, and can move it as needed.
//| A video source, such as `mipicsi.Camera` or `videogif.GifDecoder`, makes
//| video frames. A `VideoBuffer` plays a video source and tells it where to
//| put each frame: in a display's memory, such as `mipidsi.Display`, or in a
//| ``bytearray`` that a displayio Group shows.
//|
//| Stream a MIPI CSI camera live into the board's display (a MIPI DSI panel
//| on ESP32-P4-Function-EV, for example):
Expand Down
231 changes: 231 additions & 0 deletions shared-bindings/videogif/GifDecoder.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
// This file is part of the CircuitPython project: https://circuitpython.org
//
// SPDX-FileCopyrightText: Copyright (c) 2026 Mikey Sklar for Adafruit Industries
//
// SPDX-License-Identifier: MIT

#include "py/builtin.h"
#include "py/obj.h"
#include "py/objproperty.h"
#include "py/runtime.h"

#include "shared-bindings/util.h"
#include "shared-bindings/videogif/GifDecoder.h"
#include "shared/runtime/context_manager_helpers.h"

//| class GifDecoder:
//| """Decode an animated GIF file for video playback.
//|
//| Pass a GifDecoder to ``videocore.VideoBuffer.play()`` to show the GIF
//| on a display or to write its frames into a buffer.
//|
//| Each frame is shown for the time the GIF file gives it. A few cases
//| work the same way as in web browsers:
//|
//| - A frame time of 0 or 0.01 seconds is shown for 0.1 seconds.
//| - A GIF with no loop count plays once.
//| - A loop count of 0 repeats the GIF forever. Any other loop count is
//| the number of extra times to play it, so a loop count of 2 plays the
//| GIF three times.
//|
//| When the GIF finishes its last loop, its last frame stays on the screen
//| and `videocore.VideoBuffer.playing` becomes False. The same happens if
//| a frame or the file can't be read.
//|
//| Frames are decoded in the background, in short breaks between your
//| Python code. When those breaks come late, up to 3 late frames are
//| decoded in one break to catch up. If it is still behind after that,
//| the animation slows down so your Python code still gets time to run.
//|
//| The GIF starts again from its first frame, filled with its background
//| color, at each loop. `videocore.VideoBuffer.play()` always starts it
//| from its first frame. There is no pause and resume.
//|
//| When a VideoBuffer that draws straight into a framebuffer is hidden,
//| the GIF waits and its time does not advance. When the VideoBuffer is
//| shown again or moved, the GIF stops and `videocore.VideoBuffer.playing`
//| becomes False, since the screen under it may have been drawn over.
//| Call `videocore.VideoBuffer.play()` to start it again.
//|
//| When a frame asks for its area to be cleared after it is shown (GIF
//| disposal method 2), that area is filled with the background color.
//| GIFs that ask to restore the previous image (disposal method 3) may
//| show leftover pixels, because that is not supported.
//|
//| Play a GIF in a Group on a display:
//|
//| .. code-block:: python
//|
//| import board
//| import displayio
//| import videocore
//| import videogif
//|
//| gif = videogif.GifDecoder("/animation.gif")
//| video = videocore.VideoBuffer(bytearray(gif.width * gif.height * 2))
//| video.play(gif)
//|
//| root = displayio.Group()
//| root.append(video)
//| board.DISPLAY.root_group = root
//| while True:
//| pass
//| """
//|
//| def __init__(
//| self,
//| filename: str,
//| ) -> None:
//| """Open a GIF file so it can be played.
//|
//| :param str filename: The name of a GIF file, such as ``"/animation.gif"``.
//| The GifDecoder closes the file when it is deinitialized.
//|
//| Frames are RGB565, in the byte order of the display or framebuffer
//| that shows them. Raises `ValueError` if the file is not a GIF or if
//| the GIF is wider than 320 pixels.
//| """
//| ...
//|
static mp_obj_t videogif_gifdecoder_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) {
enum { ARG_filename, NUM_ARGS };
static const mp_arg_t allowed_args[] = {
{ MP_QSTR_filename, MP_ARG_REQUIRED | MP_ARG_OBJ },
};
MP_STATIC_ASSERT(MP_ARRAY_SIZE(allowed_args) == NUM_ARGS);
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);

mp_obj_t filename = mp_arg_validate_type_string(args[ARG_filename].u_obj, MP_QSTR_filename);
mp_obj_t file = mp_call_function_2(MP_OBJ_FROM_PTR(&mp_builtin_open_obj), filename, MP_ROM_QSTR(MP_QSTR_rb));

videogif_gifdecoder_obj_t *self = mp_obj_malloc_with_finaliser(videogif_gifdecoder_obj_t, &videogif_gifdecoder_type);
common_hal_videogif_gifdecoder_construct(self, file);
return MP_OBJ_FROM_PTR(self);
}

static videogif_gifdecoder_obj_t *native_gifdecoder(mp_obj_t self_in) {
return MP_OBJ_TO_PTR(mp_obj_cast_to_native_base(self_in, &videogif_gifdecoder_type));
}

static void check_for_deinit(videogif_gifdecoder_obj_t *self) {
if (common_hal_videogif_gifdecoder_deinited(self)) {
raise_deinited_error();
}
}

//| def deinit(self) -> None:
//| """Stop playing and free the memory used to decode the GIF. The
//| GifDecoder can't be used after this."""
//| ...
//|
static mp_obj_t videogif_gifdecoder_deinit(mp_obj_t self_in) {
common_hal_videogif_gifdecoder_deinit(native_gifdecoder(self_in));
return mp_const_none;
}
static MP_DEFINE_CONST_FUN_OBJ_1(videogif_gifdecoder_deinit_obj, videogif_gifdecoder_deinit);

//| def __enter__(self) -> GifDecoder:
//| """No-op used by Context Managers."""
//| ...
//|
//| def __exit__(self) -> None:
//| """Automatically deinitializes when exiting a context. See
//| :ref:`lifetime-and-contextmanagers` for more info."""
//| ...
//|

//| width: int
//| """Width of the GIF in pixels. (read-only)"""
//|
static mp_obj_t videogif_gifdecoder_get_width(mp_obj_t self_in) {
videogif_gifdecoder_obj_t *self = native_gifdecoder(self_in);
check_for_deinit(self);
return MP_OBJ_NEW_SMALL_INT(common_hal_videogif_gifdecoder_get_width(self));
}
MP_DEFINE_CONST_FUN_OBJ_1(videogif_gifdecoder_get_width_obj, videogif_gifdecoder_get_width);

MP_PROPERTY_GETTER(videogif_gifdecoder_width_obj,
(mp_obj_t)&videogif_gifdecoder_get_width_obj);

//| height: int
//| """Height of the GIF in pixels. (read-only)"""
//|
//|
static mp_obj_t videogif_gifdecoder_get_height(mp_obj_t self_in) {
videogif_gifdecoder_obj_t *self = native_gifdecoder(self_in);
check_for_deinit(self);
return MP_OBJ_NEW_SMALL_INT(common_hal_videogif_gifdecoder_get_height(self));
}
MP_DEFINE_CONST_FUN_OBJ_1(videogif_gifdecoder_get_height_obj, videogif_gifdecoder_get_height);

MP_PROPERTY_GETTER(videogif_gifdecoder_height_obj,
(mp_obj_t)&videogif_gifdecoder_get_height_obj);

static const mp_rom_map_elem_t videogif_gifdecoder_locals_dict_table[] = {
{ MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&videogif_gifdecoder_deinit_obj) },
{ MP_ROM_QSTR(MP_QSTR___enter__), MP_ROM_PTR(&default___enter___obj) },
{ MP_ROM_QSTR(MP_QSTR___exit__), MP_ROM_PTR(&default___exit___obj) },
{ MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&videogif_gifdecoder_deinit_obj) },
{ MP_ROM_QSTR(MP_QSTR_height), MP_ROM_PTR(&videogif_gifdecoder_height_obj) },
{ MP_ROM_QSTR(MP_QSTR_width), MP_ROM_PTR(&videogif_gifdecoder_width_obj) },
};
static MP_DEFINE_CONST_DICT(videogif_gifdecoder_locals_dict, videogif_gifdecoder_locals_dict_table);

// Implementation of the videocore videosource protocol so a VideoBuffer can
// tell the GifDecoder where to put frames.
static void videogif_gifdecoder_play(mp_obj_t self_in, bool paused) {
videogif_gifdecoder_obj_t *self = native_gifdecoder(self_in);
check_for_deinit(self);
common_hal_videogif_gifdecoder_play(self, paused);
}

static void videogif_gifdecoder_stop(mp_obj_t self_in) {
common_hal_videogif_gifdecoder_stop(native_gifdecoder(self_in));
}

static void videogif_gifdecoder_pause(mp_obj_t self_in) {
common_hal_videogif_gifdecoder_pause(native_gifdecoder(self_in));
}

static void videogif_gifdecoder_resume(mp_obj_t self_in) {
common_hal_videogif_gifdecoder_resume(native_gifdecoder(self_in));
}

static bool videogif_gifdecoder_set_destination(mp_obj_t self_in, const videocore_destination_t *destination,
videocore_on_frame_fun on_frame, void *context) {
return common_hal_videogif_gifdecoder_set_destination(native_gifdecoder(self_in), destination, on_frame, context);
}

static mp_int_t videogif_gifdecoder_videosource_get_width(mp_obj_t self_in) {
return common_hal_videogif_gifdecoder_get_width(native_gifdecoder(self_in));
}

static mp_int_t videogif_gifdecoder_videosource_get_height(mp_obj_t self_in) {
return common_hal_videogif_gifdecoder_get_height(native_gifdecoder(self_in));
}

static mp_int_t videogif_gifdecoder_get_bits_per_pixel(mp_obj_t self_in) {
return 16;
}

static const videosource_p_t videogif_gifdecoder_videosource_protocol = {
MP_PROTO_IMPLEMENT(MP_QSTR_protocol_videosource)
.play = videogif_gifdecoder_play,
.pause = videogif_gifdecoder_pause,
.resume = videogif_gifdecoder_resume,
.stop = videogif_gifdecoder_stop,
.set_destination = videogif_gifdecoder_set_destination,
.get_width = videogif_gifdecoder_videosource_get_width,
.get_height = videogif_gifdecoder_videosource_get_height,
.get_bits_per_pixel = videogif_gifdecoder_get_bits_per_pixel,
};

MP_DEFINE_CONST_OBJ_TYPE(
videogif_gifdecoder_type,
MP_QSTR_GifDecoder,
MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS,
make_new, videogif_gifdecoder_make_new,
locals_dict, &videogif_gifdecoder_locals_dict,
protocol, &videogif_gifdecoder_videosource_protocol
);
Loading
Loading