A four-minute revert and the fix that replaced it
At 16:43 on June 17, a commit titled "fix: add static ffmpeg binaries for production video processing" adds the ffmpeg-static and ffprobe-static packages and wires fluent-ffmpeg to point at them. At 16:47, four minutes later, the next commit reverts it outright: "This reverts commit 42d036c." No explanation in the message. No follow-up note anywhere else in the repo.
The revert has no documented reason. What's documented, added the same day in PRODUCTION_SETUP.md, is the constraint that made the static-binary approach a dead end on Vercel: the application runs in a read-only file system, only /tmp is writable, and maximum /tmp storage is typically 512MB. An ffmpeg binary inside a serverless function has nowhere durable to write its output. Neither commit ties the revert to that constraint, so read it as context, not as the reason.
What replaced it
Forty minutes after the revert, at 17:27, lib/transcoder.ts lands: 303 lines wiring the app to Google Cloud Transcoder instead of running ffmpeg in-process at all. No binary to bundle, no /tmp limit to hit, because the transcoding happens on Google's infrastructure and the app just submits a job and polls for it.
The job configuration set four quality levels and one segment duration, and that ladder is still what a real upload gets transcoded into:
| Rendition | Resolution | Video bitrate | Profile |
|---|---|---|---|
| 1080p | 1920x1080 | 5,000,000 bps | high |
| 720p | 1280x720 | 2,800,000 bps | high |
| 480p | 854x480 | 1,400,000 bps | main |
| 360p | 640x360 | 800,000 bps | main |
Every rendition muxes to .ts with a 6-second segment duration and a 3-second GOP, and all four feed one master.m3u8 manifest. Audio is a single AAC stream at 128kbps, shared across renditions rather than encoded per quality level. The commit message describes one more piece worth keeping: upload processing falls back to the original video if the transcoding job itself fails, rather than leaving the upload in a broken state.
PRODUCTION_SETUP.md spells out the same constraint in terms anyone deploying the app would need: development paths like uploads/, output/, and temp/ all have to resolve to somewhere under /tmp in production, and anything meant to persist (the actual HLS output) belongs in Google Cloud Storage. That's the same conclusion the transcoder commit reached in code a few hours earlier, written down as a rule for whoever deploys this next.
The same doc lists the bucket setup as two gsutil calls, which is a smaller thing than the transcoder migration but the same shape of decision: don't build storage, point at a managed one.
gsutil mb gs://your-hls-bucket
gsutil iam ch allUsers:objectViewer gs://your-hls-bucket
The next day, June 18, the same read-only-filesystem problem showed up again for ffprobe specifically. Rather than trying to run ffprobe inside the Next.js API route, the fix was a standalone Cloud Run service: a Dockerfile, a deploy script, and a health check, with local ffprobe kept only as a development fallback. The commit message put it plainly: "Implemented fallback to local FFprobe for development and integrated external FFprobe service for production environments." Same lesson as the transcoder, applied to the other binary the app needed.
Two binaries, one answer
Both binaries ended up off Vercel. ffmpeg became a Cloud Transcoder job and ffprobe became a Cloud Run service. Each costs a network hop and is slower to iterate on locally than a binary in the same process, and neither decision has been revisited since.
What the four minutes between 42d036c and 1380676 actually hit (a bundle-size limit, a cold-start timeout, the missing writable directory) isn't recorded anywhere. FFmpeg did come back into the product later, running in the browser as WebAssembly instead of on a server, and that's where it runs today.