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 ,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 ;onFrame : (frame :VideoFrame ) => void;signal ?:AbortSignal ;};export async functionextractFrames ({src ,timestampsInSeconds ,onFrame ,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;}constvideoFrame =videoSample .toVideoFrame ();onFrame (videoFrame );}} 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],onFrame: (frame) => {// 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;},onFrame: (frame) => {// Process each frameconsole.log(`Frame at ${frame.timestamp / 1_000_000}s`);frame.close();},});
Important notes
Memory management
Always close VideoFrame objects when you're done with them to prevent memory leaks:
tsonFrame: (frame) => {// 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],onFrame: (frame) => {// Process frameframe.close();},signal: controller.signal,});
VideoFrame timestamps
VideoFrame.timestamp is in microseconds, while mediabunny's VideoSample.timestamp is in seconds. The conversion is handled automatically by .toVideoFrame().