# Video Clipper probe

Date: 2026-08-25
Machine: macOS, host `ffmpeg 9.0.1` (Homebrew)
Browser: Chrome 151.0.0.0 via Cursor Chrome DevTools MCP
Product: `/products/video-clipper` — FFmpeg.wasm in the browser, 1GB guard, no upload
Argv source: `app/lib/video-clipper/ffmpeg-args.ts` medium MP4
(`-ss` before `-i`, `-t`, `-crf 23`, `-preset veryfast`, `libx264` + `aac`)

This file records host baselines and one browser tab. Browser numbers are
only from that tab. They are not a production-user average.

## Environment caveat (read first)

The MCP Chrome tab on `http://localhost:3000/products/video-clipper` reported:

| Check | Value |
| --- | --- |
| `navigator.userAgent` | Chrome/151.0.0.0 |
| `hardwareConcurrency` | 12 |
| `deviceMemory` | 32 |
| `window.crossOriginIsolated` | `false` |
| `typeof SharedArrayBuffer` | `"undefined"` |

Vite still sends `Cross-Origin-Embedder-Policy: require-corp` and
`Cross-Origin-Opener-Policy: same-origin` on the HTML. This tab did not
become cross-origin isolated. First load therefore:

1. Fetched `@ffmpeg/core-mt@0.12.6` from jsdelivr (`ffmpeg-core.js` at
   08:43:54Z, `ffmpeg-core.wasm` ~9.3 MB compressed at 08:43:55Z).
2. Console: `Multi-threaded FFmpeg failed, falling back:
   ReferenceError: SharedArrayBuffer is not defined`.
3. Fetched `@ffmpeg/core@0.12.6` (single-thread) at 08:44:01Z
   (`ffmpeg-core.wasm` ~9.3 MB compressed).

A normal Chrome window on the same URL may stay isolated and keep
`core-mt`. Do not write that every Chrome 151 user falls back. This
session's encode timings are **single-thread wasm**.

`performance.memory.usedJSHeapSize` is the JS heap only. It does not
include wasm linear memory. Host `ffmpeg` RSS is a different metric.
Do not treat 176 MB JS heap as peak process RAM.

## Fixtures

| File | Bytes | Duration | Notes |
| --- | ---: | ---: | --- |
| `clip-15s-720p.mp4` | 886,062 | 15.000s | `testsrc` 1280x720@30 + 440Hz sine, ultrafast H.264 |
| `clip-100mb.mp4` | 100,001,988 | 20.000s | `testsrc2` 1080p CBR 40 Mbps + filler; ~95.4 MiB / 100.0 MB |
| `clip-500mb.mp4` | 532,528,318 | 112.000s | `testsrc2` 1080p CBR 50 Mbps, 112s; ~508 MiB / 532.5 MB |
| `clip-1gb.mp4` | 1,070,342,314 | 226.368s | `testsrc2` 1080p CBR ~38 Mbps, `-fs 1070000000`; 1020.76 MiB, 3.4 MB under 1 GiB |
| `clip-approx-100mb.mp4` | 12,892,600 | 60.000s | Failed size target: plain `testsrc` compresses; unused |

Large binaries live under `/tmp/growth-evidence-2026-08-25/video/` and
are not committed. A same-origin copy was served from
`public/_tmp-evidence/` during the browser pass, then deleted.

## Host baselines

### 100MB trim (Clipper-equivalent argv)

```bash
ffmpeg -ss 0 -i clip-100mb.mp4 -t 5 -avoid_negative_ts make_zero \
  -threads 0 -movflags +faststart -crf 23 -preset veryfast \
  -c:v libx264 -c:a aac -profile:v main -level 4.0 -pix_fmt yuv420p out.mp4
```

| Metric | Value |
| --- | --- |
| Wall time | 0.51s real (2.73s user) |
| Max RSS | 632,471,552 bytes (~603 MiB) |
| Output | 3.4 MiB |

### 500MB trim (same Clipper-equivalent argv)

| Metric | Value |
| --- | --- |
| Input | 532,528,318 bytes, 112.000s, 1080p |
| First wall (cold-ish) | 1.83s real |
| Repeat wall | 0.44s real (2.67s user) |
| Max RSS | 630,652,928 bytes (~602 MiB) |
| Output | 3.3 MiB (`trim-500mb-0-5s-host-clipper-args.mp4`) |

Host RSS stayed in the same band as the 100MB job. That is **not** a
browser wasm measurement. The same file was later loaded in the Clipper tab
(see the 500MB wasm row below).

### 1GB-minus trim (same Clipper-equivalent argv)

| Metric | Value |
| --- | --- |
| Input | 1,070,342,314 bytes, 226.368s, 1080p |
| First wall | 1.84s real |
| Repeat wall | 0.43s real (2.82s user) |
| Max RSS (repeat) | 633,438,208 bytes (~604 MiB) |
| Output | 3,443,273 bytes (`trim-1gb-0-5s-host-clipper-args.mp4`, 5.013s) |

Host RSS stayed in the same band as the 100MB and 500MB jobs. `-ss` before
`-i` means the host process does not decode the unused tail.

