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,VideoCodeunion,VIDEO_CODESregistry. - 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
- Error base:
SuaveplanError<VideoCode>(genesis foundation) rather than the upstreamBaseError. Public discriminants (err.code,err.message,err.cause,err.context) are identical. - Event bus: composes
EventBus<TMap>whereTMap = VideoEventMapand payloads are single values (no[T]tuples). The processor exposeson/once/offso call sites read the same. Genesis pattern matches@suaveplan/image. - OTel: meter
@suaveplan/videowith:- 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.
- counter
- Validation: Zod schemas at every public boundary
(
VideoProcessorConfigSchema,VideoTransformPipelineInputSchema). Invalid input throwsVideoError(VIDEO.CONFIG_INVALID)/VideoError(VIDEO.INVALID_TRANSFORM). - AbortSignal: both
extractMetadataandplanTransformaccept an optionalAbortSignaland short-circuit withVideoError(VIDEO.PROCESSING_FAILED, cause: AbortError)when aborted. pipeline.builtevent: declared upstream but never emitted; genesis fires it fromVideoPipelineBuilder.build()whenever the builder was created viaVideoProcessor.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 =
ftypASCII → MP4 family; sub-brand bytes 8-11 startingqtreturnmov, elsemp4. - 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
presetset: use preset dimensions verbatim. - else if
maintainAspectRatio !== falseand exactly one dimension set: compute the other fromoriginal.width / original.height. - else: use options as-is (or
originalwhen 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
- AbortSignal pre-check.
- Start OTel span + timer.
detectVideoFormat(data); ifnulland noknownMetadata.format, throwunknownVideoFormat()(outcome=unknown_format).- Compose
VideoMetadatafromknownMetadata+ detected format, defaulting numeric fields to 0 andhasAudiototrue. - If
metadata.duration > maxDuration, throwdurationExceeded(...)(outcome=duration_exceeded). - Emit
metadata.extracted. - On any non-
VideoErrorexception: wrap invideoMetadataParseFailed(cause). - Finally: record counter + histogram + end span.
VideoProcessor.planTransform
- AbortSignal pre-check.
- Start OTel span + timer.
- Validate pipeline via
VideoTransformPipelineInputSchema. - Walk operations in order, mutating local
(width, height, duration, format, hasAudio):resize→calculateVideoResize.trim→validateTrimRangeelseinvalidTrimRange; new duration.rotate→ requiredegrees % 90 === 0elseinvalidVideoTransform; swap dims whendegrees % 180 !== 0.flip→ no-op for planning.speed→ requirefactor > 0elseinvalidVideoTransform;duration /= factor.mute→hasAudio = false.format→ set output format.codec/bitrate→ no-op for planning (informational only).
- Emit
transform.planned. - 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;
VideoErrorinstance/code/context/ cause;VIDEO_ERROR_METADATAtable shape. - Processor tests — every branch of
extractMetadata+planTransform(including eachVideoTransformOpvariant + abort + zod-config invalid). - Pipeline builder tests — every fluent method +
build()+apply()+pipeline.builtemission 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.