# Bun.Video

> Read video metadata, extract frames, audio and GIFs, remux between MP4, MOV, Matroska and WebM, and cut HLS segments with Bun's native video API.

`Bun.Video` is Bun's native video API. Its built-in decoder (FilmCraft, reported
by `Bun.Video.decoder`) reads media and decodes frames; the
[`Bun.Image`](https://bun.aphrody.app/docs/runtime/image) pipeline transforms and encodes those frames.
These operations run inside Bun without launching an external media executable.

```ts
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; there is no module to import. The separate
`@aphrody/bun-video` package adds composition engines, HLS/DASH, browser capture
and optional FFmpeg/mpv adapters on top of it.

`Bun.Video.available` reports whether this build of Bun includes the native
decoder; it does not guarantee support for every codec. MP4 and QuickTime input
keeps the budgeted sample-table path. Other containers (WebM, Matroska, AVI, MPEG-TS and the
rest that FilmCraft opens) decode frames, audio and GIFs through FilmCraft's container owners;
`remux` and `hls` read MP4, QuickTime, Matroska and WebM. For Matroska and WebM `frameCount` is the
exact packet count of the video track; for AVI and the other containers it is the duration times the
frame rate, rounded. The tests decode VP8 and VP9 WebM, H.264 Matroska and MJPEG AVI fixtures; codec profiles outside those are unqualified and may be rejected. Use `Bun.Image` for still images.

## Command line

`bun video` exposes the same native API on the command line. Inspect the
decoder, read metadata and extract frames from a local file:

```sh
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
bun video remux clip.mp4 clip.mkv
bun video hls clip.mp4 hls --segment-seconds 4
```

Output files must not already exist, and `frames` requires a new directory.
Use `bun video --help` for the options and `--json` for machine-readable
results. Composition and downloader commands such as `bun video remotion` are
provided by the optional Bun Video executable, which must be installed next to
the `bun` executable together with its release manifest.

## 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`.

```ts
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 raw RGBA8 pixels without a PNG encode and decode round trip.
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.

```ts
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 uses 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, remux and HLS

```ts
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");
await video.remux("episode.mkv");
const segments = await video.hls("episode-hls", { segmentSeconds: 4 });
```

Audio extraction returns PCM16 WAV and is limited to 30 seconds and 64 MiB.
GIF export is palette-quantized and therefore lossy; it is limited to five
seconds, 24 frames per second, 640×360 and 32 MiB. Remux copies the encoded
packets into a new file without re-encoding; the extension selects the container
(`.mp4`, `.m4v`, `.mov`, `.mkv` or `.webm`). MP4 to MP4 keeps every access unit byte for
byte through the strict sample-table path; every other pair goes through FilmCraft's
container library, which refuses a codec the target cannot carry (for example H.264 in
WebM) and leaves no partial file. Remux refuses to overwrite an existing destination
and does not make the file smaller.

`hls(directory, { segmentSeconds })` cuts an MP4, QuickTime, Matroska or WebM source into
CMAF segments without re-encoding and writes `init.mp4`, `segment-00000.m4s`, ... and a VOD
`playlist.m3u8` into a directory that must not exist; it resolves to the segment count.
Segments are written as they are produced, so memory stays bounded by one segment. Each
segment ends at the first keyframe at or after `segmentSeconds` (0.5 to 60, default 6), so
a source with sparse keyframes yields longer segments. Codecs without an ISO base media
mapping, such as VP8, are rejected. Serve the directory with `Accept-Ranges` and CORS headers
and open `playlist.m3u8` in any HLS player.

## Frame analysis with WebGPU

The `@aphrody/bun-video/frame-analysis` export computes WGSL histograms and
detail metrics on the GPU. Pass raw Image or Video pixels directly:

```ts
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 result reports the compute adapter that ran the analysis, and unsupported
backends are rejected rather than replaced by a CPU fallback. Input is limited
to 65536 pixels (each dimension from 4 to 256). Frame analysis does not use
hardware video decoding.
