Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,125 @@
# 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<boolean>;
extract(input: ExtractInput, signal?: AbortSignal): Promise<void>;
tile(input: TileInput, signal?: AbortSignal): Promise<void>;
probe(inputPath: string, signal?: AbortSignal): Promise<ProbeResult>;
}
class ThumbnailGenerator {
constructor(config: ThumbnailGeneratorConfig, runner?: FfmpegRunner);
generate(input, options?, signal?): Promise<ThumbnailResult>;
generateMultiple(input, timestamps, signal?): Promise<ThumbnailResult[]>;
extractSlidingWindow(input, count, durationSeconds, signal?): Promise<ThumbnailResult[]>;
getMetadata(input, signal?): Promise<VideoMetadata>;
isAvailable(signal?): Promise<boolean>;
}
class SpriteSheetGenerator {
constructor(config?: SpriteSheetConfig, runner?: FfmpegRunner);
generate(input, outputPath, duration?, signal?): Promise<SpriteSheetResult>;
}
```
## 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<VideoThumbnailsCode>`). 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/<feature>/` (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.