The problem
The Kolel Creator Dashboard lets educators and rabbis upload video lessons directly from their browser. Creators record on phones, tablets, and cameras ā producing files in every format imaginable: .mov, .webm, .avi, .mkv.
Our Google Cloud Video Transcoder pipeline expects MP4 input. The original solution was a dedicated pre-upload server that re-encoded every file before it hit cloud storage. One more service to deploy, monitor, and pay for.
We needed a way to normalize the format before the file ever left the creator's machine.
The solution: FFmpeg running inside the browser
We integrated @ffmpeg/ffmpeg into the Vue 2 upload component. When a creator picks a non-MP4 file, the browser:
- Downloads the FFmpeg WASM core (~30 MB, cached after first load)
- Transcodes the file to MP4 inside a Web Worker
- Uploads the resulting blob directly to a GCS Signed URL
The Rails API and the downstream transcoding pipeline never see anything other than a clean MP4.
The core transcoding function
import { createFFmpeg, fetchFile } from '@ffmpeg/ffmpeg';
async convertToMp4(file) {
const supportedTypes = ['video/mp4', 'video/mov', 'video/m4v'];
if (supportedTypes.includes(file.type)) {
return; // already compatible ā skip transcoding
}
const ffmpeg = createFFmpeg({
log: true,
corePath: 'https://unpkg.com/@ffmpeg/core@0.10.0/dist/ffmpeg-core.js',
});
if (!ffmpeg.isLoaded()) {
await ffmpeg.load();
}
const fileName = file.name;
const outputFileName = 'output.mp4';
ffmpeg.FS('writeFile', fileName, await fetchFile(file));
await ffmpeg.run('-i', fileName, outputFileName);
const data = ffmpeg.FS('readFile', outputFileName);
this.selectedFile = new Blob([data.buffer], { type: 'video/mp4' });
}
A few things worth noting:
-
We pin a specific CDN version of
@ffmpeg/coreso the WASM binary is cached in the browser after the first upload session. Repeat visitors pay zero load cost. -
We bail early for common formats (
mp4,mov,m4v). Running FFmpeg.wasm on a file that's already compatible wastes time and memory.
Uploading without exposing credentials
The browser never holds GCS credentials. The Rails API generates a short-lived Signed URL for the specific file path, then the browser PUTs directly to GCS:
async uploadFileToSignedUrl(signedUrl, selectedFile) {
await axios.put(signedUrl, selectedFile, {
headers: { 'Content-Type': selectedFile.type },
});
}
async prepareVideoUpload() {
this.loading = true;
await this.getSignedUrl();
await this.uploadFileToSignedUrl(this.signedUrl, this.selectedFile);
if (this.pickedThumbnail) {
await this.getImageSignedUrl();
await this.uploadFileToSignedUrl(this.thumbnailSignedUrl, this.pickedThumbnail);
}
this.addVideo(); // notify Rails API to create the video record
}
Results
| Before | After |
|---|---|
| Pre-upload transcoding microservice | Removed entirely |
| Format errors reaching the pipeline | Zero |
| GCS credentials in the browser | Never |
| Upload experience for MP4/MOV users | Unchanged |
Key takeaways
- FFmpeg.wasm is production-ready for this use case. The WASM core is large, but browser caching makes repeat sessions fast.
- Signed URLs are the right pattern for direct browser-to-cloud uploads. No credentials ever leave your server.
- Always allow-list known-good formats before invoking WASM ā it saves real time for the majority of your users.
Stack
Vue 2 Ā· @ffmpeg/ffmpeg 0.10 Ā· @ffmpeg/core (WebAssembly) Ā· Google Cloud Storage Ā· GCS Signed URLs Ā· Google Cloud Video Transcoder Ā· Rails 7 Ā· Axios
Originally published on the Northbeam Technologies blog.












