Skip to content

chore: gif renderer#943

Draft
cjpillsbury wants to merge 7 commits intomainfrom
feat/gif-renderer
Draft

chore: gif renderer#943
cjpillsbury wants to merge 7 commits intomainfrom
feat/gif-renderer

Conversation

@cjpillsbury
Copy link
Collaborator

@cjpillsbury cjpillsbury commented Mar 13, 2026

Summary

An animated GIF renderer built on Video.js 10's core infrastructure — no changes to any feature, store, or UI component. The GIF plays/pauses/restarts through `playbackFeature` exactly like a native `

The goal is twofold: demonstrate how to build a custom media renderer that plugs into the existing architecture, and surface any points of friction in our convenience abstractions (delegates, mixins, container discovery) for real external-media use cases.

Code walkthrough

Core delegate — `packages/sandbox/templates/gif-media/`

File Role
`gif-media.ts` `GifMedia extends EventTarget` — fetches + decodes GIFs via `gifuct-js`, drives a `` with `requestAnimationFrame`, dispatches `play` / `playing` / `pause` events
`html.ts` `GifMediaElement extends HTMLElement` — web component wrapper; owns shadow DOM with a ``, delegates the media interface (`paused`, `play()`, `pause()`, `addEventListener`) to its internal `GifMedia` instance
`react.tsx` `AnimatedGif` — React component that renders `` and registers `GifMedia` as the player's media target via `useMediaRegistration()`

Framework demos

File Role
`templates/html-gif/main.ts` HTML demo — registers `gif-video` and a custom `video-player` element built with `ProviderMixin(ContainerMixin(MediaElement))`
`templates/react-gif/main.tsx` React demo — renders `` + `` + `` with `PlayButton`

