SvelteKit Vite

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:

  1. Markup plugin (order: 'pre') scans .svelte sources for the custom enhanced:video element (same namespace as HTML, so it is written like a normal tag in Svelte). It rewrites markup to the runtime EnhancedVideo component, wires metadata from a virtual import, and preserves passthrough <video> attributes.
  2. Loader plugin (enforce: 'pre') implements the ?enhanced-video virtual 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.

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). User poster= attrs are dropped by design.
  • Lazy by defaultloading="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-motion respected 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

Binary resolution order

ffmpeg / ffprobe are resolved in this order (first hit wins):

  1. advanced.ffmpegPath / advanced.ffprobePath
  2. ffmpeg-ffprobe-static (if installed)
  3. ffmpeg-static + ffprobe-static (if installed)
  4. PATH

Install

Example
shell Install into a SvelteKit app
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.

Minimal markup (escaped here so this docs source stays parse-safe):

&lt;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' | falseH.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.
  • src must stay a compile-time string literal.
  • Cache is not auto-pruned.

Repository: github.com/voadk/enhanced-video-sveltekit · License: MIT