Files
suaveplan/openspec/archive/2026-05-25-enterprise-video/design.md
2026-05-27 23:29:53 +01:00

7.7 KiB

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

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.