App shell integration — `packages/sandbox/app/`

  • `constants.ts` — adds `'gif'` to `PRESETS`
  • `shell/navbar.tsx` — labels gif preset; disables styling/skin/source selectors (not applicable for GIF)
  • `shell/app.tsx` — auto-selects a gif source when the gif preset is chosen, reverts to default on switch away
  • `shared/sources.ts` — adds Mux animated GIF sources (`gif-1`, `gif-2`) via `https://image.mux.com/{playbackId}/animated.gif\`

Try it locally

pnpm install
pnpm dev

Open http://localhost:5173 and select Preset → GIF in the navbar. Switch between HTML and React platforms. The play/pause button and the native `` comparison render side-by-side.

Direct links (after `pnpm dev`):

What this surfaces

A few things became apparent building this against our existing abstractions:

  • Element naming convention — `container-mixin` discovers custom media elements by checking whether the tag name ends in `-video` or `-audio`. This is implicit and undocumented; `gif-media` was silently ignored until renamed to `gif-video`.
  • Mixin composition order — `ProviderMixin(ContainerMixin(MediaElement))` is required (not the reverse) because `ProviderMixin`'s constructor reads `this.store` during field initialization; reversing the order hits a private-field-before-init error.
  • `createPlayer` API — the old `{ PlayerElement }` shape no longer exists; external demos need to compose `ProviderMixin` + `ContainerMixin` themselves.

cjpillsbury and others added 7 commits March 13, 2026 11:38
Demonstrates Video.js 10's architectural versatility by building an
animated GIF renderer that plugs into the existing feature/slice system
without modifying any feature or UI component.

Key components:
- GifMedia: EventTarget subclass that fetches and decodes GIFs via
  gifuct-js, drives a canvas with requestAnimationFrame, and emits the
  play/pause/playing events that playbackFeature listens to
- AnimatedGif: React component that renders <canvas> and registers
  GifMedia as the player's media target via useMediaRegistration()
- GifMediaElement: HTMLElement custom element with EventTarget delegation
  to its internal GifMedia; sets data-media-element so the container
  mixin discovers it automatically

Demos at /react-gif/ and /html-gif/ show the Play/Pause button working
with a GIF using only playbackFeature — no changes to features or UI.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Wire up PlayButton icons via render prop (React) and icon spans with
  data-attribute CSS (HTML) — both patterns from the spf demos
- Add native <img> alongside the canvas for side-by-side comparison,
  making it easy to see the GifMedia play/pause against the always-on
  browser-native GIF rendering

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…n ms

gifuct-js converts GCE centiseconds → milliseconds internally:
  resultImage.delay = (frame.gce.delay || 10) * 10; // convert to ms

Our extra `* 10` multiplier was making every frame display 10× too
long (a 40ms frame appeared as 400ms). Also note the `|| 10` fallback:
0-delay frames become 100ms, matching the browser clamping behavior.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Demonstrate that a media renderer needs almost nothing from
HTMLMediaElement to integrate with the feature system.

gif-media.ts:
- Replace #lastTime + #elapsed with a single #nextFrameAt timestamp,
  eliminating two private fields and simplifying #tick
- Replace AbortController with a generation counter (#loadGen),
  removing #loadAbort and the redundant abort at the top of #load()
- Store the ParsedGif object directly (#gif) so lsd.width/height come
  from the library rather than extra #gifWidth/#gifHeight fields
- Fix play() before src: defer play/playing events until frames are
  available; #load() emits them when it starts playback
- Silence error events from superseded loads in the catch block

html.ts:
- Remove delegating stubs for ended, currentTime, duration, readyState,
  and load() — none are needed; playbackFeature only requires paused,
  play(), pause(), and EventTarget dispatch

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Collapse #canvas + #tempCanvas into single #render object
- Guard play() for already-unpaused and no-src cases
- Remove redundant guards from #tick and #start
- Drop optional chaining on frame index access

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Add 'gif' preset to PRESETS and app shell (constants, navbar, app.tsx)
- Auto-select gif source when switching to gif preset; revert on switch away
- Disable styling/skin/source selectors for gif preset (no tailwind variant)
- Add Mux animated GIF sources (gif-1: Dancing Dude, gif-2: Big Buck Bunny)
- Rename gif-media element to gif-video for container-mixin discovery
- Update html-gif to new createPlayer API: ProviderMixin + ContainerMixin
- Use ProviderMixin(ContainerMixin(MediaElement)) so #contextStore initializes
  before ProviderMixin constructor reads this.store
- Drop old static src/index.html superseded by the app shell from main

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove templates/index.html (superseded by app shell)
- Drop data-media-element attribute setting: container-mixin discovers
  gif-video by element name convention (*-video), not by attribute

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel
Copy link

vercel bot commented Mar 13, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
v10-sandbox Ready Ready Preview, Comment Mar 13, 2026 6:45pm

Request Review

@netlify
Copy link

netlify bot commented Mar 13, 2026

Deploy Preview for vjs10-site ready!

Name Link
🔨 Latest commit 5566ec2
🔍 Latest deploy log https://app.netlify.com/projects/vjs10-site/deploys/69b45b46048c4f00080994a9
😎 Deploy Preview https://deploy-preview-943--vjs10-site.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@cjpillsbury cjpillsbury marked this pull request as draft March 13, 2026 18:45
@github-actions
Copy link
Contributor

📦 Bundle Size Report

🎨 @videojs/html

(no changes)

Presets (7)
Entry Size
/video (default) 21.97 kB
/video (default + hls) 152.28 kB
/video (minimal) 21.79 kB
/video (minimal + hls) 152.17 kB
/audio (default) 20.65 kB
/audio (minimal) 20.64 kB
/background 6.47 kB
Media (4)
Entry Size
/media/background-video 617 B
/media/container 1.91 kB
/media/hls-video 131.23 kB
/media/simple-hls-video 11.89 kB
Players (3)
Entry Size
/video/player 6.33 kB
/audio/player 6.32 kB
/background/player 6.31 kB
Skins (16)
Entry Type Size
/video/minimal-skin.css css 2.65 kB
/video/skin.css css 2.68 kB
/video/minimal-skin js 21.21 kB
/video/minimal-skin.tailwind js 21.38 kB
/video/skin js 21.37 kB
/video/skin.tailwind js 21.63 kB
/audio/minimal-skin.css css 2.17 kB
/audio/skin.css css 2.19 kB
/audio/minimal-skin js 20.10 kB
/audio/minimal-skin.tailwind js 20.04 kB
/audio/skin js 20.15 kB
/audio/skin.tailwind js 20.28 kB
/background/skin.css css 124 B
/background/skin js 999 B
/base.css css 205 B
/shared.css css 35 B
UI Components (21)
Entry Size
/ui/alert-dialog 2.08 kB
/ui/alert-dialog-close 1.29 kB
/ui/alert-dialog-description 1.53 kB
/ui/alert-dialog-title 1.52 kB
/ui/buffering-indicator 1.82 kB
/ui/captions-button 1.79 kB
/ui/controls 1.79 kB
/ui/fullscreen-button 1.80 kB
/ui/mute-button 1.77 kB
/ui/pip-button 1.78 kB
/ui/play-button 1.76 kB
/ui/playback-rate-button 1.83 kB
/ui/popover 3.16 kB
/ui/poster 1.66 kB
/ui/seek-button 1.81 kB
/ui/slider 2.00 kB
/ui/thumbnail 2.10 kB
/ui/time 1.66 kB
/ui/time-slider 2.94 kB
/ui/tooltip 2.42 kB
/ui/volume-slider 2.17 kB

Sizes are marginal over the root entry point.

⚛️ @videojs/react

(no changes)

Presets (7)
Entry Size
/video (default) 16.93 kB
/video (default + hls) 147.62 kB
/video (minimal) 16.89 kB
/video (minimal + hls) 147.78 kB
/audio (default) 14.60 kB
/audio (minimal) 14.63 kB
/background 3.19 kB
Media (3)
Entry Size
/media/background-video 539 B
/media/hls-video 131.52 kB
/media/simple-hls-video 12.28 kB
Skins (14)
Entry Type Size
/video/minimal-skin.css css 2.64 kB
/video/skin.css css 2.68 kB
/video/minimal-skin js 16.78 kB
/video/minimal-skin.tailwind js 19.42 kB
/video/skin js 16.80 kB
/video/skin.tailwind js 19.51 kB
/audio/minimal-skin.css css 2.16 kB
/audio/skin.css css 2.18 kB
/audio/minimal-skin js 14.53 kB
/audio/minimal-skin.tailwind js 16.39 kB
/audio/skin js 14.48 kB
/audio/skin.tailwind js 16.58 kB
/background/skin.css css 90 B
/background/skin js 272 B
UI Components (17)
Entry Size
/ui/alert-dialog 2.72 kB
/ui/buffering-indicator 2.19 kB
/ui/captions-button 2.24 kB
/ui/controls 2.19 kB
/ui/fullscreen-button 2.23 kB
/ui/mute-button 2.25 kB
/ui/pip-button 2.23 kB
/ui/play-button 2.24 kB
/ui/playback-rate-button 2.27 kB
/ui/popover 3.08 kB
/ui/poster 2.02 kB
/ui/seek-button 2.25 kB
/ui/slider 3.08 kB
/ui/time 2.32 kB
/ui/time-slider 2.77 kB
/ui/tooltip 3.27 kB
/ui/volume-slider 2.72 kB

Sizes are marginal over the root entry point.

🧩 @videojs/core

(no changes)

Entries (5)
Entry Size
. 4.78 kB
/dom 8.07 kB
/dom/media/custom-media-element 1.76 kB
/dom/media/hls 131.14 kB
/dom/media/simple-hls 11.85 kB

🏷️ @videojs/element

(no changes)

Entries (2)
Entry Size
. 999 B
/context 936 B

📦 @videojs/store

(no changes)

Entries (3)
Entry Size
. 1.32 kB
/html 700 B
/react 360 B

🔧 @videojs/utils

(no changes)

Entries (10)
Entry Size
/array 104 B
/dom 1003 B
/events 227 B
/function 261 B
/object 119 B
/predicate 265 B
/string 148 B
/style 190 B
/time 478 B
/number 158 B

📦 @videojs/spf

(no changes)

Entries (3)
Entry Size
. 40 B
/dom 10.04 kB
/playback-engine 9.95 kB

ℹ️ How to interpret

All sizes are standalone totals (minified + brotli).

Icon Meaning
No change
🔺 Increased ≤ 10%
🔴 Increased > 10%
🔽 Decreased
🆕 New (no baseline)

Run pnpm size locally to check current sizes.

#nextFrameAt = 0;
#loadGen = 0;

get paused(): boolean {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TL;DR here - as long as you make this delegate "look like" an HTMLMediaElement, and only for the things you need (e.g. in this simple implementation, I don't have duration/currentTime/playbackRate/etc), you can do whatever else you want with the "innards"

return this.#paused;
}

get src(): string {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE: src isn't as strict as other HTMLMediaElement API conformance. That said, we encourage you to use a separate property (e.g. source, or, like we did for <mux-video> pre-VJSv10, playbackid for our Mux Video specific shorthand) if you want to provide something other than a string!

this.dispatchEvent(new Event('pause'));
}

attach(canvas: HTMLCanvasElement): void {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

attach(), detach() are the VJS-specific integration/extension points. They're used indirectly by the html/web component + react component integrations. note that this example accepts an HTMLCanvasElement, unlike others in our core library, which expect an HTMLMediaElement

@@ -0,0 +1,68 @@
import { GifMedia } from './gif-media';

export class GifMediaElement extends HTMLElement {
Copy link
Collaborator Author

@cjpillsbury cjpillsbury Mar 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE: You only have to implement this if you're planning on supporting web components!
Hopefully we can also improve boilerplate here so you can write less code going forward! Feedback welcome!

@@ -0,0 +1,48 @@
import type { Media } from '@videojs/react';
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE: You only have to implement this if you're planning on supporting react!
Hopefully we can also improve boilerplate here so you can write less code going forward! Feedback welcome!

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant