diff --git a/.gitignore b/.gitignore index b7b5d21..064213f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules dist /.nyc_output +.DS_Store \ No newline at end of file diff --git a/src/createFFmpeg.js b/src/createFFmpeg.js index 2c01616..6db4183 100644 --- a/src/createFFmpeg.js +++ b/src/createFFmpeg.js @@ -3,6 +3,7 @@ const { setLogging, log } = require('./utils/log'); const resolvePaths = require('./utils/resolvePaths'); const parseProgress = require('./utils/parseProgress'); const stringList2pointer = require('./utils/stringList2pointer'); +const parseArgs = require('./utils/parseArgs'); const { defaultOptions, getModule, @@ -92,7 +93,7 @@ module.exports = (_options = {}) => { } else { running = true; return new Promise((resolve) => { - const args = [...defaultArgs, ..._args.trim().split(' ')].filter((s) => s.length !== 0); + const args = [...defaultArgs, ...parseArgs(_args)].filter((s) => s.length !== 0); log('info', `ffmpeg command: ${args.join(' ')}`); runResolve = resolve; ffmpeg(args.length, stringList2pointer(Module, args)); diff --git a/src/utils/parseArgs.js b/src/utils/parseArgs.js new file mode 100644 index 0000000..27cc81b --- /dev/null +++ b/src/utils/parseArgs.js @@ -0,0 +1,51 @@ +module.exports = (cmd) => { + const args = []; + let nextDelimiter = 0; + let prevDelimiter = 0; + // eslint-disable-next-line no-cond-assign + while ((nextDelimiter = cmd.indexOf(' ', prevDelimiter)) >= 0) { + let arg = cmd.substring(prevDelimiter, nextDelimiter); + let quoteIdx = arg.indexOf('\''); + let dblQuoteIdx = arg.indexOf('"'); + + if (quoteIdx === 0 || dblQuoteIdx === 0) { + /* The argument has a quote at the start i.e, 'id=0,streams=0 id=1,streams=1' */ + const delimiter = arg[0]; + const endDelimiter = cmd.indexOf(delimiter, prevDelimiter + 1); + + if (endDelimiter < 0) { + throw new Error(`Bad command escape sequence ${delimiter} near ${nextDelimiter}`); + } + + arg = cmd.substring(prevDelimiter + 1, endDelimiter); + prevDelimiter = endDelimiter + 2; + args.push(arg); + } else if (quoteIdx > 0 || dblQuoteIdx > 0) { + /* The argument has a quote in it, it must be ended correctly i,e. title='test' */ + if (quoteIdx === -1) quoteIdx = Infinity; + if (dblQuoteIdx === -1) dblQuoteIdx = Infinity; + const delimiter = (quoteIdx < dblQuoteIdx) ? '\'' : '"'; + const quoteOffset = Math.min(quoteIdx, dblQuoteIdx); + const endDelimiter = cmd.indexOf(delimiter, prevDelimiter + quoteOffset + 1); + + if (endDelimiter < 0) { + throw new Error(`Bad command escape sequence ${delimiter} near ${nextDelimiter}`); + } + + arg = cmd.substring(prevDelimiter, endDelimiter + 1); + prevDelimiter = endDelimiter + 2; + args.push(arg); + } else if (arg !== '') { + args.push(arg); + prevDelimiter = nextDelimiter + 1; + } else { + prevDelimiter = nextDelimiter + 1; + } + } + + if (prevDelimiter !== cmd.length) { + args.push(cmd.substring(prevDelimiter)); + } + + return args; +}; diff --git a/tests/constants.js b/tests/constants.js index 5e8898c..9e9d5d7 100644 --- a/tests/constants.js +++ b/tests/constants.js @@ -4,6 +4,9 @@ const IS_BROWSER = typeof window !== 'undefined' && typeof window.document !== ' const OPTIONS = { corePath: 'http://localhost:3000/node_modules/@ffmpeg/core/ffmpeg-core.js', }; +const FLAME_MP4_LENGTH = 100374; +const META_FLAME_MP4_LENGTH = 100408; +const META_FLAME_MP4_LENGTH_NO_SPACE = 100404; if (typeof module !== 'undefined') { module.exports = { @@ -11,5 +14,8 @@ if (typeof module !== 'undefined') { BASE_URL, IS_BROWSER, OPTIONS, + FLAME_MP4_LENGTH, + META_FLAME_MP4_LENGTH, + META_FLAME_MP4_LENGTH_NO_SPACE, }; } diff --git a/tests/ffmpeg.test.js b/tests/ffmpeg.test.js index 337a2e5..b663038 100644 --- a/tests/ffmpeg.test.js +++ b/tests/ffmpeg.test.js @@ -25,3 +25,38 @@ describe('transcode()', () => { }); }); }); + +describe('run()', () => { + describe('should run a command with quoted parameters at start no spaces', () => { + ['flame.avi'].forEach((name) => ( + it(`run ${name}`, async () => { + await ffmpeg.write(name, `${BASE_URL}/${name}`); + await ffmpeg.run(`-y -i ${name} -metadata 'title="test"' output.mp4`); + const data = ffmpeg.read('output.mp4'); + expect(data.length).to.be(META_FLAME_MP4_LENGTH_NO_SPACE); + }).timeout(TIMEOUT) + )); + }); + + describe('should run a command with quoted parameters at start and a space in between', () => { + ['flame.avi'].forEach((name) => ( + it(`run ${name}`, async () => { + await ffmpeg.write(name, `${BASE_URL}/${name}`); + await ffmpeg.run(`-y -i ${name} -metadata 'title="my title"' output.mp4`); + const data = ffmpeg.read('output.mp4'); + expect(data.length).to.be(META_FLAME_MP4_LENGTH); + }).timeout(TIMEOUT) + )); + }); + + describe('should run a command with name quoted parameters and a space in between', () => { + ['flame.avi'].forEach((name) => ( + it(`run ${name}`, async () => { + await ffmpeg.write(name, `${BASE_URL}/${name}`); + await ffmpeg.run(`-y -i ${name} -metadata title="my title" output.mp4`); + const data = ffmpeg.read('output.mp4'); + expect(data.length).to.be(META_FLAME_MP4_LENGTH); + }).timeout(TIMEOUT) + )); + }); +});