Bun.Video is Bun's native video API. The Rust FilmCraft provider reads media
and decodes frames. The Bun.Image worker transforms and
encodes those frames. These operations run inside Bun without launching an
external media executable.
if (!Bun.Video.available) throw new Error("This runtime has no native video provider");
const video = new Bun.Video("episode.mp4");
const metadata = await video.metadata();
await video.frame(480).resize(640, 360).webp({ lossless: true }).write("frame.webp");
Use the global Bun.Video class. Importing bun:video or installing
@aphrody/bun-video is unnecessary for this native API. The package supplies
composition engines, HLS/DASH, browser capture and optional FFmpeg/mpv adapters.
The provider is selected at build time (--video-native=on); available reports
its inclusion, not support for every codec. MP4 with H264 video and AAC audio
is the qualified input. Other containers, profiles and targets need separate
qualification. Still-image import is owned by Bun.Image.
Command line
bun video exposes the same native owner. Inspect the provider and extract a
thumbnail from a local file:
bun video info --json
bun video metadata clip.mp4 --json
bun video frame clip.mp4 preview.webp --time 1 --width 320 --height 180 --format webp
bun video frames clip.mp4 frames --start 0 --end 1 --step 0.25 --max-frames 4
bun video audio clip.mp4 audio.wav --start 0 --duration 1 --sample-rate 16000
bun video gif clip.mp4 preview.gif --duration 1 --fps 8 --width 64
bun video remux clip.mp4 remux.mp4
Output files must be new. The batch command requires a new directory. Use
bun video --help for the options and --json for machine-readable results.
Native commands use Bun.Video directly. Composition and downloader commands
such as bun video remotion require the checksum-verified Bun Video executable
and its release receipt beside Bun.
Sources and metadata
The constructor accepts the same source types as Bun.Image: a local path,
bytes, a data URL or a Blob. Local movies use bounded reads from a regular file;
the file limit is 64 GiB. In-memory input is limited to 256 MiB. Treat path strings
as filesystem access and validate them before accepting untrusted input.
metadata() returns source width, height, duration, codec, container,
fpsNumerator, fpsDenominator, frameCount, fileBytes and audioStreams.
Each audio stream reports its sample rate, channels and codec. The source frame
rate remains rational; do not round 24000/1001 to 24 when selecting frames.
The constructor captures a source and settings without decoding it. width
and height begin at -1 and report the last transformed frame after a frame
terminal completes. Use metadata() for the source dimensions.
Frame transformations and raw pixels
frame(seconds) selects a non-negative presentation time. Chainable transforms
record settings; a terminal method schedules work off the JavaScript thread.
The order is shared with Image: autoOrient → extract → rotate → flip/flop → resize → modulate.
const pixels = await new Bun.Video("episode.mp4")
.frame(780)
.extract({ left: 320, top: 100, width: 800, height: 600 })
.resize(128, 96)
.rgba();
// { width, height, channels: 4, data: Uint8Array, iccProfile }
rgba() returns owned RGBA8 bytes without encoding and decoding a PNG.
Other terminals mirror Image: bytes, buffer/toBuffer, blob, write,
toBase64, dataurl/toDataURL and placeholder. JPEG, PNG, WebP, HEIC and
AVIF output use Image's encoders and platform support. Video.backend shares
the Image transformation backend; it does not select a video decoder.
Bounded frame batches
frames({ start, end, step, maxFrames }) returns { time, bytes } entries with
source-resolution PNGs. end is exclusive. A batch is limited to 64 frames and
64 MiB of encoded output; choose a smaller batch for large frames.
const meta = await video.metadata();
const step = meta.fpsDenominator / meta.fpsNumerator;
const frames = await video.frames({ start: 0, end: 3 * step, step, maxFrames: 3 });
Each batch owns one decoder session. Frame batches preserve source resolution;
use frame(time) to transform an individual frame. This API does not keep an
entire episode decoded in memory.
Audio, GIF and MP4 remux
await Bun.write("audio.wav", await video.audio({ start: 10, duration: 20, sampleRate: 16000 }));
await Bun.write("moment.gif", await video.gif({ start: 638, duration: 5, fps: 12, width: 640 }));
await video.remux("episode-remux.mp4");
Audio extraction returns PCM16 WAV and is bounded to 30 seconds and 64 MiB. GIF export is palette-quantized: it is a lossy derivative, limited to five seconds, 120 frames, 640×360 and 32 MiB. Remux preserves encoded access units and writes a new MP4; it refuses overwriting an existing destination. Remux is distinct from encoding a new composition or guaranteeing a smaller file.
Frame analysis with WebGPU
The package extension @aphrody/bun-video/frame-analysis owns bounded WGSL
histograms and detail metrics. Pass raw Image/Video pixels directly:
import { analyzeFrameRgba } from "@aphrody/bun-video/frame-analysis";
const rgba = await video.frame(780).resize(128, 72).rgba();
const metrics = await analyzeFrameRgba(rgba, { backend: "chromium", timeout: 5000 });
The extension reports the actual compute adapter and rejects unsupported backends. It accepts at most 65536 pixels and does not silently substitute CPU analysis. WGSL frame analysis does not establish hardware video decoding.