Initial Commit
This commit is contained in:
192
openspec/archive/2026-05-25-enterprise-video/design.md
Normal file
192
openspec/archive/2026-05-25-enterprise-video/design.md
Normal 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.
|
||||
Reference in New Issue
Block a user