first commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user