Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Apply best practices for creating programmatic videos with Remotion and React.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
rules/extract-frames.md
1---2name: extract-frames3description: Extract frames from videos at specific timestamps using Mediabunny4metadata:5tags: frames, extract, video, thumbnail, filmstrip, canvas6---78# Extracting frames from videos910Use Mediabunny to extract frames from videos at specific timestamps. This is useful for generating thumbnails, filmstrips, or processing individual frames.1112## The `extractFrames()` function1314This function can be copy-pasted into any project.1516```tsx17import {18ALL_FORMATS,19Input,20UrlSource,21VideoSample,22VideoSampleSink,23} from "mediabunny";2425type Options = {26track: { width: number; height: number };27container: string;28durationInSeconds: number | null;29};3031export type ExtractFramesTimestampsInSecondsFn = (32options: Options,33) => Promise<number[]> | number[];3435export type ExtractFramesProps = {36src: string;37timestampsInSeconds: number[] | ExtractFramesTimestampsInSecondsFn;38onVideoSample: (sample: VideoSample) => void;39signal?: AbortSignal;40};4142export async function extractFrames({43src,44timestampsInSeconds,45onVideoSample,46signal,47}: ExtractFramesProps): Promise<void> {48using input = new Input({49formats: ALL_FORMATS,50source: new UrlSource(src),51});5253const [durationInSeconds, format, videoTrack] = await Promise.all([54input.computeDuration(),55input.getFormat(),56input.getPrimaryVideoTrack(),57]);5859if (!videoTrack) {60throw new Error("No video track found in the input");61}6263if (signal?.aborted) {64throw new Error("Aborted");65}6667const timestamps =68typeof timestampsInSeconds === "function"69? await timestampsInSeconds({70track: {71width: videoTrack.displayWidth,72height: videoTrack.displayHeight,73},74container: format.name,75durationInSeconds,76})77: timestampsInSeconds;7879if (timestamps.length === 0) {80return;81}8283if (signal?.aborted) {84throw new Error("Aborted");85}8687const sink = new VideoSampleSink(videoTrack);8889for await (using videoSample of sink.samplesAtTimestamps(timestamps)) {90if (signal?.aborted) {91break;92}9394if (!videoSample) {95continue;96}9798onVideoSample(videoSample);99}100}101```102103## Basic usage104105Extract frames at specific timestamps:106107```tsx108await extractFrames({109src: "https://remotion.media/video.mp4",110timestampsInSeconds: [0, 1, 2, 3, 4],111onVideoSample: (sample) => {112const canvas = document.createElement("canvas");113canvas.width = sample.displayWidth;114canvas.height = sample.displayHeight;115const ctx = canvas.getContext("2d");116sample.draw(ctx!, 0, 0);117},118});119```120121## Creating a filmstrip122123Use a callback function to dynamically calculate timestamps based on video metadata:124125```tsx126const canvasWidth = 500;127const canvasHeight = 80;128const fromSeconds = 0;129const toSeconds = 10;130131await extractFrames({132src: "https://remotion.media/video.mp4",133timestampsInSeconds: async ({ track, durationInSeconds }) => {134const aspectRatio = track.width / track.height;135const amountOfFramesFit = Math.ceil(136canvasWidth / (canvasHeight * aspectRatio),137);138const segmentDuration = toSeconds - fromSeconds;139const timestamps: number[] = [];140141for (let i = 0; i < amountOfFramesFit; i++) {142timestamps.push(143fromSeconds + (segmentDuration / amountOfFramesFit) * (i + 0.5),144);145}146147return timestamps;148},149onVideoSample: (sample) => {150console.log(`Frame at ${sample.timestamp}s`);151152const canvas = document.createElement("canvas");153canvas.width = sample.displayWidth;154canvas.height = sample.displayHeight;155const ctx = canvas.getContext("2d");156sample.draw(ctx!, 0, 0);157},158});159```160161## Cancellation with AbortSignal162163Cancel frame extraction after a timeout:164165```tsx166const controller = new AbortController();167168setTimeout(() => controller.abort(), 5000);169170try {171await extractFrames({172src: "https://remotion.media/video.mp4",173timestampsInSeconds: [0, 1, 2, 3, 4],174onVideoSample: (sample) => {175using frame = sample;176const canvas = document.createElement("canvas");177canvas.width = frame.displayWidth;178canvas.height = frame.displayHeight;179const ctx = canvas.getContext("2d");180frame.draw(ctx!, 0, 0);181},182signal: controller.signal,183});184185console.log("Frame extraction complete!");186} catch (error) {187console.error("Frame extraction was aborted or failed:", error);188}189```190191## Timeout with Promise.race192193```tsx194const controller = new AbortController();195196const timeoutPromise = new Promise<never>((_, reject) => {197const timeoutId = setTimeout(() => {198controller.abort();199reject(new Error("Frame extraction timed out after 10 seconds"));200}, 10000);201202controller.signal.addEventListener("abort", () => clearTimeout(timeoutId), {203once: true,204});205});206207try {208await Promise.race([209extractFrames({210src: "https://remotion.media/video.mp4",211timestampsInSeconds: [0, 1, 2, 3, 4],212onVideoSample: (sample) => {213using frame = sample;214const canvas = document.createElement("canvas");215canvas.width = frame.displayWidth;216canvas.height = frame.displayHeight;217const ctx = canvas.getContext("2d");218frame.draw(ctx!, 0, 0);219},220signal: controller.signal,221}),222timeoutPromise,223]);224225console.log("Frame extraction complete!");226} catch (error) {227console.error("Frame extraction was aborted or failed:", error);228}229```230