Skip to content

Commit ee64838

Browse files
committed
Complete major refactor
1 parent b5a47b1 commit ee64838

21 files changed

+248
-239
lines changed

src/browser/defaultOptions.js

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
const resolveURL = require('resolve-url');
2-
const { dependencies } = require('../../package.json');
3-
const defaultOptions = require('../constants/defaultOptions');
2+
const { devDependencies } = require('../../package.json');
43

54
/*
6-
* Default options for browser worker
5+
* Default options for browser environment
76
*/
87
module.exports = {
9-
...defaultOptions,
108
corePath: (typeof process !== 'undefined' && process.env.FFMPEG_ENV === 'development')
11-
? resolveURL('/node_modules/@ffmpeg/core/ffmpeg-core.js')
12-
: `https://unpkg.com/@ffmpeg/core@v${dependencies['@ffmpeg/core'].substring(1)}/ffmpeg-core.js`,
9+
? resolveURL('/node_modules/@ffmpeg/core/dist/ffmpeg-core.js')
10+
: `https://unpkg.com/@ffmpeg/core@v${devDependencies['@ffmpeg/core'].substring(1)}/ffmpeg-core.js`,
1311
};

src/browser/fetchFile.js

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,5 @@
11
const resolveURL = require('resolve-url');
22

3-
/**
4-
* readFromBlobOrFile
5-
*
6-
* @name readFromBlobOrFile
7-
* @function
8-
* @access private
9-
*/
103
const readFromBlobOrFile = (blob) => (
114
new Promise((resolve, reject) => {
125
const fileReader = new FileReader();
@@ -23,19 +16,21 @@ const readFromBlobOrFile = (blob) => (
2316
module.exports = async (_data) => {
2417
let data = _data;
2518
if (typeof _data === 'undefined') {
26-
return 'undefined';
19+
return new Uint8Array();
2720
}
2821

2922
if (typeof _data === 'string') {
30-
// Base64 _data
23+
/* From base64 format */
3124
if (/data:_data\/([a-zA-Z]*);base64,([^"]*)/.test(_data)) {
3225
data = atob(_data.split(',')[1])
3326
.split('')
3427
.map((c) => c.charCodeAt(0));
28+
/* From remote server/URL */
3529
} else {
3630
const res = await fetch(resolveURL(_data));
3731
data = await res.arrayBuffer();
3832
}
33+
/* From Blob or File */
3934
} else if (_data instanceof File || _data instanceof Blob) {
4035
data = await readFromBlobOrFile(_data);
4136
}
Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
const resolveURL = require('resolve-url');
12
const { log } = require('../utils/log');
23

3-
module.exports = async ({ corePath }) => {
4-
if (typeof window.Module === 'undefined') {
4+
module.exports = async ({ corePath: _corePath }) => {
5+
if (typeof window.createFFmpegCore === 'undefined') {
56
log('info', 'fetch ffmpeg-core.worker.js script');
7+
const corePath = resolveURL(_corePath);
68
const workerBlob = await (await fetch(corePath.replace('ffmpeg-core.js', 'ffmpeg-core.worker.js'))).blob();
79
window.FFMPEG_CORE_WORKER_SCRIPT = URL.createObjectURL(workerBlob);
810
log('info', `worker object URL=${window.FFMPEG_CORE_WORKER_SCRIPT}`);
@@ -12,10 +14,7 @@ module.exports = async ({ corePath }) => {
1214
const eventHandler = () => {
1315
script.removeEventListener('load', eventHandler);
1416
log('info', 'initialize ffmpeg-core');
15-
window.Module.onRuntimeInitialized = () => {
16-
log('info', 'ffmpeg-core initialized');
17-
resolve(window.Module);
18-
};
17+
resolve(window.createFFmpegCore);
1918
};
2019
script.src = corePath;
2120
script.type = 'text/javascript';
@@ -24,5 +23,5 @@ module.exports = async ({ corePath }) => {
2423
});
2524
}
2625
log('info', 'ffmpeg-core is loaded already');
27-
return Promise.resolve(window.Module);
26+
return Promise.resolve(window.createFFmpegCore);
2827
};

src/browser/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
const defaultOptions = require('./defaultOptions');
2-
const getModule = require('./getModule');
2+
const getCreateFFmpegCore = require('./getCreateFFmpegCore');
33
const fetchFile = require('./fetchFile');
44

55
module.exports = {
66
defaultOptions,
7-
getModule,
7+
getCreateFFmpegCore,
88
fetchFile,
99
};

src/config.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
module.exports = {
2+
defaultArgs: [
3+
/* args[0] is always the binary path */
4+
'./ffmpeg',
5+
/* Disable interaction mode */
6+
'-nostdin',
7+
/* Force to override output file */
8+
'-y',
9+
/* Not to output banner */
10+
'-hide_banner',
11+
],
12+
baseOptions: {
13+
/* Flag to turn on/off log messages in console */
14+
log: false,
15+
/*
16+
* Custom logger to get ffmpeg.wasm output messages.
17+
* a sample logger looks like this:
18+
*
19+
* ```
20+
* logger = ({ type, message }) => {
21+
* console.log(type, message);
22+
* }
23+
* ```
24+
*
25+
* type can be one of following:
26+
*
27+
* info: internal workflow debug messages
28+
* fferr: ffmpeg native stderr output
29+
* ffout: ffmpeg native stdout output
30+
*/
31+
logger: () => {},
32+
/*
33+
* Progress handler to get current progress of ffmpeg command.
34+
* a sample progress handler looks like this:
35+
*
36+
* ```
37+
* progress = ({ ratio }) => {
38+
* console.log(ratio);
39+
* }
40+
* ```
41+
*
42+
* ratio is a float number between 0 to 1.
43+
*/
44+
progress: () => {},
45+
/*
46+
* Path to find/download ffmpeg.wasm-core,
47+
* this value should be overwriten by `defaultOptions` in
48+
* each environment.
49+
*/
50+
corePath: '',
51+
},
52+
};

src/constants/defaultArgs.js

Lines changed: 0 additions & 5 deletions
This file was deleted.

src/constants/defaultOptions.js

Lines changed: 0 additions & 5 deletions
This file was deleted.

src/createFFmpeg.js

Lines changed: 103 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,131 +1,149 @@
1-
const defaultArgs = require('./constants/defaultArgs');
2-
const { setLogging, log } = require('./utils/log');
3-
const resolvePaths = require('./utils/resolvePaths');
1+
const { defaultArgs, baseOptions } = require('./config');
2+
const { setLogging, setCustomLogger, log } = require('./utils/log');
43
const parseProgress = require('./utils/parseProgress');
5-
const stringList2pointer = require('./utils/stringList2pointer');
64
const parseArgs = require('./utils/parseArgs');
7-
const {
8-
defaultOptions,
9-
getModule,
10-
fetchFile,
11-
} = require('./node');
5+
const { defaultOptions, getCreateFFmpegCore } = require('./node');
126

13-
const NO_LOAD = Error('FFmpeg.js is not ready, make sure you have completed load().');
14-
const NO_MULTIPLE_RUN = Error('FFmpeg.js can only run one command at a time');
15-
let Module = null;
16-
let ffmpeg = null;
7+
const NO_LOAD = Error('ffmpeg.wasm is not ready, make sure you have completed load().');
178

189
module.exports = (_options = {}) => {
10+
let Core = null;
11+
let ffmpeg = null;
1912
let runResolve = null;
2013
let running = false;
2114
const {
2215
log: logging,
2316
logger,
2417
progress,
2518
...options
26-
} = resolvePaths({
19+
} = {
20+
...baseOptions,
2721
...defaultOptions,
2822
..._options,
29-
});
30-
const detectCompletion = ({ message, type }) => {
31-
if (type === 'ffmpeg-stdout'
32-
&& message === 'FFMPEG_END'
33-
&& runResolve !== null) {
23+
};
24+
const detectCompletion = (message) => {
25+
if (message === 'FFMPEG_END' && runResolve !== null) {
3426
runResolve();
3527
runResolve = null;
3628
running = false;
3729
}
3830
};
31+
const parseMessage = ({ type, message }) => {
32+
log(type, message);
33+
parseProgress(message, progress);
34+
detectCompletion(message);
35+
};
3936

40-
setLogging(logging);
41-
37+
/*
38+
* Load ffmpeg.wasm-core script.
39+
* In browser environment, the ffmpeg.wasm-core script is fetch from
40+
* CDN and can be assign to a local path by assigning `corePath`.
41+
* In node environment, we use dynamic require and the default `corePath`
42+
* is `$ffmpeg/core`.
43+
*
44+
* Typically the load() func might take few seconds to minutes to complete,
45+
* better to do it as early as possible.
46+
*
47+
*/
4248
const load = async () => {
43-
if (Module === null) {
44-
log('info', 'load ffmpeg-core');
45-
Module = await getModule(options);
46-
Module.setLogger((_log) => {
47-
detectCompletion(_log);
48-
parseProgress(_log, progress);
49-
logger(_log);
50-
log(_log.type, _log.message);
49+
log('info', 'load ffmpeg-core');
50+
if (Core === null) {
51+
log('info', 'loading ffmpeg-core');
52+
const createFFmpegCore = await getCreateFFmpegCore(options);
53+
Core = await createFFmpegCore({
54+
printErr: (message) => parseMessage({ type: 'fferr', message }),
55+
print: (message) => parseMessage({ type: 'ffout', message }),
5156
});
52-
if (ffmpeg === null) {
53-
ffmpeg = Module.cwrap('proxy_main', 'number', ['number', 'number']);
54-
}
57+
ffmpeg = Core.cwrap('proxy_main', 'number', ['number', 'number']);
5558
log('info', 'ffmpeg-core loaded');
56-
}
57-
};
58-
59-
const FS = (method, args) => {
60-
if (Module === null) {
61-
throw NO_LOAD;
6259
} else {
63-
log('info', `FS.${method} ${args[0]}`);
64-
return Module.FS[method](...args);
60+
throw Error('ffmpeg.wasm was loaded, you should not load it again, use ffmpeg.isLoaded() to check next time.');
6561
}
6662
};
6763

68-
const write = async (path, data) => (
69-
FS('writeFile', [path, await fetchFile(data)])
70-
);
71-
72-
const writeText = (path, text) => (
73-
FS('writeFile', [path, text])
74-
);
75-
76-
const read = (path) => (
77-
FS('readFile', [path])
78-
);
7964

80-
const remove = (path) => (
81-
FS('unlink', [path])
82-
);
65+
/*
66+
* Determine whether the Core is loaded.
67+
*/
68+
const isLoaded = () => Core !== null;
8369

84-
const ls = (path) => (
85-
FS('readdir', [path])
86-
);
87-
88-
const run = (_args) => {
89-
if (ffmpeg === null) {
70+
/*
71+
* Run ffmpeg command.
72+
* This is the major function in ffmpeg.wasm, you can just imagine it
73+
* as ffmpeg native cli and what you need to pass is the same.
74+
*
75+
* For example, you can convert native command below:
76+
*
77+
* ```
78+
* $ ffmpeg -i video.avi -c:v libx264 video.mp4
79+
* ```
80+
*
81+
* To
82+
*
83+
* ```
84+
* await ffmpeg.run('-i', 'video.avi', '-c:v', 'libx264', 'video.mp4');
85+
* ```
86+
*
87+
*/
88+
const run = (..._args) => {
89+
log('info', `run ffmpeg command: ${_args.join(' ')}`);
90+
if (Core === null) {
9091
throw NO_LOAD;
9192
} else if (running) {
92-
throw NO_MULTIPLE_RUN;
93+
throw Error('ffmpeg.wasm can only run one command at a time');
9394
} else {
9495
running = true;
9596
return new Promise((resolve) => {
96-
const args = [...defaultArgs, ...parseArgs(_args)].filter((s) => s.length !== 0);
97-
log('info', `ffmpeg command: ${args.join(' ')}`);
97+
const args = [...defaultArgs, ..._args].filter((s) => s.length !== 0);
9898
runResolve = resolve;
99-
ffmpeg(args.length, stringList2pointer(Module, args));
99+
ffmpeg(...parseArgs(Core, args));
100100
});
101101
}
102102
};
103103

104-
const transcode = (input, output, opts = '') => (
105-
run(`-i ${input} ${opts} ${output}`)
106-
);
107-
108-
const trim = (input, output, from, to, opts = '') => (
109-
run(`-i ${input} -ss ${from} -to ${to} ${opts} ${output}`)
110-
);
111-
112-
const concatDemuxer = (input, output, opts = '') => {
113-
const text = input.reduce((acc, path) => `${acc}\nfile ${path}`, '');
114-
writeText('concat_list.txt', text);
115-
return run(`-f concat -safe 0 -i concat_list.txt ${opts} ${output}`);
104+
/*
105+
* Run FS operations.
106+
* For input/output file of ffmpeg.wasm, it is required to save them to MEMFS
107+
* first so that ffmpeg.wasm is able to consume them. Here we rely on the FS
108+
* methods provided by Emscripten.
109+
*
110+
* Common methods to use are:
111+
* ffmpeg.FS('writeFile', 'video.avi', new Uint8Array(...)): writeFile writes
112+
* data to MEMFS. You need to use Uint8Array for binary data.
113+
* ffmpeg.FS('readFile', 'video.mp4'): readFile from MEMFS.
114+
* ffmpeg.FS('unlink', 'video.map'): delete file from MEMFS.
115+
*
116+
* For more info, check https://emscripten.org/docs/api_reference/Filesystem-API.html
117+
*
118+
*/
119+
const FS = (method, ...args) => {
120+
log('info', `run FS.${method} ${args.map((arg) => (typeof arg === 'string' ? arg : `<${arg.length} bytes binary file>`)).join(' ')}`);
121+
if (Core === null) {
122+
throw NO_LOAD;
123+
} else {
124+
let ret = null;
125+
try {
126+
ret = Core.FS[method](...args);
127+
} catch (e) {
128+
if (method === 'readdir') {
129+
throw Error(`ffmpeg.FS('readdir', '${args[0]}') error. Check if the path exists, ex: ffmpeg.FS('readdir', '/')`);
130+
} else if (method === 'readFile') {
131+
throw Error(`ffmpeg.FS('readFile', '${args[0]}') error. Check if the path exists`);
132+
} else {
133+
throw Error('Oops, something went wrong in FS operation.');
134+
}
135+
}
136+
return ret;
137+
}
116138
};
117139

140+
setLogging(logging);
141+
setCustomLogger(logger);
142+
118143
return {
119144
load,
120-
FS,
121-
write,
122-
writeText,
123-
read,
124-
remove,
125-
ls,
145+
isLoaded,
126146
run,
127-
transcode,
128-
trim,
129-
concatDemuxer,
147+
FS,
130148
};
131149
};

0 commit comments

Comments
 (0)