Files
2026-05-27 23:29:53 +01:00

14 KiB

Capability Spec Delta: video (video)

Package: @suaveplan/video Status: PROPOSED

Every requirement below carries a **Verified by:** line at authoring time.

ADDED Requirements

Requirement: Universal-tier package layout

The package SHALL ship at packages/media/video/video/ under the media super-category with the video plugin folder, at version 0.1.0, type: module, sideEffects: false, and feature-module subpath exports per the §6 layout rule.

Scenario: Subpath exports are tree-shakeable

  • WHEN a consumer imports @suaveplan/video/detection/detection
  • THEN only the detectVideoFormat module entry is loaded — root index is not pulled in

Verified by: packages/media/video/video/package.json exports map AND bunx turbo build --filter=@suaveplan/video produces one .js per feature under dist/.


Requirement: Error code registry

The package SHALL expose a stable VIDEO_CODES registry minted via defineCodes("VIDEO", [...]) covering every upstream failure mode plus CONFIG_INVALID. A typed VIDEO_ERROR_METADATA table SHALL map each code to its httpStatus, severity, retryable, userMessageKey, category, and description. registerVideoErrorCodes() SHALL remain callable and return the metadata table.

Scenario: Every registry entry has metadata

  • WHEN a consumer iterates VIDEO_CODES keys
  • THEN each key has a corresponding entry in VIDEO_ERROR_METADATA with all six metadata fields populated

Scenario: Codes are namespaced

  • WHEN a consumer reads VIDEO_CODES.DECODE_FAILED
  • THEN the value is "VIDEO.DECODE_FAILED" (genesis defineCodes namespacing convention)

Verified by: packages/media/video/video/src/error-codes/error-codes.test.ts::"VIDEO_CODES covers every upstream failure mode" AND packages/media/video/video/src/error-codes/error-codes.test.ts::"VIDEO_ERROR_METADATA mirrors VIDEO_CODES".


Requirement: Typed VideoError

The package SHALL expose VideoError extends SuaveplanError<VideoCode> with an optional context?: VideoErrorContext field carrying upstream-compatible hints (format, codec, duration, width, height, fileSize, maxSize, operationMs). The 15 upstream factory functions (unknownVideoFormat, invalidVideoDimensions, invalidTrimRange, videoMetadataParseFailed, invalidVideoTransform, durationExceeded, unsupportedCodec, videoNotFound, videoDecodeFailed, videoEncodeFailed, videoFormatUnsupported, videoTooLarge, videoProcessingFailed, videoCodecUnsupported, videoTimeout) SHALL be exported with matching signatures.

Scenario: Error carries code, message, cause, context

  • WHEN videoMetadataParseFailed(new Error("boom")) is called
  • THEN the returned VideoError has code === "VIDEO.METADATA_PARSE_FAILED", cause is the wrapped Error, context is defined (possibly empty), and message starts with "Failed to parse video metadata"

Verified by: packages/media/video/video/src/errors/errors.test.ts::"VideoError carries code, message, cause, and context" AND packages/media/video/video/src/errors/errors.test.ts::"every upstream factory is exported with matching shape".


Requirement: Format detection from magic bytes

detectVideoFormat(header: Uint8Array) SHALL return one of "mp4", "mov", "mkv", "avi", "flv", "ogg" when the header matches the corresponding magic-byte pattern, and null when no pattern matches or the buffer is shorter than 12 bytes.

Scenario: MP4 ftyp branch

  • WHEN detectVideoFormat receives bytes with ftyp at offset 4 and isom brand at offset 8
  • THEN the return value is "mp4"

Scenario: MOV qt brand branch

  • WHEN the header carries ftyp at offset 4 and brand qt at offset 8
  • THEN the return value is "mov"

Scenario: Short buffer

  • WHEN the header is fewer than 12 bytes
  • THEN the return value is null

Verified by: packages/media/video/video/src/detection/detection.test.ts::"detects MP4 (ftyp)", packages/media/video/video/src/detection/detection.test.ts::"detects MOV (qt brand)", packages/media/video/video/src/detection/detection.test.ts::"returns null for short header".


Requirement: Resolution math

calculateVideoResize(original, options) SHALL honor preset dimensions verbatim, otherwise interpret width/height against maintainAspectRatio (default true), and SHALL always round both output dimensions up to the next even number.

Scenario: Preset uses canonical dimensions

  • WHEN calculateVideoResize({width: 1920, height: 1080}, { preset: "720p" }) runs
  • THEN the result is { width: 1280, height: 720 }

