Files
skills/docs/media/video/video-thumbnails/src/thumbnail-generator/thumbnail-generator.md
2026-05-29 08:20:56 +01:00

3.7 KiB

ThumbnailGenerator

(no submodules)

Purpose

ThumbnailGenerator is the primary class consumers reach for to extract still frames from a video file. It accepts a Zod-validated ThumbnailConfig (output directory, dimensions, format, quality) and delegates the actual ffmpeg invocation to an injectable FfmpegRunner. The default runner — FluentFfmpegRunner — bundles ffmpeg/ffprobe binaries so the package runs unmodified on any host. Every public method honours an AbortSignal, validates input at the boundary, and records OTel histograms + counters keyed by format, fit, and outcome.

Features

  • Single-frame extract at any HH:MM:SS(.mmm)? timestamp (generate).
  • Multi-timestamp batch with abort-aware iteration (generateMultiple).
  • Sliding-window extraction — N evenly-spaced frames across a duration (extractSlidingWindow).
  • ffprobe-backed metadata (getMetadata).
  • Runner-availability probe (isAvailable).
  • Zod boundary validation with code-specific failures (INVALID_DIMENSIONS, INVALID_TIMESTAMP, FORMAT_UNSUPPORTED, INVALID_OPTIONS).
  • OTel: video_thumbnails.extract.duration_ms{format,fit,outcome} + video_thumbnails.extract.total{outcome,format}.
  • AbortSignal propagated through every call into the runner; aborts surface as VIDEO_THUMBNAILS.ABORTED.

Usage: Basic

import { ThumbnailGenerator } from "@suaveplan/video-thumbnails/thumbnail-generator";

const generator = new ThumbnailGenerator({ outputDir: "/tmp/thumbs" });
const result = await generator.generate("/data/clip.mp4");
console.log(result.outputPath, result.sizeBytes);

Usage: Advanced

import { ThumbnailGenerator } from "@suaveplan/video-thumbnails/thumbnail-generator";
import { FluentFfmpegRunner } from "@suaveplan/video-thumbnails/runner";

const generator = new ThumbnailGenerator(
    {
        outputDir: "/tmp/thumbs",
        width: 640,
        height: 360,
        format: "webp",
        quality: 90,
    },
    new FluentFfmpegRunner({ ffmpegPath: "/opt/ffmpeg/bin/ffmpeg" }),
);
const controller = new AbortController();
const metadata = await generator.getMetadata("/data/clip.mp4");
const frames = await generator.extractSlidingWindow(
    "/data/clip.mp4",
    8,
    metadata.duration,
    controller.signal,
);

API Reference

  • class ThumbnailGenerator
    • constructor(config: ThumbnailConfigInput, runner?: FfmpegRunner)
    • generate(inputPath, options?, signal?): Promise<ThumbnailResult>
    • generateMultiple(inputPath, timestamps, signal?): Promise<ThumbnailResult[]>
    • extractSlidingWindow(inputPath, count, durationSeconds, signal?): Promise<ThumbnailResult[]>
    • getMetadata(inputPath, signal?): Promise<VideoMetadata>
    • isAvailable(signal?): Promise<boolean>
  • buildEvenlySpacedTimestamps(count, durationSeconds): string[] — utility re-used by extractSlidingWindow.

Throws VideoThumbnailsError for every failure mode (INVALID_*, FORMAT_UNSUPPORTED, EXTRACT_FAILED, PROBE_FAILED, VIDEO_INVALID, ABORTED).

Implementation Notes

extractSlidingWindow divides the duration into count + 1 slots and picks the boundary at each interior slot — i.e. for count = 4, duration = 5s the timestamps are 1.0s, 2.0s, 3.0s, 4.0s. This avoids the degenerate case of putting a frame at 0.0 (often a black intro frame) or at the exact duration (frequently past the last decodable frame). The generator never writes files itself — stat() reads back the file ffmpeg produced so the returned sizeBytes reflects the actual on-disk size, not the requested dimensions multiplied by an assumed bit depth. OTel records before throwing on failure so dashboards distinguish counts from timings.