ffmpegwasm / ffmpegwasm/ffmpeg.wasm

[Guide/Feature] Fully working MV3 Chrome Extension implementation (Bypassing Worker CSP, Blob issues & exit(0) crash)

Open
#934 0 comments 1 reaction 0 assignees View on GitHub
Dominant language
C
Stars
17.8k
Forks
1.1k
PR merge metrics
No merged PRs in 30d

Description

(Description)**

```markdown
### Is your feature request related to a problem? Please describe.
Developing Chrome Extensions using Manifest V3 (MV3) with `ffmpeg.wasm` is currently a nightmare for many developers. The strict Content Security Policy (CSP) in MV3 completely blocks the creation of Web Workers via `blob:` URLs, which breaks the standard `@ffmpeg/ffmpeg` wrapper.

Furthermore, even when trying to use `@ffmpeg/core-st` inside an Offscreen Document, developers hit three major walls:
1. Emscripten's prompt asking for input (since there is no standard terminal).
2. The lack of standard documentation on how to manually pass arguments via pointers without the wrapper.
3. The `Program terminated with exit(0)` being thrown as a fatal error, crashing the JS execution despite the successful conversion.

### Describe the solution you'd like
We would like to propose adding an **Official MV3 Chrome Extension Guide** to the documentation, featuring a "Direct Core Injection" method.

By bypassing the wrapper and interacting directly with the C++ core (`ccall`), we can execute FFmpeg synchronously in an Offscreen Document without triggering any CSP violations, Worker blocks, or memory leaks.

Here is the bulletproof implementation we successfully built and tested for MV3:

### Proposed Implementation Guide (Code Snippet)

**1. Load the core safely (No Blobs):**
Instead of using `load()`, inject the raw single-thread script directly into the DOM of the `offscreen.html`.

```javascript
// Load @ffmpeg/core-st safely
const script = document.createElement('script');
script.src = chrome.runtime.getURL('ffmpeg/ffmpeg-core.js');
document.head.appendChild(script);
```

**2. The Bridge (Translating JS to C++ pointers):**
Execute the command manually using `_malloc` and `ccall` to talk to the `main` function.

```javascript
function executeFFmpeg(core, args) {
const commandArgs = ['ffmpeg', ...args];

// Allocate memory for strings
const pointers = commandArgs.map((arg) => {
const length = core.lengthBytesUTF8(arg) + 1;
const pointer = core._malloc(length);
core.stringToUTF8(arg, pointer, length);
return pointer;
});

// Allocate memory for the argv array
const argvPointer = core._malloc(pointers.length * 4);
pointers.forEach((ptr, i) => core.setValue(argvPointer + (i * 4), ptr, 'i32'));

// Call C++ main door directly
const result = core.ccall('main', 'number', ['number', 'number'], [pointers.length, argvPointer]);

// Cleanup to prevent memory leaks
pointers.forEach(ptr => core._free(ptr));
core._free(argvPointer);
return result;
}
```

**3. The Execution Flow (Handling `-nostdin` and `exit(0)`):**

```javascript
try {
// -nostdin prevents the Emscripten prompt bug
executeFFmpeg(core, [
'-nostdin', '-y', '-i', 'input.audio',
'-c:a', 'libopus', '-b:a', '16k',
'-ac', '1', '-ar', '16000', '-f', 'ogg', 'output.ogg'
]);
} catch (error) {
// Catch Emscripten's successful exit(0) signal
const errMsg = error.message || String(error);
if (errMsg.includes("exit(0)")) {
console.log("Success! FFmpeg finished the job.");
} else {
throw error; // Real error
}
}
```

### Describe alternatives you've considered
We considered chunking files and passing them via `postMessage` to a sandboxed iframe, but that disables Chrome extension APIs and creates massive overhead for large media files. The Direct Core Injection using IndexedDB for storage is drastically faster (under `600ms` for small audio files) and entirely native.

### Additional context
Providing this as a boilerplate or documentation section for "Browser Extensions (Manifest V3)" will save countless hours for developers trying to implement offline audio/video processing in Chrome. We are sharing this because it works flawlessly in production!
```

Directory Structure & Data Flow (The Architecture)
To make this work flawlessly in MV3 without hitting message-passing memory limits (e.g., crashing when sending large Base64 strings between the background and offscreen), we use IndexedDB as a shared virtual hard drive.

Here is the exact file structure you need:

Plaintext
extension-root/

├── manifest.json # MV3 Manifest (Requires 'offscreen', 'storage', and 'unlimitedStorage' permissions)
├── background.js # Service Worker (Manages queues and creates the offscreen document)
├── db-utility.js # IndexedDB Wrapper (Saves/Reads Blobs to the local browser storage)
├── offscreen.html # The invisible DOM (Only imports db-utility.js and offscreen.js)
├── offscreen.js # The Core Injector (Bridges JS and C++, reads/writes to IndexedDB)

└── ffmpeg/ # The raw Single-Thread core files (No @ffmpeg/ffmpeg wrappers!)
├── ffmpeg-core.js # From @ffmpeg/core-st (v0.11.1)
└── ffmpeg-core.wasm # From @ffmpeg/core-st (v0.11.1)
How the Data Flows (The Secret Sauce):

Background.js downloads the audio/video file and saves the raw Blob directly into IndexedDB (db-utility.js).

Background.js sends a tiny ping to offscreen.js with just the ID of the file (e.g., { action: 'CONVERT', id: '123' }).

Offscreen.js wakes up, retrieves the Blob from IndexedDB, injects the .wasm core, and executes the C++ main function using pointers.

Once converted, offscreen.js saves the new .ogg Blob back into IndexedDB and replies success: true.

Background.js retrieves the final file from IndexedDB and delivers it to the active tab.

This architecture completely eliminates the Receiving end does not exist crashes, bypasses the CSP Blob blocks, and handles files of any size without RAM bottlenecks.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.