Major refactor to adapt new ffmpeg-core.js

This commit is contained in:
Jerome Wu 2020-04-28 19:35:57 +08:00
parent cd5fe43905
commit b36360f16f
40 changed files with 226 additions and 527 deletions

6
package-lock.json generated
View File

@ -2034,9 +2034,9 @@
}
},
"@ffmpeg/core": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@ffmpeg/core/-/core-0.6.0.tgz",
"integrity": "sha512-5VNyabaCPZJEFmn92C5SIh2vFChL2l9OVGut1hmst8IPuChZahF6yniFjEdF2Ri8ZZFKXTAQrPh3WfpVaEPK9A=="
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/@ffmpeg/core/-/core-0.7.0.tgz",
"integrity": "sha512-ulDKwckLF4NrNIn98sYe8KCzKM9GfIfc9kR2Mel4pA8URQ9GEr2tGDMST5xW63Usnr6ybZCZKgwiDkAHK7VWCQ=="
},
"@hapi/address": {
"version": "2.1.2",

View File

@ -14,13 +14,13 @@
"wait": "rimraf dist && wait-on http://localhost:3000/dist/ffmpeg.dev.js",
"test": "npm-run-all -p -r start test:all",
"test:all": "npm-run-all wait test:browser:ffmpeg test:node:all",
"test:node": "NODE_OPTIONS=--experimental-worker nyc mocha --exit --bail --require ./scripts/test-helper.js",
"test:node": "node --experimental-wasm-threads --experimental-wasm-bulk-memory node_modules/.bin/_mocha --exit --bail --require ./scripts/test-helper.js",
"test:node:all": "npm run test:node -- ./tests/*.test.js",
"test:browser": "mocha-headless-chrome -a incognito -a no-sandbox -a disable-setuid-sandbox -a disable-logging -t 300000",
"test:browser": "mocha-headless-chrome -a allow-file-access-from-files -a incognito -a no-sandbox -a disable-setuid-sandbox -a disable-logging -t 300000",
"test:browser:ffmpeg": "npm run test:browser -- -f ./tests/ffmpeg.test.html"
},
"browser": {
"./src/worker/node/index.js": "./src/worker/browser/index.js"
"./src/node/index.js": "./src/browser/index.js"
},
"repository": {
"type": "git",
@ -37,11 +37,11 @@
"url": "https://github.com/ffmpegjs/ffmpeg.js/issues"
},
"engines": {
"node": ">=10.5.0"
"node": ">=12.16.1"
},
"homepage": "https://github.com/ffmpegjs/ffmpeg.js#readme",
"dependencies": {
"@ffmpeg/core": "^0.6.0",
"@ffmpeg/core": "^0.7.0",
"idb": "^4.0.5",
"is-electron": "^2.2.0",
"is-url": "^1.2.4",

View File

@ -32,8 +32,4 @@ module.exports = [
library: 'FFmpeg',
libraryTarget: 'umd',
}),
genConfig({
entry: path.resolve(__dirname, '..', 'src', 'worker-script', 'browser', 'index.js'),
filename: 'worker.dev.js',
}),
];

View File

@ -23,8 +23,4 @@ module.exports = [
library: 'FFmpeg',
libraryTarget: 'umd',
}),
genConfig({
entry: path.resolve(__dirname, '..', 'src', 'worker-script', 'browser', 'index.js'),
filename: 'worker.min.js',
}),
];

View File

@ -0,0 +1,13 @@
const resolveURL = require('resolve-url');
const { dependencies } = require('../../package.json');
const defaultOptions = require('../constants/defaultOptions');
/*
* Default options for browser worker
*/
module.exports = {
...defaultOptions,
corePath: (typeof process !== 'undefined' && process.env.FFMPEG_ENV === 'development')
? resolveURL('/node_modules/@ffmpeg/core/ffmpeg-core.js')
: `https://unpkg.com/@ffmpeg/core@v${dependencies['@ffmpeg/core'].substring(1)}/ffmpeg-core.js`,
};

23
src/browser/getModule.js Normal file
View File

