ffmpeg-extract-frames
Advanced tools
Comparing version 1.0.0 to 2.0.0
78
index.js
@@ -5,31 +5,73 @@ 'use strict' | ||
const path = require('path') | ||
const probe = require('ffmpeg-probe') | ||
const noop = () => { } | ||
module.exports = (opts) => { | ||
module.exports = async (opts) => { | ||
const { | ||
log = noop, | ||
// required | ||
input, | ||
output, | ||
// optional | ||
timestamps, | ||
offsets, | ||
folder, | ||
filename | ||
fps, | ||
numFrames | ||
} = opts | ||
if (!timestamps && !offsets) { | ||
throw new Error('missing required screenshot timestamps or offsets') | ||
} | ||
if (!input) throw new Error('missing required input') | ||
if (!output) throw new Error('missing required output') | ||
const framePattern = path.join(folder, filename) | ||
const outputPath = path.parse(output) | ||
return new Promise((resolve, reject) => { | ||
ffmpeg(input) | ||
.on('start', (cmd) => log({ cmd })) | ||
.on('end', () => resolve(framePattern)) | ||
.on('error', (err) => reject(err)) | ||
.screenshots({ | ||
folder, | ||
filename, | ||
timestamps: timestamps || offsets.map((offset) => offset / 1000) | ||
}) | ||
}) | ||
const cmd = ffmpeg(input) | ||
.on('start', (cmd) => log({ cmd })) | ||
if (timestamps || offsets) { | ||
const folder = outputPath.dir | ||
const filename = outputPath.base | ||
return new Promise((resolve, reject) => { | ||
cmd | ||
.on('end', () => resolve(output)) | ||
.on('error', (err) => reject(err)) | ||
.screenshots({ | ||
folder, | ||
filename, | ||
timestamps: timestamps || offsets.map((offset) => offset / 1000) | ||
}) | ||
}) | ||
} else { | ||
if (fps) { | ||
cmd.outputOptions([ | ||
'-r', Math.max(1, fps | 0) | ||
]) | ||
} else if (numFrames) { | ||
const info = await probe(input) | ||
const numFramesTotal = parseInt(info.streams[0].nb_frames) | ||
const nthFrame = (numFramesTotal / numFrames) | 0 | ||
cmd.outputOptions([ | ||
'-vsync', 'vfr', | ||
'-vf', `select=not(mod(n\\,${nthFrame}))` | ||
]) | ||
} | ||
if (outputPath.ext === '.raw') { | ||
cmd.outputOptions([ | ||
'-pix_fmt', 'rgba' | ||
]) | ||
} | ||
return new Promise((resolve, reject) => { | ||
cmd | ||
.on('end', () => resolve(output)) | ||
.on('error', (err) => reject(err)) | ||
.output(output) | ||
.run() | ||
}) | ||
} | ||
} |
@@ -5,2 +5,3 @@ 'use strict' | ||
const path = require('path') | ||
const rmfr = require('rmfr') | ||
const sharp = require('sharp') | ||
@@ -14,11 +15,35 @@ const tempy = require('tempy') | ||
test('jpg + offsets', async (t) => { | ||
test('default (all frames) => jpg', async (t) => { | ||
const folder = tempy.directory() | ||
const filename = 'test-%d.jpg' | ||
const output = path.join(folder, filename) | ||
await extractFrames({ | ||
log: console.log, | ||
input, | ||
output | ||
}) | ||
for (let i = 1; i <= 100; ++i) { | ||
const file = output.replace('%d', i) | ||
const image = await sharp(file).metadata() | ||
t.deepEqual(image.width, 640) | ||
t.deepEqual(image.height, 360) | ||
t.deepEqual(image.channels, 3) | ||
t.deepEqual(image.format, 'jpeg') | ||
} | ||
await rmfr(folder) | ||
}) | ||
test('offsets => jpg', async (t) => { | ||
const folder = tempy.directory() | ||
const filename = 'test-%i.jpg' | ||
const output = path.join(folder, filename) | ||
const filePattern = await extractFrames({ | ||
await extractFrames({ | ||
log: console.log, | ||
input, | ||
folder, | ||
filename, | ||
output, | ||
offsets: [ | ||
@@ -32,3 +57,3 @@ 0, | ||
for (let i = 1; i <= 3; ++i) { | ||
const file = filePattern.replace('%i', i) | ||
const file = output.replace('%i', i) | ||
const image = await sharp(file).metadata() | ||
@@ -41,13 +66,15 @@ | ||
} | ||
await rmfr(folder) | ||
}) | ||
test('png + timestamps', async (t) => { | ||
test('timestamps => png', async (t) => { | ||
const folder = tempy.directory() | ||
const filename = 'test-%i.png' | ||
const output = path.join(folder, filename) | ||
const filePattern = await extractFrames({ | ||
await extractFrames({ | ||
log: console.log, | ||
input, | ||
folder, | ||
filename, | ||
output, | ||
timestamps: [ | ||
@@ -62,3 +89,3 @@ '0%', | ||
for (let i = 1; i <= 4; ++i) { | ||
const file = filePattern.replace('%i', i) | ||
const file = output.replace('%i', i) | ||
const image = await sharp(file).metadata() | ||
@@ -71,2 +98,54 @@ | ||
} | ||
await rmfr(folder) | ||
}) | ||
test('fps => png', async (t) => { | ||
const folder = tempy.directory() | ||
const filename = 'test-%d.png' | ||
const output = path.join(folder, filename) | ||
await extractFrames({ | ||
log: console.log, | ||
input, | ||
output, | ||
fps: 2 | ||
}) | ||
for (let i = 1; i <= 10; ++i) { | ||
const file = output.replace('%d', i) | ||
const image = await sharp(file).metadata() | ||
t.deepEqual(image.width, 640) | ||
t.deepEqual(image.height, 360) | ||
t.deepEqual(image.channels, 3) | ||
t.deepEqual(image.format, 'png') | ||
} | ||
await rmfr(folder) | ||
}) | ||
test('numFrames => jpg', async (t) => { | ||
const folder = tempy.directory() | ||
const filename = 'test-%d.jpg' | ||
const output = path.join(folder, filename) | ||
await extractFrames({ | ||
log: console.log, | ||
input, | ||
output, | ||
numFrames: 7 | ||
}) | ||
for (let i = 1; i <= 7; ++i) { | ||
const file = output.replace('%d', i) | ||
const image = await sharp(file).metadata() | ||
t.deepEqual(image.width, 640) | ||
t.deepEqual(image.height, 360) | ||
t.deepEqual(image.channels, 3) | ||
t.deepEqual(image.format, 'jpeg') | ||
} | ||
await rmfr(folder) | ||
}) |
{ | ||
"name": "ffmpeg-extract-frames", | ||
"version": "1.0.0", | ||
"description": "Extracts screenshots from a video.", | ||
"version": "2.0.0", | ||
"description": "Extracts frames from a video.", | ||
"main": "index.js", | ||
@@ -19,2 +19,3 @@ "repository": "transitive-bullshit/ffmpeg-extract-frames", | ||
"ava": "^0.25.0", | ||
"rmfr": "^2.0.0", | ||
"sharp": "^0.20.1", | ||
@@ -25,4 +26,5 @@ "standard": "^11.0.0", | ||
"dependencies": { | ||
"ffmpeg-probe": "^1.0.1", | ||
"fluent-ffmpeg": "^2.1.2" | ||
} | ||
} |
# ffmpeg-extract-frames | ||
> Extracts screenshots from a video using [fluent-ffmpeg](https://github.com/fluent-ffmpeg/node-fluent-ffmpeg). | ||
> Extracts frames from a video using [fluent-ffmpeg](https://github.com/fluent-ffmpeg/node-fluent-ffmpeg). | ||
@@ -21,6 +21,5 @@ [![NPM](https://img.shields.io/npm/v/ffmpeg-extract-frames.svg)](https://www.npmjs.com/package/ffmpeg-extract-frames) [![Build Status](https://travis-ci.org/transitive-bullshit/ffmpeg-extract-frames.svg?branch=master)](https://travis-ci.org/transitive-bullshit/ffmpeg-extract-frames) [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) | ||
// extract 3 frames at 1s, 2s, and 3.5s respectively | ||
const filePattern = await extractFrames({ | ||
await extractFrames({ | ||
input: 'media/1.mp4', | ||
folder: '.', | ||
filename: 'screenshot-%i.jpg', | ||
output: './screenshot-%i.jpg', | ||
offsets: [ | ||
@@ -34,2 +33,6 @@ 1000, | ||
// filePattern = './screenshot-%i.jpg' | ||
// generated screenshots: | ||
// `./screenshot-1.jpg | ||
// `./screenshot-2.jpg | ||
// `./screenshot-3.jpg | ||
``` | ||
@@ -43,2 +46,4 @@ | ||
There are several options for specifying which frames to extract, namely `timestamps`, `offsets`, `fps`, and `numFrames`. The default behavior if you don't specify any of these options is to extract *all* frames from the input. | ||
#### options | ||
@@ -49,17 +54,17 @@ | ||
Type: `String` | ||
**Required** | ||
Path or URL to a video file. | ||
##### folder | ||
##### output | ||
Type: `String` | ||
**Required** | ||
Output directory. | ||
Output file pattern. | ||
##### filename | ||
Note that for `timestamps` or `offsets`, the pattern should include a `%i` or `%s` ([details](https://github.com/fluent-ffmpeg/node-fluent-ffmpeg#screenshotsoptions-dirname-generate-thumbnails)). | ||
Type: `String` | ||
For any other call, you should use the `%d` format specifier. I know this is confusing, but it's how [fluent-ffmpeg](https://github.com/fluent-ffmpeg/node-fluent-ffmpeg) works under the hood. | ||
Output file pattern including a `%i` | ||
##### offsets | ||
@@ -71,4 +76,2 @@ | ||
Note: you must pass either `offsets` or `timestamps`, with `timestamps` taking precedence. | ||
##### timestamps | ||
@@ -80,4 +83,14 @@ | ||
Note: you must pass either `offsets` or `timestamps`, with `timestamps` taking precedence. | ||
##### fps | ||
Type: `Number` | ||
Frames per second to output. | ||
##### numFrames | ||
Type: `Number` | ||
Output a specific number of frames. The input video's frames will be skipped such that only this number of frames are output. | ||
##### log | ||
@@ -92,4 +105,5 @@ | ||
- [ffmpeg-extract-frame](https://github.com/transitive-bullshit/ffmpeg-extract-frame) extracts a single frame from a video. | ||
- [fluent-ffmpeg](https://github.com/fluent-ffmpeg/node-fluent-ffmpeg) | ||
- [ffmpeg-extract-frame](https://github.com/transitive-bullshit/ffmpeg-extract-frame) - Extracts a single frame from a video. | ||
- [ffmpeg-generate-video-preview](https://github.com/transitive-bullshit/ffmpeg-generate-video-preview) - Generates an attractive image strip or GIF preview from a video. | ||
- [fluent-ffmpeg](https://github.com/fluent-ffmpeg/node-fluent-ffmpeg) - A fluent API to [FFmpeg](http://ffmpeg.org/). | ||
@@ -96,0 +110,0 @@ ## License |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
8813
7
180
106
2
5
+ Addedffmpeg-probe@^1.0.1
+ Addedcross-spawn@6.0.6(transitive)
+ Addedexeca@0.10.0(transitive)
+ Addedffmpeg-probe@1.0.6(transitive)
+ Addedget-stream@3.0.0(transitive)
+ Addedis-stream@1.1.0(transitive)
+ Addednice-try@1.0.5(transitive)
+ Addednpm-run-path@2.0.2(transitive)
+ Addedp-finally@1.0.0(transitive)
+ Addedpath-key@2.0.1(transitive)
+ Addedsemver@5.7.2(transitive)
+ Addedshebang-command@1.2.0(transitive)
+ Addedshebang-regex@1.0.0(transitive)
+ Addedsignal-exit@3.0.7(transitive)
+ Addedstrip-eof@1.0.0(transitive)