Last active
October 13, 2024 16:58
-
-
Save Chris1234567899/a00afe5e2de1beb1cb4053cbbafc4fe8 to your computer and use it in GitHub Desktop.
Angular/Typescript adjustments of ffmpeg-core for ffmpeg.wasm with web workers
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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() | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
Could you make a guide please?