Last active
January 7, 2025 06:20
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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