# `runner` _(no submodules)_ ## 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.