A file grid full of videos needs poster images, and generating them is where many web apps reach for a server round-trip or a heavyweight bundled codec. Neither fits a local-first tool: the video should never leave the disk, and shipping a multi-megabyte engine to make a 160-pixel thumbnail is overkill.

ZumiFiles grabs a single representative frame using the browser's own media pipeline - a hidden <video> element seeked to a timestamp and painted to a canvas - then caches the result as a small sidecar image so it never has to decode that frame again.

Key Technical Concepts Covered

  • Seek, don't play: jump to one timestamp and decode a single frame.
  • Canvas capture: paint the frame and export a compressed JPEG.
  • Object-URL hygiene: revoke blob URLs so buffers don't leak.
  • Sidecar caching: persist the thumbnail next to the file for instant reloads.

Part of the engineering behind ZumiFiles.

Tested with Chrome 149 (July 2026). The File System Access API is supported in Chromium-based browsers.

Decoding One Frame

You don't need to play a video to see it - setting currentTime and waiting for seeked decodes exactly one frame. ZumiFiles seeks a short way in (to skip black intro frames) and draws that frame to a canvas:

async function grabFrame(file, atSeconds = 1) {
  const url = URL.createObjectURL(file);
  const video = document.createElement('video');
  video.muted = true; video.src = url;

  await new Promise((res) => (video.onloadedmetadata = res));
  video.currentTime = Math.min(atSeconds, video.duration / 2);
  await new Promise((res) => (video.onseeked = res));

  const w = 320, h = Math.round(320 * (video.videoHeight / video.videoWidth));
  // OffscreenCanvas gives us convertToBlob(); a DOM canvas would use toBlob().
  const canvas = new OffscreenCanvas(w, h);
  canvas.getContext('2d').drawImage(video, 0, 0, w, h);

  URL.revokeObjectURL(url);   // release the decoded buffer
  return canvas.convertToBlob({ type: 'image/jpeg', quality: 0.8 });
}

Caching as a Sidecar

Decoding is cheap but not free, so ZumiFiles writes the thumbnail once to a dot-prefixed sidecar image beside the video. On the next scan it loads that file instantly instead of decoding again - the same sidecar approach it uses for metadata:

async function cacheThumbnail(dirHandle, fileName, blob) {
  const thumbName = `.${fileName}.thumbnail.jpg`;
  const handle = await dirHandle.getFileHandle(thumbName, { create: true });
  const writable = await handle.createWritable();
  await writable.write(blob);
  await writable.close();
}

When a persisted thumbnail already exists, the grid skips decoding entirely and streams the cached image - which is also how ZumiFiles shows posters for formats a browser can't decode live, like RAW photos and PSDs.

Conclusion: The Browser Already Has a Decoder

Between a hidden video element, a canvas, and a sidecar cache, ZumiFiles produces video posters with no server and no bundled codec. Seek one frame, paint it, revoke the URL, and cache the result - a full thumbnailing pipeline in a few dozen lines, running entirely on the user's machine.