enhanced-video-sveltekit
Build-time video pipeline for SvelteKit — multi-format transcodes, auto poster, lazy loading, and a single custom element in your markup.
Last updated
Overview
enhanced-video-sveltekit ships enhancedVideos() — two coordinated Vite plugins:
- Markup plugin (
order: 'pre') scans.sveltesources for the customenhanced:videoelement (same namespace as HTML, so it is written like a normal tag in Svelte). It rewrites markup to the runtimeEnhancedVideocomponent, wiresmetadatafrom a virtual import, and preserves passthrough<video>attributes. - Loader plugin (
enforce: 'pre') implements the?enhanced-videovirtual module: ffprobe the source, ffmpeg each selected format × resolution, write content-hashed artifacts to disk cache, emit URLs. In dev, middleware serves bytes with HTTP range support; in build, assets are emitted like other Vite files.
You keep a static string src in markup; the build resolves paths, encodes, and hands the browser a <picture> poster plus ordered <source> children so playback picks the first decodable stream.
The package on npm is enhanced-video-sveltekit. Internal strings (?enhanced-video, /@enhanced-video/, cache dir enhanced-video) stay short on purpose.
What you get
- Multi-height ladder — default targets 1080p, 720p, 480p; rungs taller than the source are skipped, and rungs within ~10% height of a kept larger rung are deduplicated.
- Multi-codec outputs — default
mp4+webm(H.264 / VP9); optional AV1 and HEVC-in-MP4 (mp4_hevc). - Auto poster — extracted frame as jpg plus optional webp / avif behind
<picture>(helps LCP / CLS). Userposter=attrs are dropped by design. - Lazy by default —
loading="lazy"keeps<source>out of the DOM until an IntersectionObserver says the block is near the viewport, so no fetch until needed. - Playback polish — poster overlay fades on real playback (timeupdate / playing / error + short fallback), optional auto-pause when scrolled away,
prefers-reduced-motionrespected for autoplay,loading="click"for explicit play. - Disk cache — repeat dev / CI runs hit cache; key includes source bytes, args, quality profile, package version, ffmpeg banner.
- Dev UX — cold encode can show a placeholder first; when encodes finish, modules invalidate for HMR. Terminal progress for long jobs (TTY bar, plain lines otherwise).
- Types — re-exported
EnhancedVideoMetadata,EnhancedVideosOptions,VideoFormat, etc., from the package entry.
Requirements
| Requirement | Notes |
|---|---|
| Node | ≥ 18 |
| ffmpeg / ffprobe | ≥ 6 with libx264, libx265, libvpx-vp9, libsvtav1 when you use those codecs (Homebrew / Debian / Chocolatey builds usually include them; static npm packages too) |
| Svelte | ^5 |
| Vite | ^6.3 or ≥ 7 |
| @sveltejs/vite-plugin-svelte | ^6 or ^7 |
Encoding runs in dev (background + HMR) and synchronously on production build. Missing binaries or a failed encode fails the build — treat ffmpeg like a compiler dependency.
Binary resolution order
ffmpeg / ffprobe are resolved in this order (first hit wins):
advanced.ffmpegPath/advanced.ffprobePathffmpeg-ffprobe-static(if installed)ffmpeg-static+ffprobe-static(if installed)PATH
Install
pnpm add -D enhanced-video-sveltekit# optional: vendored binaries so CI agents do not need system ffmpeg
pnpm add -D ffmpeg-ffprobe-static Vite setup
Call enhancedVideos() before sveltekit() (or svelte() in a non-Kit Vite + Svelte app) so the markup transform runs ahead of vite-plugin-svelte:compile. With Tailwind v4’s Vite plugin, a typical stack looks like:
import tailwindcss from '@tailwindcss/vite';
import { sveltekit } from '@sveltejs/kit/vite';
import { enhancedVideos } from 'enhanced-video-sveltekit';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [tailwindcss(), enhancedVideos({ resolutions: [360] }), sveltekit()]
}); This demo passes resolutions: [360] so first-time encodes stay small; defaults are [1080, 720, 480].
Authoring in Svelte
Author the tag in .svelte files only (for MDsveX / markdown, import a tiny .svelte wrapper that contains the tag — raw .svx must not include the literal open tag sequence or the markup plugin will try to parse markdown as Svelte).
Passthrough works like HTMLVideoElement: autoplay, muted, loop, controls, playsinline, class, style, preload, ARIA, data-*, events, bindings, etc.
src must be a string literal at transform time. Dynamic src={expr} throws on purpose — the tool has to read the file from disk during the build.
Minimal markup (escaped here so this docs source stays parse-safe):
<enhanced:video src="./hero.mp4" autoplay muted loop playsinline /> Runtime props (enhanced-video-sveltekit/runtime)
| Prop | Default | Purpose |
|---|---|---|
loading | lazy | lazy: gate <source> behind IntersectionObserver. eager: load immediately. click: poster + button until the user plays. |
preload | metadata | Forwarded to the underlying <video>. |
autoPause | true | Pause off-screen; resume when visible again. |
respectReducedMotion | true | Skip autoplay when prefers-reduced-motion: reduce. |
playLabel | Play video | ARIA label for click-to-play (loading="click"). |
Output formats
formats key | Video codec | Audio | Container | Notes |
|---|---|---|---|---|
mp4 | H.264 | AAC | MP4 | Widest compatibility |
webm | VP9 | Opus | WebM | Often smaller than H.264 for web |
mp4_hevc | H.265 | AAC | MP4 | hvc1 style tagging for Apple-friendly MP4 |
av1 | AV1 | Opus | WebM | Best compression; decode on current Chrome / Firefox / Safari |
<source type="…"> order follows your formats array — put the stream you want modern browsers to try first (for example av1 before mp4).
Posters: jpg always; webp / avif when encoders succeed — wrapped in <picture>.
Quality & ladder
quality picks a preset bundle (CRF-ish targets, encoder speed, audio bitrates, poster quality):
| Value | When to use |
|---|---|
web | Faster CI / local iteration; smaller outputs vs balanced |
balanced | Default sweet spot |
archive | Slower, higher fidelity, larger files |
resolutions is a list of target heights in px, descending. The loader skips outputs taller than the source and collapses rungs that are redundant (within ~10% of a larger kept rung).
enhancedVideos(options?)
| Option | Type | Default | Purpose |
|---|---|---|---|
formats | VideoFormat[] | ['mp4', 'webm'] | Which codec families to emit |
resolutions | number[] | [1080, 720, 480] | Height ladder (see above) |
quality | 'web' \| 'balanced' \| 'archive' | 'balanced' | Preset bundle |
cacheDirectory | string | nearest node_modules/.cache/enhanced-video | Output cache root |
maxJobs | number | max(1, cpus - 1) | Parallel ffmpeg cap |
advanced | AdvancedOptions | {} | Hardware accel, fps cap, paths, overrides, locks |
advanced
| Field | Purpose |
|---|---|
hwAccel | 'auto' | 'videotoolbox' | 'nvenc' | 'vaapi' | 'qsv' | false — H.264 / HEVC only; VP9 + AV1 stay software |
fps | min(fps, source_fps) cap |
ffmpegPath / ffprobePath | Explicit binaries |
lockMaxAgeMs | Stale lock cleanup (default 2h) |
overrides | Per-codec / poster knobs layered on quality |
QualityOverrides (TypeScript)
{
h264?: { crf?: number; preset?: string };
hevc?: { crf?: number; preset?: string };
vp9?: { crf?: number; cpuUsed?: number };
av1?: { crf?: number; preset?: number };
audio?: { mp4Bitrate?: string; webmBitrate?: string };
poster?: { jpg?: number; webp?: number; avif?: number };
} Recipe snippets
enhancedVideos({ quality: 'web' });
enhancedVideos({ quality: 'archive' });
enhancedVideos({ formats: ['av1', 'webm', 'mp4'] });
enhancedVideos({ resolutions: [720] });
enhancedVideos({ advanced: { hwAccel: false } });
enhancedVideos({
quality: 'balanced',
advanced: {
overrides: {
h264: { crf: 18, preset: 'slow' },
poster: { jpg: 95 }
}
}
}); Programmatic metadata
Skip the custom element and import metadata only:
import meta from './clip.mp4?enhanced-video';
// EnhancedVideoMetadata — width, height, duration, poster { jpg, webp?, avif? }, sources[] Useful for custom <video> markup, CMS previews, or tests.
Browser notes (default formats)
Exact stream picked is codec + browser dependent. With ['mp4','webm'] you generally see H.264 and VP9 candidates; adding av1 / mp4_hevc changes the matrix.
| Browser | Typical first choice (defaults) |
|---|---|
| Chrome / Edge | H.264 in MP4, then VP9 WebM if skipped |
| Firefox | H.264 / VP9 depending on profile |
| Safari 17+ | H.264; HEVC only if you enabled mp4_hevc |
Dev vs production
- Dev — first request may encode in the background while a placeholder points at the original asset; when jobs finish, module invalidation swaps in final URLs (HMR). Range requests supported on generated URLs.
- Production — all referenced sources are fully encoded before the build completes; failures surface as build errors.
Cache
Outputs live under node_modules/.cache/enhanced-video/ (or cacheDirectory), one folder per hash mixing source bytes, encoder args, resolved quality profile, package version, and ffmpeg version banner. There is no automatic pruning — delete the folder to reclaim disk.
WebContainers
On process.versions.webcontainer, enhancedVideos() returns no plugins (StackBlitz-style environments without ffmpeg). Use the docs locally or in CI with ffmpeg installed.
See it running
Live pipeline (real tag lives in DocsVideoFeature.svelte, not in this .svx):
Live clip (build pipeline)
Limitations
- No HLS / DASH — static
<source>list only. - No subtitle / caption pipeline.
srcmust stay a compile-time string literal.- Cache is not auto-pruned.
Repository: github.com/voadk/enhanced-video-sveltekit · License: MIT