Scenario: Even-dimension rounding

  • WHEN the computed width is odd (e.g. caller asks for width: 641)
  • THEN the returned width is width + 1 so both dimensions are even

Verified by: packages/media/video/video/src/resize/resize.test.ts::"uses preset dimensions" AND packages/media/video/video/src/resize/resize.test.ts::"ensures even dimensions".


Requirement: Trim range validation

validateTrimRange(duration, range) SHALL return true iff `range.startTime

= 0, range.endTime > range.startTime, and range.endTime <= duration`.

Scenario: Negative start

  • WHEN the range has startTime: -1
  • THEN the return value is false

Scenario: End beyond duration

  • WHEN the range has endTime > duration
  • THEN the return value is false

Verified by: packages/media/video/video/src/trim/trim.test.ts::"rejects negative start" AND packages/media/video/video/src/trim/trim.test.ts::"rejects end beyond duration".


Requirement: Duration round-trip

formatDuration(seconds) SHALL render HH:MM:SS with zero-padded fields and parseDuration SHALL accept SS, MM:SS, or HH:MM:SS and produce a seconds total. The two functions SHALL be exact inverses on integer-second inputs.

Scenario: Round-trip

  • WHEN parseDuration(formatDuration(3661)) is evaluated
  • THEN the result is 3661

Verified by: packages/media/video/video/src/duration/duration.test.ts::"round-trips with formatDuration".


Requirement: Format-to-extension mapping

videoFormatToExtension(format) SHALL return ".<format>" for every member of VideoFormat.

Scenario: mp4 maps to .mp4

  • WHEN videoFormatToExtension("mp4") is called
  • THEN the return value is ".mp4"

Verified by: packages/media/video/video/src/format/format.test.ts::"mp4 -> .mp4".


Requirement: Public Zod schemas

The package SHALL expose Zod schemas for every public type (VideoFormatSchema, VideoCodecSchema, AudioCodecSchema, ResolutionPresetSchema, VideoDimensionsSchema, VideoMetadataSchema, TrimRangeSchema, VideoResizeOptionsSchema, VideoTransformOpSchema (discriminated union over 9 op types), VideoTransformPipelineSchema, VideoTransformResultSchema, VideoProcessorConfigSchema). VideoTransformOpSchema SHALL accept and reject by type discriminant.

Scenario: Discriminated union rejects unknown op type

  • WHEN VideoTransformOpSchema.safeParse({ type: "explode" }) runs
  • THEN result.success === false

Scenario: Resize op accepts preset

  • WHEN VideoTransformOpSchema.parse({ type: "resize", options: { preset: "1080p" } }) runs
  • THEN the parse succeeds and the typed result is the same shape

Verified by: packages/media/video/video/src/types/video-types/video-types.test.ts::"VideoTransformOpSchema rejects unknown discriminant" AND packages/media/video/video/src/types/video-types/video-types.test.ts::"VideoTransformOpSchema parses every variant".


Requirement: Constants

RESOLUTION_PRESETS SHALL expose six presets (360p, 480p, 720p, 1080p, 1440p, 4k) each with width/height rounded to canonical dimensions. VIDEO_MIME_TYPES SHALL expose a MIME string for each of the eight VideoFormat members.

Scenario: 4k is 3840x2160

  • WHEN RESOLUTION_PRESETS["4k"] is read
  • THEN the entry is { width: 3840, height: 2160 }

Verified by: packages/media/video/video/src/types/video-types/video-types.test.ts::"4k is 3840x2160".


Requirement: VideoProcessor.extractMetadata

VideoProcessor.extractMetadata(data: Uint8Array, knownMetadata?: Partial<VideoMetadata>, signal?: AbortSignal) SHALL detect the format from header bytes, merge caller-supplied known fields, default hasAudio to true, default missing numeric fields to 0, record fileSize from data.byteLength, throw unknownVideoFormat() when no format can be determined, throw durationExceeded(...) when the merged duration exceeds the configured maxDuration, throw videoMetadataParseFailed(cause) on any other unexpected error, emit metadata.extracted, and short-circuit with VideoError(VIDEO.PROCESSING_FAILED, cause: AbortError) when signal is already aborted.

Scenario: Format detected from MP4 header

  • WHEN extractMetadata runs against an MP4 ftyp/isom header
  • THEN the returned metadata.format is "mp4"

Scenario: Duration exceeded

  • WHEN new VideoProcessor({ maxDuration: 60 }).extractMetadata(header, { duration: 120 })
  • THEN a VideoError with code VIDEO.DURATION_EXCEEDED is thrown

