Skip to content

Instantly share code, notes, and snippets.

@Chris1234567899
Last active April 7, 2024 19:51
Show Gist options
  • Save Chris1234567899/a00afe5e2de1beb1cb4053cbbafc4fe8 to your computer and use it in GitHub Desktop.
Save Chris1234567899/a00afe5e2de1beb1cb4053cbbafc4fe8 to your computer and use it in GitHub Desktop.
Angular/Typescript adjustments of ffmpeg-core for ffmpeg.wasm with web workers
Typescript/angular port of @ffmpeg/ffmpeg library which is used with @ffmpeg/core (see https://github.com/ffmpegwasm/ffmpeg.wasm). Some smaller adjustments have also been made.
This snippet is designed to make use of web workers in angular (see https://angular.io/guide/web-worker) to encode media in a single threaded browser environment.
// Start/stop web worker from component, use e.g. an file from an html file input
worker: Worker
async testWebWorker(file: File) {
if (typeof Worker !== 'undefined') {
// Create a new
this.worker = new Worker(new URL('../../ffmpeg-worker.worker', import.meta.url));
this.worker.onmessage = ({ data }) => {
if (data.type == "progress")
this.encodingProgress = data.data
else if (data.type == "result") {
// encoded arraybuffer -> convert to blob
let blob = new Blob([data.data]
// use the blob for whatever you want, e.g in a html5 media player
let audioPlayer: HTMLAudioElement = this.track.nativeElement;
audioPlayer.src = URL.createObjectURL(blob, { type: 'audio/mp3' }));
// terminate worker to avoid memory leaks
this.worker.terminate()
}else{
console.warn("unknown message",data)
}
};
this.worker.onerror = (err)=>{
console.error(err)
}
this.worker.onmessageerror = (err)=>{
console.error(err)
}
this.worker.postMessage(file);
} else {
// Web Workers are not supported in this environment.
// You should add a fallback so that your program still executes correctly.
}
return
}
//abort encoding
stopWorker() {
if (this.worker)
this.worker.terminate()
}
import { FFmpeg } from "./ffmpeg";
// web worker example, initializing FFMPEG
/// <reference lib="webworker" />
addEventListener('message', async ({ data }) => {
const file:File = data;
const nameIn = file.name;
const outStr = nameIn.split(".");
outStr.pop();
const nameOut = outStr.join(".") + ".wav";
// ffmpeg-core.js, ffmpeg-core.worker.js and ffmpeg-core.wasm
// all under assets/ffmpeg -> make sure .wasm is single threaded version
const settings = {
log: false,
corePath: "/assets/ffmpeg/ffmpeg-core.js",
logger: (msg) => {
console.log("Log", msg)
},
progress: (msg) => {
console.log("Progress", msg.ratio)
postMessage({type:"progress", data: msg.ratio});
}
}
// The log true is optional, shows ffmpeg logs in the console
const ffmpeg = new FFmpeg(settings);
// This loads up the ffmpeg.wasm files from a CDN
await ffmpeg.load();
//fetch the file
const arrayBuffer = await file.arrayBuffer()
// write file to the FFmpeg file system
ffmpeg.writeFile( nameIn, new Uint8Array(arrayBuffer));
// run the FFmpeg command-line tool, converting
await ffmpeg.run('-i', nameIn, '-ac','1', nameOut);
// read the MP4 file back from the FFmpeg file system
const res = ffmpeg.readFile(nameOut);
// Delete files in MEMFS
ffmpeg.unlink(nameIn);
ffmpeg.unlink(nameOut);
postMessage({type:"result", data: res.buffer});
});
declare var createFFmpegCore;
// FFMPEG class - which was former createFFmpeg in ffmpeg/ffmpeg
export class FFmpeg {
private core = null;
private ffmpeg = null;
private runResolve = null;
private running = false;
private settings;
private duration = 0;
private ratio = 0;
constructor(settings) {
this.settings = settings;
}
async load() {
this.log('info', 'load ffmpeg-core');
if (this.core === null) {
this.log('info', 'loading ffmpeg-core');
/*
* In node environment, all paths are undefined as there
* is no need to set them.
*/
let res = await this.getCreateFFmpegCore(this.settings);
this.core = await res.createFFmpegCore({
/*
* Assign mainScriptUrlOrBlob fixes chrome extension web worker issue
* as there is no document.currentScript in the context of content_scripts
*/
mainScriptUrlOrBlob: res.corePath,
printErr: (message) => this.parseMessage({ type: 'fferr', message }),
print: (message) => this.parseMessage({ type: 'ffout', message }),
/*
* locateFile overrides paths of files that is loaded by main script (ffmpeg-core.js).
* It is critical for browser environment and we override both wasm and worker paths
* as we are using blob URL instead of original URL to avoid cross origin issues.
*/
locateFile: (path, prefix) => {
if (typeof res.wasmPath !== 'undefined'
&& path.endsWith('ffmpeg-core.wasm')) {
return res.wasmPath;
}
if (typeof res.workerPath !== 'undefined'
&& path.endsWith('ffmpeg-core.worker.js')) {
return res.workerPath;
}
return prefix + path;
},
});
this.ffmpeg = this.core.cwrap('main', 'number', ['number', 'number']);
this.log('info', 'ffmpeg-core loaded');
} else {
throw Error('ffmpeg.wasm was loaded, you should not load it again, use ffmpeg.isLoaded() to check next time.');
}
}
public writeFile(fileName: string, buffer: Uint8Array) {
if (this.core === null) {
throw NO_LOAD;
} else {
let ret = null;
try {
ret = this.core.FS.writeFile(...[fileName, buffer]);
} catch (e) {
throw Error('Oops, something went wrong in FS operation.');
}
return ret;
}
}
public readFile(fsFileName: string) {
if (this.core === null) {
throw NO_LOAD;
} else {
let ret = null;
try {
ret = this.core.FS.readFile(...[fsFileName]);
} catch (e) {
throw Error(`ffmpeg.FS('readFile', '${fsFileName}') error. Check if the path exists`);
}
return ret;
}
}
public unlink(fsFileName: string) {
if (this.core === null) {
throw NO_LOAD;
} else {
let ret = null;
try {
ret = this.core.FS.unlink(...[fsFileName]);
} catch (e) {
throw Error(`ffmpeg.FS('unlink', '${fsFileName}') error. Check if the path exists`);
}
return ret;
}
}
async run(..._args) {
this.log('info', `run ffmpeg command: ${_args.join(' ')}`);
if (this.core === null) {
throw NO_LOAD;
} else if (this.running) {
throw Error('ffmpeg.wasm can only run one command at a time');
} else {
this.running = true;
return new Promise((resolve) => {
const args = [...defaultArgs, ..._args].filter((s) => s.length !== 0);
this.runResolve = resolve;
this.ffmpeg(...FFmpeg.parseArgs(this.core, args));
});
}
}
exit() {
if (this.core === null) {
throw NO_LOAD;
} else {
this.running = false;
this.core.exit(1);
this.core = null;
this.ffmpeg = null;
this.runResolve = null;
}
};
get isLoaded(): boolean {
return this.core !== null;
}
private parseMessage({ type, message }) {
this.log(type, message);
this.parseProgress(message, this.settings.progress);
this.detectCompletion(message);
};
private detectCompletion(message) {
if (message === 'FFMPEG_END' && this.runResolve !== null) {
this.runResolve();
this.runResolve = null;
this.running = false;
}
};
private static parseArgs(Core, args) {
const argsPtr = Core._malloc(args.length * Uint32Array.BYTES_PER_ELEMENT);
args.forEach((s, idx) => {
const buf = Core._malloc(s.length + 1);
Core.writeAsciiToMemory(s, buf);
Core.setValue(argsPtr + (Uint32Array.BYTES_PER_ELEMENT * idx), buf, 'i32');
});
return [args.length, argsPtr];
};
private ts2sec(ts) {
const [h, m, s] = ts.split(':');
return (parseFloat(h) * 60 * 60) + (parseFloat(m) * 60) + parseFloat(s);
};
private parseProgress(message, progress) {
if (typeof message === 'string') {
if (message.startsWith(' Duration')) {
const ts = message.split(', ')[0].split(': ')[1];
const d = this.ts2sec(ts);
progress({ duration: d, ratio: this.ratio });
if (this.duration === 0 || this.duration > d) {
this.duration = d;
}
} else if (message.startsWith('frame') || message.startsWith('size')) {
const ts = message.split('time=')[1].split(' ')[0];
const t = this.ts2sec(ts);
this.ratio = t / this.duration;
progress({ ratio: this.ratio, time: t });
} else if (message.startsWith('video:')) {
progress({ ratio: 1 });
this.duration = 0;
}
}
}
private log(type, message) {
if (this.settings.logger)
this.settings.logger({ type, message })
if (this.settings.log)
console.log(type, message)
}
async toBlobURL(url, mimeType) {
this.log('info', `fetch ${url}`);
const buf = await (await fetch(url)).arrayBuffer();
this.log('info', `${url} file size = ${buf.byteLength} bytes`);
const blob = new Blob([buf], { type: mimeType });
const blobURL = URL.createObjectURL(blob);
this.log('info', `${url} blob URL = ${blobURL}`);
return blobURL;
};
async getCreateFFmpegCore({ corePath: _corePath }): Promise<{
createFFmpegCore: any,
corePath: string,
wasmPath: string,
workerPath: string,
}> {
if (typeof _corePath !== 'string') {
throw Error('corePath should be a string!');
}
// const coreRemotePath = self.location.host +_corePath
const coreRemotePath = self.location.origin + _corePath
const corePath = await this.toBlobURL(
coreRemotePath,
'application/javascript',
);
const wasmPath = await this.toBlobURL(
coreRemotePath.replace('ffmpeg-core.js', 'ffmpeg-core.wasm'),
'application/wasm',
);
const workerPath = await this.toBlobURL(
coreRemotePath.replace('ffmpeg-core.js', 'ffmpeg-core.worker.js'),
'application/javascript',
);
if (typeof createFFmpegCore === 'undefined') {
return new Promise((resolve) => {
globalThis.importScripts(corePath);
if (typeof createFFmpegCore === 'undefined') {
throw Error("CREATE_FFMPEG_CORE_IS_NOT_DEFINED");
}
this.log('info', 'ffmpeg-core.js script loaded');
resolve({
createFFmpegCore,
corePath,
wasmPath,
workerPath,
});
});
}
this.log('info', 'ffmpeg-core.js script is loaded already');
return Promise.resolve({
createFFmpegCore,
corePath,
wasmPath,
workerPath,
});
};
}
const NO_LOAD = Error('ffmpeg.wasm is not ready, make sure you have completed load().');
const defaultArgs = [
/* args[0] is always the binary path */
'./ffmpeg',
/* Disable interaction mode */
'-nostdin',
/* Force to override output file */
'-y',
]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment