Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,91 @@
# `runner`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The runner module defines the `FfmpegRunner` interface plus a handful of pure helpers used by
every implementation: `buildScaleFilter`, `buildQualityArgs`, and `classifyRunnerError`.
Centralising filter construction means the generator classes stay focused on orchestration
while every implementation (the bundled `FluentFfmpegRunner` and any future native or browser
variant) shares the exact same algorithmic shape. `classifyRunnerError` is the single point
where an `AbortSignal` distinguishes an `ABORTED` failure from a real ffmpeg crash.
## Features
- `FfmpegRunner` interface — four methods (`isAvailable`, `extract`, `tile`, `probe`), each
taking an optional `AbortSignal`.
- Strong typed payloads: `ExtractInput`, `TileInput`, `ProbeResult`.
- `buildScaleFilter` — encodes the `cover | contain | fill` fit semantics into ffmpeg
filter-graph syntax once and only once.
- `buildQualityArgs` — translates the 1–100 quality scale into ffmpeg's inverted 1–31 `-q:v`
scale (PNG returns an empty arg list).
- `classifyRunnerError` — single guarded helper that turns any raw ffmpeg failure into a
typed `VideoThumbnailsError`, distinguishing `ABORTED` from `EXTRACT_FAILED` /
`PROBE_FAILED` based on the supplied `AbortSignal`.
## Usage: Basic
```ts
import { buildScaleFilter, buildQualityArgs } from "@suaveplan/video-thumbnails/runner";
buildScaleFilter(320, 180, "cover");
// "scale=320:180:force_original_aspect_ratio=increase,crop=320:180"
buildQualityArgs("jpg", 80);
// ["-q:v", "7"]
```
## Usage: Advanced
```ts
import {
classifyRunnerError,
type FfmpegRunner,
} from "@suaveplan/video-thumbnails/runner";
import { VIDEO_THUMBNAILS_CODES } from "@suaveplan/video-thumbnails/error-codes";
class CustomRunner implements FfmpegRunner {
async extract(input, signal) {
try {
await runViaCustomTransport(input, signal);
} catch (error) {
throw classifyRunnerError(
error,
signal,
VIDEO_THUMBNAILS_CODES.EXTRACT_FAILED,
"videoThumbnails.errors.extractFailed",
);
}
}
// ... isAvailable, tile, probe
}
```
## API Reference
- `FfmpegRunner` — `{ isAvailable(signal?), extract(input, signal?), tile(input, signal?),
probe(inputPath, signal?) }`.
- `ExtractInput` — `{ inputPath, outputPath, timestamp, width, height, fit, format,
quality }`.
- `TileInput` — `{ inputPath, outputPath, columns, rows, thumbnailWidth, thumbnailHeight,
interval, format }`.
- `ProbeResult` — `{ duration, width, height, codec, fps, bitrate }`.
- `buildScaleFilter(width, height, fit): string`.
- `buildQualityArgs(format, quality): readonly string[]`.
- `classifyRunnerError(cause, signal, failureCode, failureKey): VideoThumbnailsError` — when
`signal.aborted`, returns `ABORTED`; otherwise the supplied `failureCode`.
## Implementation Notes
Quality mapping uses ffmpeg's inverted scale where `1` is best and `31` is worst — the
inverse arithmetic (`31 - quality/100 × 30`) keeps the round-trip stable for the canonical
80% quality (returning `-q:v 7`, the most commonly cited "good MJPEG" value). The fit
strategies are deliberately limited to ffmpeg-native filter chains so no second-pass image
processing is needed; consumers wanting more exotic compositing (gravity, focal point) can
implement a custom `FfmpegRunner` and reuse `buildQualityArgs` while ignoring
`buildScaleFilter`. `classifyRunnerError` reads `signal.aborted` lazily and only after the
underlying call has thrown — this avoids a race where the signal aborts after the work
already finished successfully.