fix: Add proper GIF support with transparency and per-frame timing by Dav1d-Fn · Pull Request #595 · StreamController/StreamController · GitHub
Skip to content

fix: Add proper GIF support with transparency and per-frame timing#595

Open
Dav1d-Fn wants to merge 3 commits into
StreamController:mainfrom
Dav1d-Fn:main
Open

fix: Add proper GIF support with transparency and per-frame timing#595
Dav1d-Fn wants to merge 3 commits into
StreamController:mainfrom
Dav1d-Fn:main

Conversation

@Dav1d-Fn

Copy link
Copy Markdown

Problem

GIFs were decoded using OpenCV (cv2.VideoCapture), which has two limitations:

  1. No transparency support — GIF frames in palette mode (P) with a transparency index were composited onto a white background, losing the alpha channel.
  2. Incorrect frame timing — All frames played at a fixed FPS value, ignoring the per-frame delay stored in the GIF metadata. This caused animations with variable timing (e.g. a 2-second intro frame followed by fast animation frames) to play incorrectly or appear cut short.

Solution

GIF files are now detected by extension and handled separately using PIL:

  • key_video_cache.py: GIFs are opened with Image.open() and each frame is converted to RGBA, preserving transparency. Frames are cached as PNG instead of JPEG to retain the alpha channel. Per-frame delays are read from the GIF metadata (img.info["duration"]) and stored in frame_delays.
  • KeyVideo.py: A new _get_next_gif_frame() method advances playback by accumulating elapsed milliseconds and comparing against the current frame's delay, rather than using a global FPS value.

All non-GIF video formats (mp4, webm, etc.) are unaffected and continue to use the existing cv2 code path.

Testing

Tested with:

  • Transparent GIF (palette mode with transparency index) — now renders correctly on dark backgrounds
  • GIF with uniform frame delays — plays at correct speed
  • GIF with variable frame delays (long first frame + short animation frames) — each frame now shown for its correct duration
  • Non-GIF videos (mp4) — behaviour unchanged

GIFs were previously decoded using OpenCV (cv2), which does not support
GIF transparency or animation metadata. This caused two issues:
- Transparent GIFs rendered with a white background
- All GIF frames played at a fixed FPS regardless of per-frame delays,
  causing animations to play at the wrong speed or appear cut short

Changes:
- key_video_cache.py: Detect GIFs by file extension and decode them
  using PIL instead of cv2. Convert each frame to RGBA to preserve
  the alpha channel. Cache GIF frames as PNG instead of JPEG to retain
  transparency. Read per-frame delays from GIF metadata and store them
  in frame_delays for use during playback.
- KeyVideo.py: Add _get_next_gif_frame() which advances playback based
  on per-frame delay in milliseconds rather than a fixed FPS value.
  This correctly handles GIFs with variable frame timing (e.g. a long
  first frame followed by short animation frames).
… deduplication

Loading pages with multiple animated GIFs caused multi-second freezes because
all frames were decoded synchronously on the calling thread.

- Render frame 0 synchronously in __init__ so every button shows a static
  preview immediately; all remaining work runs in a background daemon thread
- Save per-frame delays to delays.json next to the cached frames so the slow
  PIL seek-loop only runs once; subsequent loads read the JSON instantly
- Add VideoFrameCache.get_or_create() registry so the same GIF+size is only
  loaded once even when multiple buttons reference the same file; use it in
  InputVideo to avoid redundant work on page switches
@Dav1d-Fn

Copy link
Copy Markdown
Author

…ming accuracy

key_video_cache.py: Replace the single global registry lock with a
per-key initialization lock. The previous approach ran __init__ (including
Image.open and LANCZOS resize) outside the global lock, allowing two threads
to create instances for the same GIF simultaneously and write to the same
cache files concurrently — corrupting PNGs and causing segfaults in PIL.
The per-key lock ensures only one thread initialises a given (path, size)
at a time without blocking unrelated keys.

KeyVideo.py: Replace tick-based GIF timing with time.perf_counter(). The
previous implementation accumulated elapsed time in media player ticks,
which caused playback to drift when the media player was under CPU load.
Also replace the single if with a while loop so multiple frames are skipped
when catch-up is needed, keeping the animation in sync with real time.
@Dav1d-Fn

Copy link
Copy Markdown
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

1 participant