(Description)**
### 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.
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)):
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.
(Description)**
2. The Bridge (Translating JS to C++ pointers):
Execute the command manually using
_mallocandccallto talk to themainfunction.3. The Execution Flow (Handling
-nostdinandexit(0)):Describe alternatives you've considered
We considered chunking files and passing them via
postMessageto 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 (under600msfor 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!