@ -0,0 +1,23 @@
const { log } = require('../utils/log');
module.exports = ({ corePath }) => new Promise((resolve) => {
if (typeof window.Module === 'undefined') {
log('info', `download ffmpeg-core script (~25 MB) from ${corePath}`);
const script = document.createElement('script');
const eventHandler = () => {
script.removeEventListener('load', eventHandler);
log('info', 'initialize ffmpeg-core');
window.Module.onRuntimeInitialized = () => {
log('info', 'ffmpeg-core initialized');
resolve(window.Module);
};
};
script.src = corePath;
script.type = 'text/javascript';
script.addEventListener('load', eventHandler);
document.getElementsByTagName('head')[0].appendChild(script);
} else {
log('info', 'ffmpeg-core is loaded already');
resolve(window.Module);
}
});

View File

@ -1,11 +1,9 @@
const defaultOptions = require('./defaultOptions');
const spawnWorker = require('./spawnWorker');
const onMessage = require('./onMessage');
const getModule = require('./getModule');
const fetchFile = require('./fetchFile');
module.exports = {
defaultOptions,
spawnWorker,
onMessage,
getModule,
fetchFile,
};

View File

@ -1,4 +1,5 @@
module.exports = [
'./ffmpeg', // args[0] is always binary path
'-nostdin', // Disable interaction mode
'-hide_banner', // Not to output banner
];

View File

@ -1,4 +1,5 @@
module.exports = {
log: false,
logger: () => {},
progress: () => {},
};

130
src/createFFmpeg.js Normal file
View File

@ -0,0 +1,130 @@
const defaultArgs = require('./constants/defaultArgs');
const { setLogging, log } = require('./utils/log');
const resolvePaths = require('./utils/resolvePaths');
const parseProgress = require('./utils/parseProgress');
const stringList2pointer = require('./utils/stringList2pointer');
const {
defaultOptions,
getModule,
fetchFile,
} = require('./node');
const NO_LOAD = Error('FFmpeg.js is not ready, make sure you have completed load().');
const NO_MULTIPLE_RUN = Error('FFmpeg.js can only run one command at a time');
let Module = null;
let ffmpeg = null;
module.exports = (_options = {}) => {
let runResolve = null;
let running = false;
const {
log: logging,
logger,
progress,
...options
} = resolvePaths({
...defaultOptions,
..._options,
});
const detectCompletion = ({ message, type }) => {
if (type === 'ffmpeg-stdout'
&& message === 'FFMPEG_END'
&& runResolve !== null) {
runResolve();
runResolve = null;
running = false;
}
};
setLogging(logging);
const load = async () => {
if (Module === null) {
log('info', 'load ffmpeg-core');
Module = await getModule(options);
Module.setLogger((_log) => {
detectCompletion(_log);
parseProgress(_log, progress);
logger(_log);
log(_log.type, _log.message);
});
if (ffmpeg === null) {
ffmpeg = Module.cwrap('proxy_main', 'number', ['number', 'number']);
}
log('info', 'ffmpeg-core loaded');
}
};
const FS = (method, args) => {
if (Module === null) {
throw NO_LOAD;
} else {
log('info', `FS.${method} ${args[0]}`);
return Module.FS[method](...args);
}
};
const write = async (path, data) => (
FS('writeFile', [path, await fetchFile(data)])
);
const writeText = (path, text) => (
FS('writeFile', [path, text])
);
const read = (path) => (
FS('readFile', [path])
);
const remove = (path) => (
FS('unlink', [path])
);
const ls = (path) => (
FS('readir', [path])
);
const run = (_args) => {
if (ffmpeg === null) {
throw NO_LOAD;
} else if (running) {
throw NO_MULTIPLE_RUN;
} else {
running = true;
return new Promise((resolve) => {
const args = [...defaultArgs, ..._args.trim().split(' ')].filter((s) => s.length !== 0);
log('info', `ffmpeg command: ${args.join(' ')}`);
runResolve = resolve;
ffmpeg(args.length, stringList2pointer(Module, args));
});
}
};
const transcode = (input, output, opts = '') => (
run(`-i ${input} ${opts} ${output}`)
);
const trim = (input, output, from, to, opts = '') => (
run(`-i ${input} -ss ${from} -to ${to} ${opts} ${output}`)
);
const concatDemuxer = (input, output, opts = '') => {
const text = input.reduce((acc, path) => `${acc}\nfile ${path}`, '');
writeText('concat_list.txt', text);
return run(`-f concat -safe 0 -i concat_list.txt ${opts} ${output}`);
};
return {
load,
FS,
write,
writeText,
read,
remove,
ls,
run,
transcode,
trim,
concatDemuxer,
};
};

View File

@ -1,21 +0,0 @@
const getId = require('./utils/getId');
let jobCounter = 0;
module.exports = ({
id: _id,
action,
payload = {},
}) => {
let id = _id;
if (typeof id === 'undefined') {
id = getId('Job', jobCounter);
jobCounter += 1;
}
return {
id,
action,
payload,
};
};

View File

@ -1,217 +0,0 @@
const createJob = require('./createJob');
const { log } = require('./utils/log');
const getId = require('./utils/getId');
const parseProgress = require('./utils/parseProgress');
const resolvePaths = require('./utils/resolvePaths');
const getTransferables = require('./utils/getTransferables');
const {
defaultOptions,
spawnWorker,
onMessage,
fetchFile,
} = require('./worker/node');
let workerCounter = 0;
module.exports = (_options = {}) => {
const id = getId('Worker', workerCounter);
const {
logger,
progress,
...options
} = resolvePaths({
...defaultOptions,
..._options,
});
const resolves = {};
const rejects = {};
let worker = spawnWorker(options);
workerCounter += 1;
const setResolve = (action, res) => {
resolves[action] = res;
};
const setReject = (action, rej) => {
rejects[action] = rej;
};
const startJob = ({ id: jobId, action, payload }) => (
new Promise((resolve, reject) => {
const packet = {
workerId: id,
jobId,
action,
payload,
};
log(`[${id}]: Start ${jobId}, action=${action}`);
setResolve(action, resolve);
setReject(action, reject);
/*
* By using Transferable in postMessage, we are able
* to send large files to worker
* @ref: https://github.com/ffmpegjs/ffmpeg.js/issues/8#issuecomment-572230128
*/
worker.postMessage(packet, getTransferables(packet));
})
);
const load = (jobId) => (
startJob(createJob({
id: jobId, action: 'load', payload: { options },
}))
);
const write = async (path, data, jobId) => (
startJob(createJob({
id: jobId,
action: 'FS',
payload: {
method: 'writeFile',
args: [path, await fetchFile(data)],
},
}))
);
const writeText = (path, text, jobId) => (
startJob(createJob({
id: jobId,
action: 'FS',
payload: {
method: 'writeFile',
args: [path, text],
},
}))
);
const read = (path, jobId) => (
startJob(createJob({
id: jobId,
action: 'FS',
payload: {
method: 'readFile',
args: [path],
},
}))
);
const remove = (path, jobId) => (
startJob(createJob({
id: jobId,
action: 'FS',
payload: {
method: 'unlink',
args: [path],
},
}))
);
const run = (args, jobId) => (
startJob(createJob({
id: jobId,
action: 'run',
payload: {
args,
},
}))
);
const ls = (path, jobId) => (
startJob(createJob({
id: jobId,
action: 'FS',
payload: {
method: 'readdir',
args: [path],
},
}))
);
const transcode = (input, output, opts = '', jobId) => (
run(
`-i ${input} ${opts} ${output}`,
jobId,
)
);
const trim = (input, output, from, to, opts = '', jobId) => (
run(
`-i ${input} -ss ${from} -to ${to} ${opts} ${output}`,
jobId,
)
);
const concatDemuxer = async (input, output, opts = '', jobId) => {
const text = input.reduce((acc, path) => `${acc}\nfile ${path}`, '');
await writeText('concat_list.txt', text);
return run(`-f concat -safe 0 -i concat_list.txt ${opts} ${output}`, jobId);
};
const terminate = async () => {
if (worker !== null) {
/*
await startJob(createJob({
id: jobId,
action: 'terminate',
}));
*/
worker.terminate();
worker = null;
}
return Promise.resolve();
};
onMessage(worker, ({
workerId, jobId, action, status, payload,
}) => {
if (status === 'resolve') {
const { message, data } = payload;
log(`[${workerId}]: Complete ${jobId}`);
resolves[action]({
workerId,
jobId,
message,
data,
});
} else if (status === 'reject') {
rejects[action](payload);
throw Error(
`${payload}
To get more informaion for debugging, please add logger in createWorker():
const worker = createWorker({
logger: ({ message }) => console.log(message),
});
Even more details:
const { setLogging } = require('@ffmpeg/ffmpeg');
setLogging(true);
`,
);
} else if (status === 'progress') {
parseProgress(payload, progress);
logger(payload);
}
});
return {
id,
worker,
setResolve,
setReject,
load,
write,
writeText,
read,
remove,
ls,
run,
transcode,
trim,
concatDemuxer,
terminate,
};
};

