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