The Tool Desk
Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
Use an HTML <video> element as a canvas image source. For each timestamp, assign video.currentTime, wait for the seek to finish, draw the video frame into a canvas, then export it with canvas.toBlob(). Process timestamps one at a time: setting currentTime starts a seek but does not guarantee that the requested frame is already ready.
How the capture process works
A browser can draw the current video frame into a 2D canvas with drawImage(). To capture several points in a video, repeat this sequence for each time: validate the time, seek, wait for the seek to complete, draw the frame, and encode the canvas as an image.
- Wait until video metadata is available so dimensions and duration can be read.
- Assign a requested time in seconds to
video.currentTime. - Wait for the
seekedevent before drawing. - Call
ctx.drawImage(video, 0, 0, canvas.width, canvas.height). - Export the canvas, preferably as a Blob using
toBlob().
MDN describes currentTime as the media’s current playback time in seconds; assigning it requests a seek. The seek may be constrained by available timeline data, and the resulting position is not necessarily an exact arbitrary frame. See MDN’s currentTime reference.
The seeked event fires when a seek operation completes and the current playback position changes. It is the basic synchronization point for this workflow; it does not promise frame-accurate seeking for every codec, browser, or stream. See MDN’s seeked event reference.
#1 Best Overall
Runnable example: capture multiple timestamps
This example takes a list of times, seeks serially, and displays downloadable PNGs in a gallery. It assumes the page already contains the video element below and that its source can be read by canvas under the same-origin or CORS rules discussed later.
<video id="video" controls preload="metadata" src="/media/demo.mp4"></video>
<canvas id="canvas" hidden></canvas>
<div id="frames"></div>
<script>
function waitForEvent(target, eventName, timeoutMs = 15000) {
return new Promise((resolve, reject) => {
const cleanup = () => {
clearTimeout(timer);
target.removeEventListener(eventName, onEvent);
target.removeEventListener("error", onError);
};
const onEvent = (event) => {
cleanup();
resolve(event);
};
const onError = () => {
cleanup();
reject(target.error || new Error("Video failed to load"));
};
const timer = setTimeout(() => {
cleanup();
reject(new Error(`Timed out waiting for ${eventName}`));
}, timeoutMs);
target.addEventListener(eventName, onEvent, { once: true });
target.addEventListener("error", onError, { once: true });
});
}
async function ensureMetadata(video) {
if (video.readyState >= HTMLMediaElement.HAVE_METADATA) return;
await waitForEvent(video, "loadedmetadata");
}
function validateTime(video, seconds) {
if (!Number.isFinite(seconds) || seconds < 0) {
throw new RangeError("Timestamp must be a finite, non-negative number of seconds");
}
if (Number.isFinite(video.duration) && seconds > video.duration) {
throw new RangeError(`Timestamp ${seconds}s is beyond the video duration ${video.duration}s`);
}
if (video.seekable.length) {
let available = false;
for (let i = 0; i < video.seekable.length; i++) {
if (seconds >= video.seekable.start(i) && seconds <= video.seekable.end(i)) {
available = true;
break;
}
}
if (!available) throw new RangeError(`Timestamp ${seconds}s is outside the seekable ranges`);
}
}
async function captureAt(video, canvas, seconds) {
await ensureMetadata(video);
validateTime(video, seconds);
const ctx = canvas.getContext("2d");
if (!ctx) throw new Error("Canvas 2D context is unavailable");
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
if (!canvas.width || !canvas.height) {
throw new Error("Video dimensions are not available");
}
// Install the listener before assigning currentTime so a quick seek is not missed.
const seekFinished = waitForEvent(video, "seeked");
video.currentTime = seconds;
if (video.seeking) await seekFinished;
else {
// The requested position may already be current; no seeked event is then required.
// Avoid leaving a pending listener if no event will arrive.
video.removeEventListener("seeked", () => {});
}
// If supported, wait for a frame-aware callback before drawing.
if ("requestVideoFrameCallback" in video) {
await new Promise((resolve) => video.requestVideoFrameCallback(resolve));
}
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
return new Promise((resolve, reject) => {
canvas.toBlob((blob) => {
if (blob) resolve(blob);
else reject(new Error("Canvas image encoding failed"));
}, "image/png");
});
}
async function captureMany(video, canvas, timesInSeconds) {
const results = [];
for (const seconds of timesInSeconds) {
const blob = await captureAt(video, canvas, seconds);
results.push({ seconds, blob });
}
return results;
}
const video = document.querySelector("#video");
const canvas = document.querySelector("#canvas");
const gallery = document.querySelector("#frames");
captureMany(video, canvas, [1, 5.5, 12])
.then((results) => {
for (const { seconds, blob } of results) {
const url = URL.createObjectURL(blob);
const figure = document.createElement("figure");
const image = document.createElement("img");
const link = document.createElement("a");
const caption = document.createElement("figcaption");
image.src = url;
image.alt = `Video frame at ${seconds} seconds`;
link.href = url;
link.download = `frame-${seconds}.png`;
link.textContent = "Download PNG";
caption.textContent = `Frame at ${seconds}s`;
figure.append(image, caption, link);
gallery.append(figure);
}
})
.catch((error) => console.error("Frame capture failed:", error));
</script>
The event waiter includes a timeout and media error handling so an unavailable seek does not leave the operation waiting forever. The no-seek path can occur when the playhead is already at the requested position. In production code, manage event listeners explicitly if your application allows concurrent operations on the same video element; this example is designed for a single serial capture loop.
For strict cleanup, use an event-wait helper that can cancel its listener when the seek does not occur. Do not fire multiple currentTime assignments concurrently: a simple seek listener can otherwise resolve against the wrong requested timestamp.
Do these 3 things before closing this tab:
1Fix the driver behind crashes, sound loss and screen glitches2Clear out junk files and repair common Windows errors3Scan for outdated or missing drivers - takes under a minuteWait for data and choose output dimensions
Metadata versus frame data
loadedmetadata means the browser has metadata such as duration and dimensions. The media readiness states distinguish this from HAVE_CURRENT_DATA, which indicates that data for the current position is available. The loadeddata event often signals that the current frame has loaded, but MDN notes it may not fire on mobile or tablet devices when data saver is enabled. Do not make that event your only readiness mechanism. See MDN’s readyState reference.
Rank #2
Native resolution and scaling
Set the canvas width and height from video.videoWidth and video.videoHeight after metadata is ready. These are the video’s intrinsic dimensions. If you deliberately want a smaller thumbnail or a different aspect ratio, set canvas dimensions to your desired output size and draw into those dimensions; that scales the source frame and may change its proportions if the ratios differ.
Frame callback support
requestVideoFrameCallback() can provide a frame-aware hook before drawing. MDN labels it Baseline 2024 on the reviewed page and notes availability across latest browser versions since October 2024, while warning that older devices or browsers may not support it. Detect it with "requestVideoFrameCallback" in video and fall back to a tested seeked-based flow where needed. MDN also cautions that the callback is not guaranteed to remain strictly synchronized with the video frame rate, so validate the actual seek-and-capture behavior on the browser and media formats you support. See MDN’s requestVideoFrameCallback reference.
Validate timestamps and handle unusual timelines
A timestamp is a number of seconds, not a frame number. A requested time can be unavailable or land at a nearby supported point rather than an exact frame. This matters especially for compressed video, variable frame rates, and media with sparse keyframes: the browser’s seek behavior depends on the media timeline and available data.
- Finite, on-demand video: reject negative values and values beyond a finite
video.duration. Check theseekableranges too; a known duration does not prove that every portion has been downloaded or is seekable. - Live streams: duration can be unknown or effectively unbounded, and the seekable window can move. Check the current
video.seekableranges before each capture. A segment that has expired may no longer be available. - Non-zero timeline start: do not assume every media timeline begins at zero. Base validation on the actual seekable ranges rather than a hard-coded minimum.
- Duplicate or nearby times: serial capture still works, but duplicates may produce the same image. If avoiding redundant output matters, normalize and deduplicate the requested times before processing.
- Large batches: keep the loop sequential and limit the number of frames. Encoding many full-resolution images can consume substantial memory; this is an engineering concern, not a fixed browser limit.
When the requested timestamp falls outside the ranges reported by seekable, report that clearly or skip it according to the product’s needs. Do not silently label a nearby frame as the exact requested frame.
Export images and build a gallery
canvas.toBlob() is usually the better choice for downloadable image files. It returns binary image data that can be held or downloaded without creating a large encoded string. To show or download it, create an object URL with URL.createObjectURL(blob). Revoke URLs with URL.revokeObjectURL(url) when the gallery is removed or the images are no longer needed.
For a gallery, store each Blob alongside the requested timestamp. Keep the caption explicit about the requested time; if the application needs to distinguish it from the actual resulting video.currentTime, record both. toDataURL() can be convenient for a small preview, but it creates an encoded string in memory and is subject to the same canvas security restrictions as toBlob().
PNG is a straightforward lossless choice. Canvas encoders also support other formats depending on the browser; if you request a format the browser does not support, behavior may fall back to PNG. Check the resulting Blob’s type if the exact output format matters.
Cross-origin video and canvas security
A video from another origin may play in the page yet still be blocked from canvas export. Drawing cross-origin pixels without the server’s CORS approval taints the canvas. A tainted canvas blocks readback operations such as getImageData(), toBlob(), and toDataURL(), which can fail with a SecurityError.
Rank #4
- Set the video’s
crossoriginattribute (orcrossOriginproperty) before assigning or loading its source. Useanonymousfor ordinary CORS access; credentialed requests require an appropriately configured server. - Ensure the media host returns CORS headers permitting the page’s origin.
- Reload the media after changing the CORS setting, then draw and export again.
For example, use <video crossorigin="anonymous" src="https://media.example/video.mp4">, provided that host is configured to allow your page’s origin. Adding the attribute alone cannot grant permission. Client-side JavaScript cannot override the remote server’s CORS policy; only use a same-origin proxy or host configuration when you control and are entitled to serve the media. See MDN’s guide to cross-origin images in canvas and MDN’s crossOrigin reference.
Troubleshooting common failures
| Symptom | Likely cause | What to check or change |
|---|---|---|
Canvas export throws SecurityError |
The video was drawn from another origin without successful CORS approval, tainting the canvas. | Set crossorigin="anonymous" before loading and configure the media server’s CORS response. A JavaScript change alone cannot fix missing server permission. |
| Black, blank, or stale image | The frame was drawn before the seek completed or before current-position data was ready. | Wait for seeked; check video.seeking and video.readyState; test a frame-aware callback where supported. |
| Capture promise never resolves | The target cannot be sought, a media error occurred, or the listener missed its event. | Install the listener before setting currentTime, handle the media error event, add a timeout, and validate the target against current seekable ranges. |
| “Video dimensions are not available” | Metadata has not loaded, or the media has no usable video dimensions. | Wait for loadedmetadata, confirm video.videoWidth and video.videoHeight are nonzero, and check the source actually contains video. |
| Requested time is rejected or produces a nearby frame | The time is outside the duration or seekable range, or the encoding/browser does not provide exact arbitrary-frame seeking. | Check the current range and actual video.currentTime after seeking. Explain approximation rather than claiming frame-level precision. |
Canvas encoding returns null |
The browser could not encode the canvas output. | Confirm the canvas has nonzero dimensions and try a supported type such as image/png; handle the null result as an error. |
| It works on desktop but not some mobile devices | Browser support may differ, or data saver can affect media readiness events. | Feature-detect requestVideoFrameCallback(), avoid relying only on loadeddata, and test the target browser/device combination. |
Performance, reliability, and cost considerations
This method performs capture in the visitor’s browser, so the page must load and decode the media and the browser must be able to seek to each requested point. Total time varies with source delivery, decoding, seek distance, and image encoding; there is no single reliable duration for all videos. Sequential processing is slower than issuing overlapping seeks, but it keeps each result associated with the intended timestamp and avoids races on one video element.
- Capture only the frames the feature needs, especially at intrinsic video resolution.
- Use Blob output and release object URLs and references when they are no longer needed.
- Add per-seek timeouts and clear error reporting for media failures or unavailable ranges.
- Test representative formats, browsers, devices, and CORS configurations; seek precision and readiness vary with the media.
- For very large or unattended batch jobs, consider whether client-side capture is suitable for the user’s device, network, and memory limits.
Or skip the browser setup
If you need a screenshot of a web page rather than frames extracted from a video, ScreenshotNeo provides a website screenshot API and MCP server for developers. A single GET request returns a PNG, JPEG, WebP, or PDF capture. It is not a substitute for timestamped video-frame extraction: use the canvas method above when you need frames from an HTML5 video.
Quick wins for a faster PC:
Clear out junk files and repair common Windows errorsFree Scan →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Example cURL request (see the ScreenshotNeo API documentation for request options):
Best Value
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
ScreenshotNeo accepts cookie or consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; those steps can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and responses identify the page verdict and billing status in headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The free plan includes 1,000 shots per month without a card; paid plans start at $5 for 3,000 shots.
Sign up for 1,000 free screenshots a month, with no card required.
Frequently Asked Questions
Does this method capture every frame between two timestamps?
No. It captures only the times in the timestamp list. To sample a range, generate the desired time values and pass them to the same serial capture loop.
Windows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallCrashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteCan JavaScript guarantee an exact frame at an arbitrary time?
No. The browser seeks on the media timeline, and codec, browser, and available media data can affect the resulting position and frame.
Can I save the captured frames as JPEG or WebP instead of PNG?
Often, yes. Request the format in canvas.toBlob() and check the returned Blob type because supported encodings vary by browser.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