7
src/index.js Executable file → Normal file
View File

@ -1,9 +1,6 @@
require('regenerator-runtime/runtime');
const { logging, setLogging } = require('./utils/log');
const createWorker = require('./createWorker');
const createFFmpeg = require('./createFFmpeg');
module.exports = {
logging,
setLogging,
createWorker,
createFFmpeg,
};

View File

@ -0,0 +1,8 @@
const defaultOptions = require('../constants/defaultOptions');
/*
* Default options for node environment
*/
module.exports = {
...defaultOptions,
};

6
src/node/getModule.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = () => new Promise((resolve) => {
const Module = require('@ffmpeg/core');
Module.onRuntimeInitialized = () => {
resolve(Module);
};
});

9
src/node/index.js Normal file
View File

@ -0,0 +1,9 @@
const defaultOptions = require('./defaultOptions');
const getModule = require('./getModule');
const fetchFile = require('./fetchFile');
module.exports = {
defaultOptions,
getModule,
fetchFile,
};

View File

@ -1,3 +0,0 @@
module.exports = (prefix, cnt) => (
`${prefix}-${cnt}-${Math.random().toString(16).slice(3, 8)}`
);

View File

@ -1,16 +0,0 @@
module.exports = (packet) => {
const transferables = [];
const check = (b) => {
if (b instanceof Uint8Array) {
transferables.push(b.buffer);
} else if (b instanceof ArrayBuffer) {
transferables.push(b);
}
};
const { payload: { args, data } } = packet;
check(data);
if (Array.isArray(args)) {
args.forEach((arg) => check(arg));
}
return transferables;
};

View File

@ -6,4 +6,4 @@ exports.setLogging = (_logging) => {
logging = _logging;
};
exports.log = (...args) => (logging ? console.log.apply(this, args) : null);
exports.log = (type, message) => (logging ? console.log(`[${type}] ${message}`) : null);

View File

