Extracting frames from a video in JavaScript
Extracting frames from a video file, for example to display a filmstrip in an editing interface, can be done using Mediabunny.
Here's a extractFrames() function you can use coyp and paste into your project:
extract-frames.tstsimport {ALL_FORMATS ,Input ,InputDisposedError ,UrlSource ,VideoSample ,VideoSampleSink } from 'mediabunny';typeOptions = {track : {width : number;height : number};container : string;durationInSeconds : number | null;};export typeExtractFramesTimestampsInSecondsFn = (options :Options ) =>Promise <number[]> | number[];export typeExtractFramesProps = {src : string;timestampsInSeconds : number[] |ExtractFramesTimestampsInSecondsFn ;onVideoSample : (sample :VideoSample ) => void;signal ?:AbortSignal ;};export async functionextractFrames ({src ,timestampsInSeconds ,onVideoSample ,signal }:ExtractFramesProps ):Promise <void> {constinput = newInput ({formats :ALL_FORMATS ,source : newUrlSource (src ),});constdispose = () => {input .dispose ();};if (signal ) {signal .addEventListener ('abort',dispose , {once : true});}try {const [durationInSeconds ,format ,videoTrack ] = awaitPromise .all ([input .computeDuration (),input .getFormat (),input .getPrimaryVideoTrack ()]);if (!videoTrack ) {throw newError ('No video track found in the input');}consttimestamps =typeoftimestampsInSeconds === 'function'? awaittimestampsInSeconds ({track : {width :videoTrack .displayWidth ,height :videoTrack .displayHeight ,},container :format .name ,durationInSeconds ,}):timestampsInSeconds ;if (timestamps .length === 0) {return;}constsink = newVideoSampleSink (videoTrack );for await (constvideoSample ofsink .samplesAtTimestamps (timestamps )) {if (signal ?.aborted ) {break;}if (!videoSample ) {continue;}onVideoSample (videoSample );}} catch (error ) {if (error instanceofInputDisposedError ) {return;}throwerror ;} finally {dispose ();if (signal ) {signal .removeEventListener ('abort',dispose );}}}
Usage
Basic example: Extract frames at specific times
tsawait extractFrames({src: 'https://remotion.media/video.mp4',timestampsInSeconds: [0, 1, 2, 3, 4],onVideoSample: (sample) => {// Convert sample to VideoFrameconst frame = sample.toVideoFrame();// Draw frame to canvasconst canvas = document.createElement('canvas');canvas.width = frame.displayWidth;canvas.height = frame.displayHeight;const ctx = canvas.getContext('2d');ctx.drawImage(frame, 0, 0);// Don't forget to close the frame when doneframe.close();},});
Advanced: Create a filmstrip
Extract as many frames as fit in a canvas based on the video's aspect ratio:
tsconst canvasWidth = 500;const canvasHeight = 80;const fromSeconds = 0;const toSeconds = 10;await extractFrames({src: 'https://example.com/video.mp4',timestampsInSeconds: async ({track, durationInSeconds}) => {const aspectRatio = track.width / track.height;const amountOfFramesFit = Math.ceil(canvasWidth / (canvasHeight * aspectRatio));const segmentDuration = toSeconds - fromSeconds;const timestamps: number[] = [];for (let i = 0; i < amountOfFramesFit; i++) {timestamps.push(fromSeconds + (segmentDuration / amountOfFramesFit) * (i + 0.5));}return timestamps;},onVideoSample: (sample) => {// Convert to VideoFrame if neededconst frame = sample.toVideoFrame();// Process the frameconsole.log(`Frame at ${sample.timestamp}s`);// Clean upframe.close();},});
Important notes
Memory management
The onVideoSample callback receives a VideoSample object. You need to convert it to a VideoFrame using .toVideoFrame(), and always close the VideoFrame when you're done with it to prevent memory leaks:
tsonVideoSample: (sample) => {const frame = sample.toVideoFrame();// Use the framectx.drawImage(frame, 0, 0);// Clean upframe.close();};
Abort handling
Use an AbortSignal to cancel frame extraction:
tsconst controller = new AbortController();// Cancel after 5 secondssetTimeout(() => controller.abort(), 5000);await extractFrames({src: 'https://example.com/video.mp4',timestampsInSeconds: [0, 1, 2, 3, 4],onVideoSample: (sample) => {const frame = sample.toVideoFrame();// Process frameframe.close();},signal: controller.signal,});