first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,91 @@
# `ThumbnailGenerator`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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
```ts
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
```ts
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.