diff --git a/docs/api.md b/docs/api.md index 1be2595..907eb0f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -8,6 +8,7 @@ - [Worker.remove](#worker-remove) - [Worker.transcode](#worker-transcode) - [Worker.trim](#worker-trim) + - [Worker.concatDemuxer](#worker-concatDemuxer) - [Worker.run](#worker-run) --- @@ -135,14 +136,14 @@ Worker.remove() removes files in file system, it will be better to delete unused -### Worker.transcode(inputPath, outputPath, options, del, jobId): Promise +### Worker.transcode(input, output, options, del, jobId): Promise Worker.transcode() transcode a video file to another format. **Arguments:** -- `inputPath` input file path, the input file should be written through Worker.write() -- `outputPath` output file path, can be read with Worker.read() later +- `input` input file path, the input file should be written through Worker.write() +- `output` output file path, can be read with Worker.read() later - `options` a string to add extra arguments to ffmpeg - `del` a boolean to determine whether to delete input file after the task is done, default: true - `jobId` check Worker.load() @@ -157,7 +158,7 @@ Worker.transcode() transcode a video file to another format. -### Worker.trim(inputPath, outputPath, from, to, options, del, jobId): Promise +### Worker.trim(input, output, from, to, options, del, jobId): Promise Worker.trim() trims video to specific interval. @@ -181,14 +182,14 @@ Worker.trim() trims video to specific interval. -### Worker.concatDemuxer(inputPaths, outputPath, options, del, jobId): Promise +### Worker.concatDemuxer(input, output, options, del, jobId): Promise Worker.concatDemuxer() concatenates multiple videos using concatDemuxer. This method won't encode the videos again. But it has its limitations. See [Concat demuxer Wiki](https://trac.ffmpeg.org/wiki/Concatenate) **Arguments:** -- `inputPaths` input file paths as an Array, the input files should be written through Worker.write() -- `outputPath` output file path, can be read with Worker.read() later +- `input` input file paths as an Array, the input files should be written through Worker.write() +- `output` output file path, can be read with Worker.read() later - `options` a string to add extra arguments to ffmpeg - `del` a boolean to determine whether to delete input file after the task is done, default: true - `jobId` check Worker.load() @@ -197,7 +198,7 @@ Worker.concatDemuxer() concatenates multiple videos using concatDemuxer. This me ```javascript (async () => { - await worker.trim(["flame-1.avi", "flame-2.avi"], "output.mp4"); + await worker.concatDemuxer(["flame-1.avi", "flame-2.avi"], "output.mp4"); })(); ``` @@ -210,7 +211,10 @@ Worker.run() is similar to FFmpeg cli tool, aims to provide maximum flexiblity f **Arguments:** - `args` a string to represent arguments, note: inputPath must start with `/data/` as worker.write write to this path by default. -- `options` a object to define the value for inputPath, outputPath and del. +- `options` a object to define the value for input, output and del. + - `input` a string or an array of strings to indicate input files, ffmpeg.js deletes these files for you. + - `output` a string or an array of strings to indicate output files, ffmpeg.js moves these files to `/data`, deletes them from MEMFS and you can read them with Worker.read() + - `del` a boolean to determine whether to delete input file after the task is done, default: true - `jobId` check Worker.load() **Examples:** @@ -218,8 +222,8 @@ Worker.run() is similar to FFmpeg cli tool, aims to provide maximum flexiblity f ```javascript (async () => { await worker.run("-i /data/flame.avi -s 1920x1080 output.mp4", { - inputPath: "flame.avi", - outputPath: "output.mp4" + input: "flame.avi", + output: "output.mp4" }); })(); ``` diff --git a/examples/browser/image2video.html b/examples/browser/image2video.html index 76bfb64..831a3dd 100644 --- a/examples/browser/image2video.html +++ b/examples/browser/image2video.html @@ -38,7 +38,7 @@ await worker.write(`tmp.${num}.png`, `../../tests/assets/triangle/tmp.${num}.png`); } message.innerHTML = 'Start transcoding'; - await worker.run('-framerate 30 -pattern_type glob -i /data/*.png -i /data/audio.ogg -c:a copy -shortest -c:v libx264 -pix_fmt yuv420p out.mp4', { outputPath: 'out.mp4' }); + await worker.run('-framerate 30 -pattern_type glob -i /data/*.png -i /data/audio.ogg -c:a copy -shortest -c:v libx264 -pix_fmt yuv420p out.mp4', { output: 'out.mp4' }); const { data } = await worker.read('out.mp4'); await worker.remove('audio.ogg'); for (let i = 0; i < 60; i += 1) { diff --git a/examples/browser/run.html b/examples/browser/run.html index 565d4a0..36c1abe 100644 --- a/examples/browser/run.html +++ b/examples/browser/run.html @@ -33,7 +33,7 @@ await worker.load(); message.innerHTML = 'Start transcoding'; await worker.write(name, files[0]); - await worker.run(`-i /data/${name} output.mp4`, { inputPath: name, outputPath: 'output.mp4' }); + await worker.run(`-i /data/${name} output.mp4`, { input: name, output: 'output.mp4' }); message.innerHTML = 'Complete transcoding'; const { data } = await worker.read('output.mp4'); diff --git a/examples/browser/transcode.html b/examples/browser/transcode.html index bf36b59..cb14c37 100644 --- a/examples/browser/transcode.html +++ b/examples/browser/transcode.html @@ -23,6 +23,7 @@ const { createWorker } = FFmpeg; const worker = createWorker({ corePath: '../../node_modules/@ffmpeg/core/ffmpeg-core.js', + logger: ({ message }) => console.log(message), progress: p => console.log(p), }); @@ -36,7 +37,6 @@ await worker.transcode(name, 'output.mp4'); message.innerHTML = 'Complete transcoding'; const { data } = await worker.read('output.mp4'); - console.log(data); const video = document.getElementById('output-video'); video.src = URL.createObjectURL(new Blob([data.buffer], { type: 'video/mp4' })); diff --git a/examples/browser/trim.html b/examples/browser/trim.html index 1e7ade0..8d8250b 100644 --- a/examples/browser/trim.html +++ b/examples/browser/trim.html @@ -15,7 +15,7 @@ -

Upload a mp4 (x264) video and trim its first 2 seconds and play!

+

Upload a mp4 (x264) video and trim its first 10 seconds and play!


@@ -33,7 +33,7 @@ await worker.load(); message.innerHTML = 'Start trimming'; await worker.write(name, files[0]); - await worker.trim(name, 'output.mp4', 0, 2); + await worker.trim(name, 'output.mp4', 0, 10); message.innerHTML = 'Complete trimming'; const { data } = await worker.read('output.mp4'); diff --git a/examples/node/image2video.js b/examples/node/image2video.js index d9202e6..f705f0c 100755 --- a/examples/node/image2video.js +++ b/examples/node/image2video.js @@ -15,7 +15,7 @@ const worker = createWorker({ await worker.write(`tmp.${num}.png`, `../../tests/assets/triangle/tmp.${num}.png`); } console.log('Start transcoding'); - await worker.run('-framerate 30 -pattern_type glob -i /data/*.png -i /data/audio.ogg -c:a copy -shortest -c:v libx264 -pix_fmt yuv420p out.mp4', { outputPath: 'out.mp4' }); + await worker.run('-framerate 30 -pattern_type glob -i /data/*.png -i /data/audio.ogg -c:a copy -shortest -c:v libx264 -pix_fmt yuv420p out.mp4', { output: 'out.mp4' }); const { data } = await worker.read('out.mp4'); console.log('Complete transcoding'); await worker.remove('audio.ogg'); diff --git a/examples/node/run.js b/examples/node/run.js index c55082f..4946333 100755 --- a/examples/node/run.js +++ b/examples/node/run.js @@ -9,7 +9,7 @@ const worker = createWorker({ await worker.load(); console.log('Start transcoding'); await worker.write('flame.avi', '../../tests/assets/flame.avi'); - await worker.run('-i /data/flame.avi flame.mp4', { inputPath: 'flame.avi', outputPath: 'flame.mp4' }); + await worker.run('-i /data/flame.avi flame.mp4', { input: 'flame.avi', output: 'flame.mp4' }); const { data } = await worker.read('flame.mp4'); console.log('Complete transcoding'); fs.writeFileSync('flame.mp4', Buffer.from(data)); diff --git a/examples/node/trim.js b/examples/node/trim.js index a03a9e5..d9689b7 100755 --- a/examples/node/trim.js +++ b/examples/node/trim.js @@ -9,7 +9,7 @@ const worker = createWorker({ await worker.load(); console.log('Start trimming'); await worker.write('flame.avi', '../../tests/assets/flame.avi'); - await worker.trim('flame.avi', 'flame_trim.avi', 1, 2); + await worker.trim('flame.avi', 'flame_trim.avi', 0, 10); const { data } = await worker.read('flame_trim.avi'); console.log('Complete trimming'); fs.writeFileSync('flame_trim.avi', Buffer.from(data)); diff --git a/src/createWorker.js b/src/createWorker.js index 344d6b8..1459351 100644 --- a/src/createWorker.js +++ b/src/createWorker.js @@ -106,33 +106,35 @@ module.exports = (_options = {}) => { })) ); - const transcode = (inputPath, outputPath, opts = '', del = true, jobId) => ( + const transcode = (input, output, opts = '', del = true, jobId) => ( run( - `${opts} -i /data/${inputPath} ${outputPath}`, - { inputPath, outputPath, del }, + `${opts} -i /data/${input} ${output}`, + { input, output, del }, jobId, ) ); - const trim = (inputPath, outputPath, from, to, opts = '', del = true, jobId) => ( + const trim = (input, output, from, to, opts = '', del = true, jobId) => ( run( - `${opts} -i /data/${inputPath} -ss ${from} -to ${to} -c copy ${outputPath}`, - { inputPath, outputPath, del }, + `${opts} -i /data/${input} -ss ${from} -to ${to} -c copy ${output}`, + { input, output, del }, jobId, ) ); - const concatDemuxer = async (inputPaths, outputPath, opts = '', del = true, jobId) => { - const text = inputPaths.reduce((acc, input) => `${acc}\nfile ${input}`, ''); + const concatDemuxer = async (input, output, opts = '', del = true, jobId) => { + const text = input.reduce((acc, path) => `${acc}\nfile ${path}`, ''); await writeText('concat_list.txt', text); - return run(`${opts} -f concat -safe 0 -i /data/concat_list.txt -c copy ${outputPath}`, - { del, outputPath, inputPaths: [...inputPaths, 'concat_list.txt'] }, + return run(`${opts} -f concat -safe 0 -i /data/concat_list.txt -c copy ${output}`, + { del, output, input: [...input, 'concat_list.txt'] }, jobId); }; const ls = (path, jobId) => ( startJob(createJob({ - id: jobId, action: 'ls', payload: { path }, + id: jobId, + action: 'FS', + payload: { method: 'readdir', args: [path] }, })) ); @@ -154,10 +156,13 @@ module.exports = (_options = {}) => { if (status === 'resolve') { log(`[${workerId}]: Complete ${jobId}`); let d = data; - if (action === 'read') { - d = Uint8Array.from({ ...data, length: Object.keys(data).length }); - } else { - logger(d); + if (action === 'FS') { + const { method, data: _data } = data; + if (method === 'readFile') { + d = Uint8Array.from({ ..._data, length: Object.keys(_data).length }); + } else { + d = _data; + } } resolves[action]({ jobId, data: d }); } else if (status === 'reject') { diff --git a/src/worker-script/index.js b/src/worker-script/index.js index ae90ab8..269a8e7 100644 --- a/src/worker-script/index.js +++ b/src/worker-script/index.js @@ -1,31 +1,12 @@ require('regenerator-runtime/runtime'); const defaultArgs = require('./constants/defaultArgs'); +const strList2ptr = require('./utils/strList2ptr'); let action = 'unknown'; let Module = null; let adapter = null; let ffmpeg = null; -const str2ptr = (s) => { - const ptr = Module._malloc((s.length + 1) * Uint8Array.BYTES_PER_ELEMENT); - for (let i = 0; i < s.length; i += 1) { - Module.setValue(ptr + i, s.charCodeAt(i), 'i8'); - } - Module.setValue(ptr + s.length, 0, 'i8'); - return ptr; -}; - -const strList2ptr = (strList) => { - const listPtr = Module._malloc(strList.length * Uint32Array.BYTES_PER_ELEMENT); - - strList.forEach((s, idx) => { - const strPtr = str2ptr(s); - Module.setValue(listPtr + (4 * idx), strPtr, 'i32'); - }); - - return listPtr; -}; - const load = ({ workerId, payload: { options: { corePath } } }, res) => { if (Module == null) { const Core = adapter.getCore(corePath); @@ -54,33 +35,57 @@ const syncfs = async ({ res.resolve({ message: `Sync file system with populate=${populate}` }); }; -const ls = ({ +const FS = ({ payload: { - path, + method, + args, }, }, res) => { - const dirs = Module.FS.readdir(path); - res.resolve({ message: `List path ${path}`, dirs }); + const data = Module.FS[method](...args); + res.resolve({ + message: `${method} ${args.join(',')}`, + method, + data, + }); }; const run = async ({ payload: { args: _args, options: { - inputPath, inputPaths, outputPath, del, + input, output, del = true, }, }, }, res) => { const args = [...defaultArgs, ..._args.trim().split(' ')]; - ffmpeg(args.length, strList2ptr(args)); - await adapter.fs.writeFile(outputPath, Module.FS.readFile(outputPath)); - Module.FS.unlink(outputPath); - if (del && typeof inputPath === 'string') { - await adapter.fs.deleteFile(inputPath); - } else if (del && Array.isArray(inputPaths)) { - inputPaths.reduce((promise, input) => promise.then(() => adapter.fs.deleteFile(input)), - Promise.resolve()); + ffmpeg(args.length, strList2ptr(Module, args)); + + /* + * After executing the ffmpeg command, the data is saved in MEMFS, + * if `output` is specified in the options, here ffmpeg.js will move + * these files to IDBFS or NODEFS here. + */ + if (typeof output === 'string') { + await adapter.fs.writeFile(output, Module.FS.readFile(output)); + Module.FS.unlink(output); + } else if (Array.isArray(output)) { + await Promise.all(output.map(async (p) => { + await adapter.fs.writeFile(p, Module.FS.readFile(p)); + Module.FS.unlink(p); + })); } + + /* + * To prevent input files occupy filesystem without notice, + * if `input` is specified in the options, ffmpeg.js cleans these + * files for you + */ + if (del && typeof input === 'string') { + await adapter.fs.deleteFile(input); + } else if (del && Array.isArray(input)) { + await Promise.all(input.map((p) => adapter.fs.deleteFile(p))); + } + res.resolve({ message: `Complete ${args.join(' ')}` }); }; @@ -100,8 +105,8 @@ exports.dispatchHandlers = (packet, send) => { try { ({ load, - ls, syncfs, + FS, run, })[packet.action](packet, res); } catch (err) { diff --git a/src/worker-script/utils/str2ptr.js b/src/worker-script/utils/str2ptr.js new file mode 100644 index 0000000..f8e1198 --- /dev/null +++ b/src/worker-script/utils/str2ptr.js @@ -0,0 +1,8 @@ +module.exports = (Module, s) => { + const ptr = Module._malloc((s.length + 1) * Uint8Array.BYTES_PER_ELEMENT); + for (let i = 0; i < s.length; i += 1) { + Module.setValue(ptr + i, s.charCodeAt(i), 'i8'); + } + Module.setValue(ptr + s.length, 0, 'i8'); + return ptr; +}; diff --git a/src/worker-script/utils/strList2ptr.js b/src/worker-script/utils/strList2ptr.js new file mode 100644 index 0000000..a3af49f --- /dev/null +++ b/src/worker-script/utils/strList2ptr.js @@ -0,0 +1,12 @@ +const str2ptr = require('./str2ptr'); + +module.exports = (Module, strList) => { + const listPtr = Module._malloc(strList.length * Uint32Array.BYTES_PER_ELEMENT); + + strList.forEach((s, idx) => { + const strPtr = str2ptr(Module, s); + Module.setValue(listPtr + (4 * idx), strPtr, 'i32'); + }); + + return listPtr; +};