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,192 @@
# Design: enterprise-video
## Architecture
`@suaveplan/video` is a universal-tier metadata + transform-planning library.
It has no Node-only or browser-only imports in `src/` — every public surface
operates on `Uint8Array` inputs and plain JS objects so the same module ships
to Bun, Node, and browsers. Heavy codec work (real transcoding, frame
extraction, thumbnail render) is delegated to sibling packages
(`@suaveplan/video-thumbnails` ffmpeg renderer, `@suaveplan/react-video`
HTMLVideoElement-backed playback) that consume this package's planning surface.
## Module layout
```
packages/media/video/video/
package.json
tsconfig.lib.json
vite.config.ts
vitest.config.ts
README.md
tests/setup/polyfills.ts
src/
index.ts # explicit named re-exports
error-codes/{error-codes.ts,error-codes.md,error-codes.test.ts,index.ts}
errors/{errors.ts,errors.md,errors.test.ts,index.ts}
types/video-types/{video-types.ts,video-types.md,video-types.test.ts,index.ts}
detection/{detection.ts,detection.md,detection.test.ts,index.ts}
resize/{resize.ts,resize.md,resize.test.ts,index.ts}
trim/{trim.ts,trim.md,trim.test.ts,index.ts}
duration/{duration.ts,duration.md,duration.test.ts,index.ts}
format/{format.ts,format.md,format.test.ts,index.ts}
instruments/{instruments.ts,instruments.md,index.ts}
processor/{processor.ts,processor.md,processor.test.ts,processor.spec-conformance.test.ts,index.ts}
pipeline-builder/{pipeline-builder.ts,pipeline-builder.md,pipeline-builder.test.ts,index.ts}
```
## Public surface (mirrors upstream)
- Error layer: `VideoError`, 15 factories, `VIDEO_ERROR_METADATA`,
`registerVideoErrorCodes`, `VideoCode` union, `VIDEO_CODES` registry.
- Types layer: every upstream type plus Zod schemas.
- Helpers: `detectVideoFormat`, `calculateVideoResize`, `validateTrimRange`,
`formatDuration`, `parseDuration`, `videoFormatToExtension`.
- Core: `VideoProcessor`, `VideoPipelineBuilder`.
- Constants: `RESOLUTION_PRESETS`, `VIDEO_MIME_TYPES`.
## Genesis-specific changes
1. **Error base**: `SuaveplanError<VideoCode>` (genesis foundation) rather
than the upstream `BaseError`. Public discriminants (`err.code`,
`err.message`, `err.cause`, `err.context`) are identical.
2. **Event bus**: composes `EventBus<TMap>` where `TMap = VideoEventMap` and
payloads are single values (no `[T]` tuples). The processor exposes
`on`/`once`/`off` so call sites read the same. Genesis pattern matches
`@suaveplan/image`.
3. **OTel**: meter `@suaveplan/video` with:
- counter `video.metadata.extracted_total{outcome}`
- histogram `video.metadata.extract_duration_ms{outcome}`
- counter `video.transform.planned_total{outcome}`
- histogram `video.transform.plan_duration_ms{outcome}`
- tracer spans `video.metadata.extract` + `video.transform.plan`.
4. **Validation**: Zod schemas at every public boundary
(`VideoProcessorConfigSchema`, `VideoTransformPipelineInputSchema`).
Invalid input throws `VideoError(VIDEO.CONFIG_INVALID)` /
`VideoError(VIDEO.INVALID_TRANSFORM)`.
5. **AbortSignal**: both `extractMetadata` and `planTransform` accept an
optional `AbortSignal` and short-circuit with
`VideoError(VIDEO.PROCESSING_FAILED, cause: AbortError)` when aborted.
6. **`pipeline.built` event**: declared upstream but never emitted; genesis
fires it from `VideoPipelineBuilder.build()` whenever the builder was
created via `VideoProcessor.createPipeline()` (so the builder has a bus
reference).
## Algorithm notes
### `detectVideoFormat`
Byte-for-byte port of the upstream sniffer (header must be ≥12 bytes):
- bytes 4-7 = `ftyp` ASCII → MP4 family; sub-brand bytes 8-11 starting `qt`
return `mov`, else `mp4`.
- bytes 0-3 = `1A 45 DF A3` → `mkv` (Matroska EBML).
- bytes 0-3 = `RIFF`, bytes 8-11 = `AVI ` → `avi`.
- bytes 0-2 = `FLV` → `flv`.
- bytes 0-3 = `OggS` → `ogg`.
- else `null`.
### `calculateVideoResize`
Identical to upstream:
- if `preset` set: use preset dimensions verbatim.
- else if `maintainAspectRatio !== false` and exactly one dimension set:
compute the other from `original.width / original.height`.
- else: use options as-is (or `original` when both omitted).
- finally: round both dims up to the next even number (`x % 2 === 0 ? x : x+1`)
because most codecs require even chroma sub-sampling alignment.
### `validateTrimRange`
`start ≥ 0 && end > start && end ≤ duration`.
### `formatDuration` / `parseDuration`
`HH:MM:SS` ↔ seconds. `parseDuration` accepts `SS`, `MM:SS`, or `HH:MM:SS`
and silently returns `0` for empty string.
### `VideoProcessor.extractMetadata`
1. AbortSignal pre-check.
2. Start OTel span + timer.
3. `detectVideoFormat(data)`; if `null` and no `knownMetadata.format`,
throw `unknownVideoFormat()` (outcome=`unknown_format`).
4. Compose `VideoMetadata` from `knownMetadata` + detected format,
defaulting numeric fields to 0 and `hasAudio` to `true`.
5. If `metadata.duration > maxDuration`, throw `durationExceeded(...)`
(outcome=`duration_exceeded`).
6. Emit `metadata.extracted`.
7. On any non-`VideoError` exception: wrap in `videoMetadataParseFailed(cause)`.
8. Finally: record counter + histogram + end span.
### `VideoProcessor.planTransform`
1. AbortSignal pre-check.
2. Start OTel span + timer.
3. Validate pipeline via `VideoTransformPipelineInputSchema`.
4. Walk operations in order, mutating local `(width, height, duration,
format, hasAudio)`:
- `resize` → `calculateVideoResize`.
- `trim` → `validateTrimRange` else `invalidTrimRange`; new duration.
- `rotate` → require `degrees % 90 === 0` else `invalidVideoTransform`;
swap dims when `degrees % 180 !== 0`.
- `flip` → no-op for planning.
- `speed` → require `factor > 0` else `invalidVideoTransform`;
`duration /= factor`.
- `mute` → `hasAudio = false`.
- `format` → set output format.
- `codec` / `bitrate` → no-op for planning (informational only).
5. Emit `transform.planned`.
6. Return `VideoTransformResult`.
## Error code registry
```ts
export const VIDEO_CODES = defineCodes("VIDEO", [
"UNKNOWN_FORMAT",
"INVALID_DIMENSIONS",
"INVALID_TRIM_RANGE",
"METADATA_PARSE_FAILED",
"INVALID_TRANSFORM",
"DURATION_EXCEEDED",
"UNSUPPORTED_CODEC",
"NOT_FOUND",
"DECODE_FAILED",
"ENCODE_FAILED",
"FORMAT_UNSUPPORTED",
"TOO_LARGE",
"PROCESSING_FAILED",
"CODEC_UNSUPPORTED",
"TIMEOUT",
"CONFIG_INVALID",
] as const);
```
`VIDEO_ERROR_METADATA` is a separately-exported `Record<VideoCode,
VideoCodeMetadata>` (httpStatus, severity, retryable, userMessageKey,
category, description) — used by HTTP error mappers and retry policies.
`registerVideoErrorCodes()` returns it (no-op for compatibility).
## File-size budget
Every implementation file targets ≤250 lines (300 hard). The processor splits
naturally into:
- `processor/processor.ts` (orchestration ~200 lines)
- `pipeline-builder/pipeline-builder.ts` (fluent builder ~110 lines)
- `instruments/instruments.ts` (~60 lines)
- helpers each ≤60 lines.
## Test strategy
- **Helper tests** (detection, resize, trim, duration, format) — table-driven
byte-pattern checks + boundary cases.
- **Errors tests** — every factory exercised; `VideoError` instance/code/context/
cause; `VIDEO_ERROR_METADATA` table shape.
- **Processor tests** — every branch of `extractMetadata` + `planTransform`
(including each `VideoTransformOp` variant + abort + zod-config invalid).
- **Pipeline builder tests** — every fluent method + `build()` + `apply()` +
`pipeline.built` emission when bound to a processor.
- **Spec conformance** — OTel counter/histogram/span observation + AbortSignal
propagation + event emission end-to-end via in-test MeterProvider /
TracerProvider.
100/100/100/100 istanbul coverage is mandatory.