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
detectVideoFormatmodule entry is loaded — rootindexis 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_CODESkeys - THEN each key has a corresponding entry in
VIDEO_ERROR_METADATAwith all six metadata fields populated
Scenario: Codes are namespaced
- WHEN a consumer reads
VIDEO_CODES.DECODE_FAILED - THEN the value is
"VIDEO.DECODE_FAILED"(genesisdefineCodesnamespacing 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
VideoErrorhascode === "VIDEO.METADATA_PARSE_FAILED",causeis the wrapped Error,contextis defined (possibly empty), andmessagestarts 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
detectVideoFormatreceives bytes withftypat offset 4 andisombrand at offset 8 - THEN the return value is
"mp4"
Scenario: MOV qt brand branch
- WHEN the header carries
ftypat offset 4 and brandqtat 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 + 1so 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, andrange.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
extractMetadataruns against an MP4ftyp/isomheader - THEN the returned
metadata.formatis"mp4"
Scenario: Duration exceeded
- WHEN
new VideoProcessor({ maxDuration: 60 }).extractMetadata(header, { duration: 120 }) - THEN a
VideoErrorwith codeVIDEO.DURATION_EXCEEDEDis thrown
Scenario: Aborted signal short-circuits
- WHEN an already-aborted
AbortSignalis passed - THEN a
VideoErrorwith codeVIDEO.PROCESSING_FAILEDand anAbortErrorcause 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: 1280andheight: 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: 2is 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()thenbuild() - THEN the resulting
pipeline.operationshas length 3 andpipeline.outputFormatis 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 topipeline.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
extractMetadatasucceeds against a registered MeterProvider - THEN an entry with name
video.metadata.extracted_total, value1, and attributeoutcome: "success"is recorded
Scenario: Counter increments on failure
- WHEN
extractMetadatathrowsunknownVideoFormat() - 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
VideoErrorwith codeVIDEO.CONFIG_INVALIDis thrown
Verified by:
packages/media/video/video/src/processor/processor.test.ts::"throws CONFIG_INVALID for invalid config".