@ -3,7 +3,7 @@ const resolveURL = isBrowser ? require('resolve-url') : s => s; // eslint-disabl
module.exports = (options) => {
const opts = { ...options };
['corePath', 'workerPath'].forEach((key) => {
['corePath'].forEach((key) => {
if (typeof options[key] !== 'undefined') {
opts[key] = resolveURL(opts[key]);
}

View File

@ -1,10 +1,10 @@
const str2ptr = require('./str2ptr');
const string2pointer = require('./string2pointer');
module.exports = (Module, strList) => {
const listPtr = Module._malloc(strList.length * Uint32Array.BYTES_PER_ELEMENT);
strList.forEach((s, idx) => {
const strPtr = str2ptr(Module, s);
const strPtr = string2pointer(Module, s);
Module.setValue(listPtr + (4 * idx), strPtr, 'i32');
});

View File

@ -1,5 +0,0 @@
{
"env": {
"worker": true
}
}

View File

@ -1,6 +0,0 @@
module.exports = (corePath) => {
if (typeof global.Module === 'undefined') {
global.importScripts(corePath);
}
return global.Module;
};

View File

@ -1,10 +0,0 @@
const worker = require('..');
const getCore = require('./getCore');
global.addEventListener('message', ({ data }) => {
worker.dispatchHandlers(data, postMessage);
});
worker.setAdapter({
getCore,
});

View File

@ -1,96 +0,0 @@
require('regenerator-runtime/runtime');
const defaultArgs = require('./constants/defaultArgs');
const strList2ptr = require('./utils/strList2ptr');
const getTransferables = require('../utils/getTransferables');
const NO_LOAD_ERROR = 'FFmpegCore is not ready, make sure you have completed Worker.load().';
let action = 'unknown';
let Module = null;
let adapter = null;
let ffmpeg = null;
const load = ({ workerId, payload: { options: { corePath } } }, res) => {
if (Module === null) {
const Core = adapter.getCore(corePath);
Core()
.then(async (_Module) => {
Module = _Module;
Module.setLogger((message, type) => {
res.progress({
workerId, action, type, message,
});
});
ffmpeg = Module.cwrap('ffmpeg', 'number', ['number', 'number']);
res.resolve({ message: 'Loaded ffmpeg-core' });
});
} else {
res.resolve({ message: 'Loaded ffmpeg-core' });
}
};
const FS = ({
payload: {
method,
args,
},
}, res) => {
if (Module === null) {
throw NO_LOAD_ERROR;
} else {
res.resolve({
message: `Complete ${method}`,
data: Module.FS[method](...args),
});
}
};
const run = ({
payload: {
args: _args,
},
}, res) => {
if (Module === null) {
throw NO_LOAD_ERROR;
} else {
const args = [...defaultArgs, ..._args.trim().split(' ')].filter((s) => s.length !== 0);
ffmpeg(args.length, strList2ptr(Module, args));
res.resolve({
message: `Complete ${args.join(' ')}`,
});
}
};
exports.dispatchHandlers = (packet, send) => {
const { workerId, jobId, action: act } = packet;
const res = (status, payload) => {
const pkt = {
workerId,
jobId,
action: act,
status,
payload,
};
send(pkt, getTransferables(pkt));
};
res.resolve = res.bind(this, 'resolve');
res.reject = res.bind(this, 'reject');
res.progress = res.bind(this, 'progress');
action = act;
try {
({
load,
FS,
run,
})[act](packet, res);
} catch (err) {
/** Prepare exception to travel through postMessage */
res.reject(err.toString());
}
action = 'unknown';
};
exports.setAdapter = (_adapter) => {
adapter = _adapter;
};

View File

@ -1,8 +0,0 @@
let FFmpegCore = null;
module.exports = () => {
if (FFmpegCore === null) {
FFmpegCore = require('@ffmpeg/core');
}
return FFmpegCore;
};

View File

@ -1,16 +0,0 @@
const { parentPort } = require('worker_threads');
const worker = require('..');
const getCore = require('./getCore');
parentPort.on('message', (packet) => {
worker.dispatchHandlers(
packet,
(...args) => {
parentPort.postMessage(...args);
},
);
});
worker.setAdapter({
getCore,
});

View File

@ -1,15 +0,0 @@
const resolveURL = require('resolve-url');
const { version, dependencies } = require('../../../package.json');
const defaultOptions = require('../../constants/defaultOptions');
/*
* Default options for browser worker
*/
module.exports = {
...defaultOptions,
workerPath: (typeof process !== 'undefined' && process.env.FFMPEG_ENV === 'development')
? resolveURL(`/dist/worker.dev.js?nocache=${Math.random().toString(36).slice(3)}`)
: `https://unpkg.com/@ffmpeg/ffmpeg@v${version}/dist/worker.min.js`,
corePath: `https://unpkg.com/@ffmpeg/core@v${dependencies['@ffmpeg/core'].substring(1)}/ffmpeg-core.js`,
workerBlobURL: true,
};

View File

@ -1,20 +0,0 @@
/**
*
* Tesseract Worker adapter for browser
*
* @fileoverview Tesseract Worker adapter for browser
* @author Kevin Kwok <antimatter15@gmail.com>
* @author Guillermo Webster <gui@mit.edu>
* @author Jerome Wu <jeromewus@gmail.com>
*/
const defaultOptions = require('./defaultOptions');
const spawnWorker = require('./spawnWorker');
const onMessage = require('./onMessage');
const fetchFile = require('./fetchFile');
module.exports = {
defaultOptions,
spawnWorker,
onMessage,
fetchFile,
};

View File

@ -1,5 +0,0 @@
module.exports = (worker, handler) => {
worker.onmessage = ({ data }) => { // eslint-disable-line
handler(data);
};
};

View File

@ -1,20 +0,0 @@
/**
* spawnWorker
*
* @name spawnWorker
* @function create a new Worker in browser
* @access public
*/
module.exports = ({ workerPath, workerBlobURL }) => {
let worker;
if (Blob && URL && workerBlobURL) {
/* Use Blob to load cross domain worker script */
const blob = new Blob([`importScripts("${workerPath}");`], {
type: 'text/javascript',
});
worker = new Worker(URL.createObjectURL(blob));
} else {
worker = new Worker(workerPath);
}
return worker;
};

View File

@ -1,10 +0,0 @@
const path = require('path');
const defaultOptions = require('../../constants/defaultOptions');
/*
* Default options for node worker
*/
module.exports = {
...defaultOptions,
workerPath: path.join(__dirname, '..', '..', 'worker-script', 'node', 'index.js'),
};

View File

@ -1,3 +0,0 @@
module.exports = (worker, handler) => {
worker.on('message', handler);
};

View File

@ -1,12 +0,0 @@
const { Worker } = require('worker_threads');
/**
* spawnWorker
*
* @name spawnWorker
* @function fork a new worker thread in node
* @access public
*/
module.exports = ({ workerPath }) => (
new Worker(workerPath)
);

BIN
tests/assets/StarWars3.wav Normal file

Binary file not shown.

View File

@ -1,11 +1,9 @@
const TIMEOUT = 30000;
const TIMEOUT = 60000;
const BASE_URL = 'http://localhost:3000/tests/assets';
const IS_BROWSER = typeof window !== 'undefined' && typeof window.document !== 'undefined';
const OPTIONS = {
corePath: '../node_modules/@ffmpeg/core/ffmpeg-core.js',
...(IS_BROWSER ? { workerPath: '../dist/worker.dev.js' } : {}),
};
const FLAME_MP4_LENGTH = 100374;
if (typeof module !== 'undefined') {
module.exports = {
@ -13,6 +11,5 @@ if (typeof module !== 'undefined') {
BASE_URL,
IS_BROWSER,
OPTIONS,
FLAME_MP4_LENGTH,
};
}

View File

@ -1,20 +1,27 @@
const { createWorker } = FFmpeg;
const worker = createWorker(OPTIONS);
const { createFFmpeg } = FFmpeg;
const ffmpeg = createFFmpeg(OPTIONS);
before(async function cb() {
this.timeout(0);
await worker.load();
await ffmpeg.load();
});
describe('transcode()', () => {
describe('should transcode different format', () => {
['flame.avi'].forEach((name) => (
it(`transcode ${name}`, async () => {
await worker.write(name, `${BASE_URL}/${name}`);
await worker.transcode(name, 'output.mp4');
const { data } = await worker.read('output.mp4');
expect(data.length).to.be(FLAME_MP4_LENGTH);
[1, 2, 4].forEach((n) => {
[
{ from: 'flame.avi', to: 'flame.mp4' },
{ from: 'flame.avi', to: 'flame.webm' },
{ from: 'StarWars3.wav', to: 'StarWars3.mp3' },
].forEach(({ from, to }) => (
it(`transcode ${from} to ${to} (${n} threads)`, async () => {
await ffmpeg.write(from, `${BASE_URL}/${from}`);
await ffmpeg.transcode(from, to, `-threads ${n}`);
const data = ffmpeg.read(to);
ffmpeg.remove(to);
expect(data.length).not.to.be(0);
}).timeout(TIMEOUT)
));
});
});
});