diff --git a/lib/extract-video-frames.js b/lib/extract-video-frames.js index ff47efe..3b8a446 100644 --- a/lib/extract-video-frames.js +++ b/lib/extract-video-frames.js @@ -14,7 +14,9 @@ module.exports = (opts) => { return new Promise((resolve, reject) => { const cmd = ffmpeg(videoPath) - if (seek) cmd.seek(seek / 1000) + // https://trac.ffmpeg.org/wiki/Seeking + // input seek has been frame-accurate since 2.1.0 (Released 2013-10-28) + if (seek) cmd.seekInput(seek / 1000) if (duration) cmd.setDuration(duration / 1000) cmd diff --git a/lib/index.js b/lib/index.js index eca2aaa..d47db40 100644 --- a/lib/index.js +++ b/lib/index.js @@ -18,11 +18,12 @@ module.exports = async (opts) => { transition = undefined, transitions = undefined, audio = undefined, - videos, output, tempDir } = opts + const videos = opts.videos.map(v => typeof v === 'string' ? { video: v } : v) + const temp = tempDir || tempy.directory() console.time(`ffmpeg-concat`) diff --git a/lib/index.test.js b/lib/index.test.js index fc4f965..843ae9c 100644 --- a/lib/index.test.js +++ b/lib/index.test.js @@ -14,6 +14,7 @@ const videos = [ path.join(fixturesPath, '1.mp4'), path.join(fixturesPath, '2.mp4') ] +const cleanupFrames = process.env.CLEANUP_FRAMES !== '0' test('concat 3 mp4s with using constant 500ms transitions', async (t) => { const output = tempy.file({ extension: 'mp4' }) @@ -33,7 +34,41 @@ test('concat 3 mp4s with using constant 500ms transitions', async (t) => { t.truthy(probe.duration >= 10500) t.truthy(probe.duration <= 11500) - await rmfr(output) + if (cleanupFrames) { + await rmfr(output) + } +}) + +test('concat 3 trimmed mp4s with using constant 500ms transitions', async (t) => { + const output = tempy.file({ extension: '.trimmed.mp4' }) + await concat({ + log: console.log, + output, + videos: [ + videos[0], + { + video: videos[1], + start: 1000, + duration: 2000 + }, + videos[2] + ], + transition: { + name: 'directionalwipe', + duration: 500 + } + }) + + // expected duration = 4 + 2 + 4 (=10) - 500 - 500 = 9 + + const probe = await ffmpegProbe(output) + t.is(probe.width, 640) + t.is(probe.height, 360) + assertBetween(t, probe.duration, 8900, 9100) + + if (cleanupFrames) { + await rmfr(output) + } }) test('concat 9 mp4s with unique transitions', async (t) => { @@ -84,5 +119,12 @@ test('concat 9 mp4s with unique transitions', async (t) => { t.truthy(probe.duration >= 27000) t.truthy(probe.duration <= 28000) - await rmfr(output) + if (cleanupFrames) { + await rmfr(output) + } }) + +function assertBetween (t, actualValue, expectedMinimum, expectedMaximum) { + t.truthy(actualValue >= expectedMinimum, `expected '${actualValue}' to be between '${expectedMinimum}' and '${expectedMaximum}'`) + t.truthy(actualValue <= expectedMaximum, `expected '${actualValue}' to be between '${expectedMinimum}' and '${expectedMaximum}'`) +} diff --git a/lib/init-frames.js b/lib/init-frames.js index 16159ba..c1974eb 100644 --- a/lib/init-frames.js +++ b/lib/init-frames.js @@ -47,16 +47,16 @@ module.exports = async (opts) => { const scene = scenes[i] const next = scenes[i + 1] - scene.trimStart = (prev ? prev.transition.duration : 0) + scene.trimStart = (prev ? prev.transition.duration : 0) + scene.start // sanitize transition durations to never be longer than scene durations - scene.transition.duration = Math.max(0, Math.min(scene.transition.duration, scene.duration - scene.trimStart)) + scene.transition.duration = Math.max(0, Math.min(scene.transition.duration, scene.duration - (prev ? prev.transition.duration : 0))) if (next) { scene.transition.duration = Math.min(scene.transition.duration, next.duration) } - scene.trimEnd = scene.duration - (next ? scene.transition.duration : 0) + scene.trimEnd = scene.start + scene.duration - (next ? scene.transition.duration : 0) scene.trimDuration = scene.trimEnd - scene.trimStart if (next) { @@ -77,7 +77,7 @@ module.exports = async (opts) => { frameFormat, outputDir, videoPath: next.video, - seek: 0, + seek: next.start || 0, duration: scene.transition.duration, fps }) @@ -119,16 +119,18 @@ module.exports.initScene = async (opts) => { } = opts const video = videos[index] - const probe = await ffmpegProbe(video) + const probe = await ffmpegProbe(video.video) const scene = { - video, + video: video.video, index, width: probe.width, height: probe.height, - duration: probe.duration, - fps: probe.fps, - numFrames: parseInt(probe.streams[0].nb_frames) + start: video.start || 0, + duration: video.duration == null + ? probe.duration - (video.start == null ? 0 : video.start) + : video.duration, + fps: probe.fps } const t = (transitions ? transitions[index] : transition) diff --git a/readme.md b/readme.md index bbc46d6..0a9709a 100644 --- a/readme.md +++ b/readme.md @@ -78,6 +78,24 @@ await concat({ }) ``` +```js +const concat = require('ffmpeg-concat') + +// concat 3 mp4s together taking only the middle 2 seconds of video 2 +await concat({ + output: 'test.mp4', + videos: [ + 'media/0.mp4', + { video: 'media/1.mp4', start: 1000, duration: 2000 }, + 'media/2.mp4' + ], + transition: { + name: 'directionalWipe', + duration: 500 + } +}) +``` + ```js // concat 5 mp4s together using 4 different transitions await concat({