Scenario: Aborted signal short-circuits

  • WHEN an already-aborted AbortSignal is passed
  • THEN a VideoError with code VIDEO.PROCESSING_FAILED and an AbortError cause is thrown before any work runs

Verified by: packages/media/video/video/src/processor/processor.test.ts::"extracts from MP4 header", packages/media/video/video/src/processor/processor.test.ts::"throws for duration exceeding max", AND packages/media/video/video/src/processor/processor.spec-conformance.test.ts::"aborts extractMetadata before work".


Requirement: VideoProcessor.planTransform

planTransform(metadata, pipeline, signal?) SHALL validate the pipeline via the Zod VideoTransformPipelineInputSchema, walk the operations in order applying the canonical algorithm (resize/trim/rotate/flip/speed/mute/format /codec/bitrate), throw invalidTrimRange for invalid trim, throw invalidVideoTransform for non-90-degree rotation or non-positive speed, emit transform.planned, and short-circuit on an aborted signal.

Scenario: Resize op applies preset

  • WHEN planTransform(meta, { operations: [{ type: "resize", options: { preset: "720p" } }] })
  • THEN the result has width: 1280 and height: 720

Scenario: Non-90 rotation rejected

  • WHEN planTransform(meta, { operations: [{ type: "rotate", degrees: 45 }] })
  • THEN VideoError(VIDEO.INVALID_TRANSFORM) is thrown

Scenario: Speed mutates duration

  • WHEN speed factor: 2 is applied to a 120s metadata
  • THEN the result has duration: 60

Verified by: packages/media/video/video/src/processor/processor.test.ts::"applies resize", packages/media/video/video/src/processor/processor.test.ts::"throws for non-90 rotation", AND packages/media/video/video/src/processor/processor.test.ts::"applies speed factor".


Requirement: VideoPipelineBuilder fluent surface

VideoPipelineBuilder SHALL provide chainable resize, trim, rotate, flip, speed, mute, toFormat, codec, bitrate methods, each returning this. build() SHALL return a frozen-shape VideoTransformPipeline carrying every appended operation. apply(metadata, processor) SHALL run the built pipeline through processor.planTransform.

Scenario: Chained builder retains every op

  • WHEN a builder calls resize().trim().toFormat() then build()
  • THEN the resulting pipeline.operations has length 3 and pipeline.outputFormat is the chosen format

Verified by: packages/media/video/video/src/pipeline-builder/pipeline-builder.test.ts::"build returns pipeline".


Requirement: pipeline.built emission

A VideoPipelineBuilder created via processor.createPipeline() SHALL emit pipeline.built on the processor's bus whenever build() is called.

Scenario: Build event fires

  • WHEN processor.createPipeline().resize({preset:"720p"}).build() is called and a listener is attached to pipeline.built
  • THEN the listener receives the built pipeline

Verified by: packages/media/video/video/src/pipeline-builder/pipeline-builder.test.ts::"emits pipeline.built when bound to a processor".


Requirement: OpenTelemetry instrumentation

The processor SHALL record exactly two counters (video.metadata.extracted_total, video.transform.planned_total) and two histograms (video.metadata.extract_duration_ms, video.transform.plan_duration_ms) on every public-method invocation (success and failure), tagged with an outcome attribute, and SHALL start named spans video.metadata.extract and video.transform.plan that are ended in a finally block.

Scenario: Counter increments on success

  • WHEN extractMetadata succeeds against a registered MeterProvider
  • THEN an entry with name video.metadata.extracted_total, value 1, and attribute outcome: "success" is recorded

Scenario: Counter increments on failure

  • WHEN extractMetadata throws unknownVideoFormat()
  • THEN the counter records an entry with outcome: "unknown_format"

Verified by: packages/media/video/video/src/processor/processor.spec-conformance.test.ts::"increments extracted counter on success" AND packages/media/video/video/src/processor/processor.spec-conformance.test.ts::"records failure outcome on unknown_format".


Requirement: Zod-validated config

new VideoProcessor(config) SHALL run config through VideoProcessorConfigSchema.safeParse; on failure SHALL throw VideoError(VIDEO.CONFIG_INVALID) carrying the Zod error message in the message.

Scenario: Invalid config rejected

  • WHEN new VideoProcessor({ maxDuration: -1 })
  • THEN a VideoError with code VIDEO.CONFIG_INVALID is thrown

Verified by: packages/media/video/video/src/processor/processor.test.ts::"throws CONFIG_INVALID for invalid config".