|
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'); |
4 | 3 | const parseProgress = require('./utils/parseProgress');
|
5 |
| -const stringList2pointer = require('./utils/stringList2pointer'); |
6 | 4 | const parseArgs = require('./utils/parseArgs');
|
7 |
| -const { |
8 |
| - defaultOptions, |
9 |
| - getModule, |
10 |
| - fetchFile, |
11 |
| -} = require('./node'); |
| 5 | +const { defaultOptions, getCreateFFmpegCore } = require('./node'); |
12 | 6 |
|
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().'); |
17 | 8 |
|
18 | 9 | module.exports = (_options = {}) => {
|
| 10 | + let Core = null; |
| 11 | + let ffmpeg = null; |
19 | 12 | let runResolve = null;
|
20 | 13 | let running = false;
|
21 | 14 | const {
|
22 | 15 | log: logging,
|
23 | 16 | logger,
|
24 | 17 | progress,
|
25 | 18 | ...options
|
26 |
| - } = resolvePaths({ |
| 19 | + } = { |
| 20 | + ...baseOptions, |
27 | 21 | ...defaultOptions,
|
28 | 22 | ..._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) { |
34 | 26 | runResolve();
|
35 | 27 | runResolve = null;
|
36 | 28 | running = false;
|
37 | 29 | }
|
38 | 30 | };
|
| 31 | + const parseMessage = ({ type, message }) => { |
| 32 | + log(type, message); |
| 33 | + parseProgress(message, progress); |
| 34 | + detectCompletion(message); |
| 35 | + }; |
39 | 36 |
|
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 | + */ |
42 | 48 | 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 }), |
51 | 56 | });
|
52 |
| - if (ffmpeg === null) { |
53 |
| - ffmpeg = Module.cwrap('proxy_main', 'number', ['number', 'number']); |
54 |
| - } |
| 57 | + ffmpeg = Core.cwrap('proxy_main', 'number', ['number', 'number']); |
55 | 58 | log('info', 'ffmpeg-core loaded');
|
56 |
| - } |
57 |
| - }; |
58 |
| - |
59 |
| - const FS = (method, args) => { |
60 |
| - if (Module === null) { |
61 |
| - throw NO_LOAD; |
62 | 59 | } 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.'); |
65 | 61 | }
|
66 | 62 | };
|
67 | 63 |
|
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 |
| - ); |
79 | 64 |
|
80 |
| - const remove = (path) => ( |
81 |
| - FS('unlink', [path]) |
82 |
| - ); |
| 65 | + /* |
| 66 | + * Determine whether the Core is loaded. |
| 67 | + */ |
| 68 | + const isLoaded = () => Core !== null; |
83 | 69 |
|
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) { |
90 | 91 | throw NO_LOAD;
|
91 | 92 | } else if (running) {
|
92 |
| - throw NO_MULTIPLE_RUN; |
| 93 | + throw Error('ffmpeg.wasm can only run one command at a time'); |
93 | 94 | } else {
|
94 | 95 | running = true;
|
95 | 96 | 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); |
98 | 98 | runResolve = resolve;
|
99 |
| - ffmpeg(args.length, stringList2pointer(Module, args)); |
| 99 | + ffmpeg(...parseArgs(Core, args)); |
100 | 100 | });
|
101 | 101 | }
|
102 | 102 | };
|
103 | 103 |
|
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 | + } |
116 | 138 | };
|
117 | 139 |
|
| 140 | + setLogging(logging); |
| 141 | + setCustomLogger(logger); |
| 142 | + |
118 | 143 | return {
|
119 | 144 | load,
|
120 |
| - FS, |
121 |
| - write, |
122 |
| - writeText, |
123 |
| - read, |
124 |
| - remove, |
125 |
| - ls, |
| 145 | + isLoaded, |
126 | 146 | run,
|
127 |
| - transcode, |
128 |
| - trim, |
129 |
| - concatDemuxer, |
| 147 | + FS, |
130 | 148 | };
|
131 | 149 | };
|
0 commit comments