5.5 KiB
Design — enterprise-video-thumbnails
Non-normative. Architecture, file layout, rationale.
Architecture
The package is split into four feature modules under src/:
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
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.
generateMultipleover three timestamps.extractSlidingWindowreturns exactly N evenly-spaced frames.getMetadatareturns duration ≈ 5s, width 320, height 240, codech264.- Sprite sheet 3×3 generation against the same fixture.
AbortSignalcancels 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 flatsrc/-root impl files). - Every
.ts(exceptindex.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, noexport *, no directvitest/bun:testimports.