### Equal split: copy vs re-encode (15s `testsrc`)

`ffmpeg -f segment -segment_time 5 -c copy` produced **two** parts
(8.36s and 6.67s), not three. Encode used `keyint=250` at 30 fps, so
keyframes are ~8.3s apart. Copy-mode splits follow keyframes.

Video Clipper does not use `-c copy`. It re-encodes each segment with
the same trim argv. Host re-encode of three 5s windows:

| Segment | Duration | Size | Wall time |
| --- | ---: | ---: | ---: |
| 0 | 5.016s | 120,930 | 0.19s |
| 1 | 5.023s | 120,473 | 0.19s |
| 2 | 5.023s | 120,259 | 0.23s |

### 100MB equal split (host, 2026-08-26)

Same Clipper argv, three windows on `clip-100mb.mp4` (20.000s):

| Segment | `-ss` / `-t` | Duration | Bytes | Wall |
| --- | --- | ---: | ---: | ---: |
| 1 | 0 / 6.667 | 6.700s | 4,843,975 | 1.11s |
| 2 | 6.667 / 6.667 | 6.721s | 4,780,455 | 0.56s |
| 3 | 13.334 / 6.666 | 6.688s | 4,901,243 | 0.53s |

Sequential host total **2.20s**. Browser wasm on the same file (table below) was ~44–47s in the MCP tab.

## Browser wasm (this tab, single-thread core)

Cold start (click at 08:43:42Z) included both wasm downloads plus the
first 0–5s trim of the 15s file. The first-run wall clock was not
isolated from later idle, so it is not used as an encode number.

Warm runs after the single-thread engine was already loaded:

| Job | Input | Output | Wall | JS heap start → peak | Notes |
| --- | --- | --- | ---: | --- | --- |
| Trim 0–5s, medium MP4 | `clip-15s-720p.mp4` (865 KiB, 15s, 720p) | `clip-0s-5s.mp4` 121,940 bytes | **3.063s** | 68.9 MB → 74.7 MB | Same bytes on a repeat |
| Split into 3, medium MP4 | same 15s file | `segment_{1,2,3}.mp4` 121,940 / 120,887 / 120,042 | **9.004s** | 69.1 MB → 78.3 MB | ~3s per segment; durations shown as 0:05 each |
| Split into 3, medium MP4 | `clip-100mb.mp4` (100,001,988 bytes, 20s, 1080p) | blob segments 4,594,065 / 4,506,774 / 4,591,497 | **43.755s** first (includes `core-mt` fail → ST core); **46.920s** immediate rerun | JS heap ~70 MB → ~85 MB at end | UI: `3 segments created`, ranges `0:00–0:06` / `0:06–0:13` / `0:13–0:20`. Same MCP tab, still not isolated. Rerun was not faster: each job `writeFile`s the whole 100MB again. |
| Trim 0–5s, medium MP4 | `clip-100mb.mp4` (100,001,988 bytes, 20s, 1080p) | `clip-0s-5s.mp4` 3,446,559 bytes | **9.204s** | 69.6 MB → 175.9 MB | UI labeled the file 95.4 MB |
| Trim 0–5s, medium MP4 | `clip-500mb.mp4` (532,528,318 bytes, 112s, 1080p) | `clip-0s-5s.mp4` 3,418,128 bytes | **11.003s** | 68.8 MB → 616.3 MB | UI labeled the file 507.9 MB, duration 1:52 |
| Trim 0–30s, medium MP4 | `clip-1gb.mp4` (1,070,342,314 bytes, 226.368s, 1080p) | `clip-0s-30s.mp4` 20,685,166 bytes | **56.771s** | 68.7 MB → 1,150.3 MB | Default window after load; React state was still 0–30 |
| Trim 0–5s, medium MP4 | same 1GB-minus file | `clip-0s-5s.mp4` 3,443,386 bytes | **10.254s** | 70.0 MB → 1,145.2 MB | UI accepted the file (under the 1 GiB guard) |

UI after the 15s trim: "Processing complete", clipped video `0:05 · MP4`,
download name `clip-0s-5s.mp4`. Files never left the device (blob URLs
only).

## 1GB guard

`MAX_VIDEO_BYTES = 1024 * 1024 * 1024` in
`app/lib/video-clipper/settings.ts`. Unit test rejects
`MAX_VIDEO_BYTES + 1`.

This tab also showed the live alert
`File size must be under 1GB for optimal performance` after a `File`
whose `size` was set to `1073741825`. A bitstream just under the guard
(`clip-1gb.mp4`, 1,070,342,314 bytes) was accepted and encoded (table
above). A file *over* 1 GiB was not generated.

Encode wall for the 0–5s window stayed near the 500MB job because `-ss`
is before `-i`. JS heap peaked around 1.15 GB while the whole `File` was
held in the tab. That is not process RSS and not a comfortable working
size on a smaller machine.

## Product limits already in code

- Per-file guard is 1GB; FAQ says practical limit also depends on RAM,
  browser, and codec
- Not an AI highlight cutter
- Loads `core-mt@0.12.6` first, then `core@0.12.6` on failure
