Skip to content

Instantly share code, notes, and snippets.

@jord-goldberg
Last active March 22, 2024 14:35
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jord-goldberg/41670e2cac3da0bef1df0a49eceb01d7 to your computer and use it in GitHub Desktop.
Save jord-goldberg/41670e2cac3da0bef1df0a49eceb01d7 to your computer and use it in GitHub Desktop.
useVideoFrames react hook - a callback for every frame of a video element
import React, { useEffect, useRef, useState } from "react";
type VideoEventListenerMap = {
[EventName in keyof HTMLMediaElementEventMap]?: EventListener;
};
const useVideoFrames = (
frameCallback = (videoTime: number) => {}
): [HTMLVideoElement | null, React.RefCallback<HTMLVideoElement>] => {
const [video, setVideo] = useState<HTMLVideoElement | null>(null);
const callbackRef = useRef(frameCallback);
callbackRef.current = frameCallback;
useEffect(() => {
if (!video) return;
let frameId: number | null;
let requestFrame = requestAnimationFrame;
let cancelFrame = cancelAnimationFrame;
if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) {
// https://web.dev/requestvideoframecallback-rvfc/
const vid = video as HTMLVideoElement & {
requestVideoFrameCallback: typeof requestAnimationFrame;
cancelVideoFrameCallback: typeof cancelAnimationFrame;
};
requestFrame = vid.requestVideoFrameCallback.bind(vid);
cancelFrame = vid.cancelVideoFrameCallback.bind(vid);
}
const callbackFrame = (now: number, metadata?: any) => {
const videoTime = metadata?.mediaTime ?? video.currentTime;
callbackRef.current(videoTime);
frameId = requestFrame(callbackFrame);
};
const eventListeners: VideoEventListenerMap = {
loadeddata() {
requestAnimationFrame(() => callbackRef.current(video.currentTime));
},
play() {
frameId = requestFrame(callbackFrame);
},
pause() {
cancelFrame(frameId ?? 0);
frameId = null;
},
timeupdate() {
if (!frameId) {
requestAnimationFrame(() => callbackRef.current(video.currentTime));
}
},
};
Object.keys(eventListeners).forEach((eventName) => {
const eventListener =
eventListeners[eventName as keyof HTMLMediaElementEventMap];
if (eventListener != null) {
video.addEventListener(eventName, eventListener);
}
});
return () => {
cancelFrame(frameId ?? 0);
Object.keys(eventListeners).forEach((eventName) => {
const eventListener =
eventListeners[eventName as keyof HTMLMediaElementEventMap];
if (eventListener != null) {
video.removeEventListener(eventName, eventListener);
}
});
};
}, [video]);
return [video, setVideo];
};
export default useVideoFrames;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment