# Design — enterprise-video-thumbnails > Non-normative. Architecture, file layout, rationale. ## Architecture The package is split into four feature modules under `src/`: ```text packages/media/video/video-thumbnails/src/ index.ts # package root barrel (named re-exports) error-codes/ index.ts error-codes.ts # defineCodes("VIDEO_THUMBNAILS", [...]) error-codes.md errors/ index.ts video-thumbnails-error.ts # VideoThumbnailsError extends SuaveplanError video-thumbnails-error.md schemas/ index.ts schemas.ts # Zod schemas for every option type schemas.md schemas.types.ts # inferred TS types telemetry/ index.ts telemetry.ts # meter + counters/histograms factory telemetry.md runner/ index.ts runner.ts # FfmpegRunner interface + helpers runner.md runner.types.ts fluent-ffmpeg-runner.ts # default impl using fluent-ffmpeg + @ffmpeg-installer fluent-ffmpeg-runner.md thumbnail-generator/ index.ts thumbnail-generator.ts # ThumbnailGenerator class thumbnail-generator.md thumbnail-generator.types.ts sprite-sheet/ index.ts sprite-sheet.ts # SpriteSheetGenerator class sprite-sheet.md sprite-sheet.types.ts ``` ## Key types ```ts interface FfmpegRunner { isAvailable(signal?: AbortSignal): Promise; extract(input: ExtractInput, signal?: AbortSignal): Promise; tile(input: TileInput, signal?: AbortSignal): Promise; probe(inputPath: string, signal?: AbortSignal): Promise; } class ThumbnailGenerator { constructor(config: ThumbnailGeneratorConfig, runner?: FfmpegRunner); generate(input, options?, signal?): Promise; generateMultiple(input, timestamps, signal?): Promise; extractSlidingWindow(input, count, durationSeconds, signal?): Promise; getMetadata(input, signal?): Promise; isAvailable(signal?): Promise; } class SpriteSheetGenerator { constructor(config?: SpriteSheetConfig, runner?: FfmpegRunner); generate(input, outputPath, duration?, signal?): Promise; } ``` ## Default runner `FluentFfmpegRunner` wraps `fluent-ffmpeg`, pinning the binary to `require("@ffmpeg-installer/ffmpeg").path` so consumers do not need ffmpeg on PATH. `extract`, `tile`, `probe` translate the typed call into a fluent-ffmpeg chain and resolve when the command emits `end`/`error`. The runner registers an `abort` listener on the supplied `AbortSignal` and calls `command.kill("SIGKILL")` on trigger, rejecting with `VIDEO_THUMBNAILS.ABORTED`. ## Error model All public methods throw `VideoThumbnailsError` (extends `SuaveplanError`). Codes: | Code | When raised | |---|---| | `VIDEO_THUMBNAILS.EXTRACT_FAILED` | ffmpeg exited non-zero on a single-frame extract or sprite render. | | `VIDEO_THUMBNAILS.PROBE_FAILED` | ffprobe failed or produced unparseable JSON. | | `VIDEO_THUMBNAILS.VIDEO_INVALID` | Probe succeeded but found no video stream or duration ≤ 0. | | `VIDEO_THUMBNAILS.INVALID_TIMESTAMP` | Caller supplied a timestamp string that does not match `HH:MM:SS(.mmm)?`. | | `VIDEO_THUMBNAILS.INVALID_DIMENSIONS` | Width or height ≤ 0, or `count` for sliding-window ≤ 0. | | `VIDEO_THUMBNAILS.INVALID_OPTIONS` | Zod boundary validation failed for `ThumbnailConfig`/`ThumbnailOptions`/`SpriteSheetConfig`. | | `VIDEO_THUMBNAILS.FORMAT_UNSUPPORTED` | Output format not in the supported set. | | `VIDEO_THUMBNAILS.RUNNER_UNAVAILABLE` | `runner.isAvailable()` returned `false` and a guard was requested. | | `VIDEO_THUMBNAILS.ABORTED` | `AbortSignal` triggered mid-operation. | ## Observability A package-scoped meter (`@suaveplan/video-thumbnails`) emits: - `video_thumbnails.extract.duration_ms` (histogram, attrs `{format, fit, outcome}`) - `video_thumbnails.extract.total` (counter, attrs `{outcome, format}`) - `video_thumbnails.sprite_sheet.duration_ms` (histogram, attrs `{outcome, format}`) - `video_thumbnails.sprite_sheet.frames` (histogram, attrs `{format}`) The meter is lazily fetched per call via `metrics.getMeter` so the package retains `sideEffects: false` and tree-shakes when callers do not import the generator entry. ## Testing strategy A `tests/setup/fixtures.ts` helper synthesises a 5-second 320×240 colour-gradient MP4 once per worker using ffmpeg's `lavfi` source (`-f lavfi -i color=c=red:size=320x240:rate=10:duration=5`). The path is cached and reused across tests. Tests cover: - Real single-frame extraction at 0s, mid, near-end for each format. - `generateMultiple` over three timestamps. - `extractSlidingWindow` returns exactly N evenly-spaced frames. - `getMetadata` returns duration ≈ 5s, width 320, height 240, codec `h264`. - Sprite sheet 3×3 generation against the same fixture. - `AbortSignal` cancels mid-extract → `ABORTED`. - Zod boundary rejects invalid options. - Every error code is reachable from a real call site. No mocks. ffmpeg binary is sourced from `@ffmpeg-installer/ffmpeg`. ## File layout vs. AGENTS rules - All feature modules live under `src//` (no flat `src/`-root impl files). - Every `.ts` (except `index.ts`, `*.types.ts`, `*.test.ts`) has a sibling `.md`. - Barrels are explicit named re-exports. - Test files import from `@suaveplan/testing/runner`. - No `biome-ignore`, no `export *`, no direct `vitest`/`bun:test` imports.