UNPKG

5.3 kBJavaScriptView Raw
1'use strict'
2
3const { spawn } = require('child_process')
4
5/**
6 * Parse a size string (eg. '240x?')
7 * @func parseSize
8 * @param {String} sizeStr A string in the form of '240x100' or '50%'
9 * @returns {Object} Object containing numeric values of the width and height
10 * of the thumbnail, or the scaling percentage
11 * @throws {Error} Throws on malformed size string
12 */
13function parseSize (sizeStr) {
14 const invalidSizeString = new Error('Invalid size string')
15 const percentRegex = /(\d+)%/g
16 const sizeRegex = /(\d+|\?)x(\d+|\?)/g
17 let size
18
19 const percentResult = percentRegex.exec(sizeStr)
20 const sizeResult = sizeRegex.exec(sizeStr)
21
22 if (percentResult) {
23 size = { percentage: Number.parseInt(percentResult[1]) }
24 } else if (sizeResult) {
25 const sizeValues = sizeResult.map(x => x === '?' ? null : Number.parseInt(x))
26
27 size = {
28 width: sizeValues[1],
29 height: sizeValues[2]
30 }
31 } else {
32 throw invalidSizeString
33 }
34
35 if (size.width === null && size.height === null) {
36 throw invalidSizeString
37 }
38
39 return size
40}
41
42/**
43 * Return an array of string arguments to be passed to ffmpeg
44 * @func buildArgs
45 * @param {String} input The input argument for ffmpeg
46 * @param {String} output The output path for the generated thumbnail
47 * @param {Object} size A size object returned from parseSize
48 * @param {Number} [size.height] The thumbnail height, in pixels
49 * @param {Number} [size.width] The thumbnail width, in pixels
50 * @param {Number} [size.percentage] The thumbnail scaling percentage
51 * @param {String} seek The time to seek, formatted as hh:mm:ss[.ms]
52 * @returns {Array<string>} Array of arguments for ffmpeg
53 */
54function buildArgs (input, output, { width, height, percentage }, seek) {
55 const scaleArg = (percentage)
56 ? `-vf scale=iw*${percentage / 100}:ih*${percentage / 100}`
57 : `-vf scale=${width || -1}:${height || -1}`
58
59 return [
60 '-y',
61 `-i ${input}`,
62 '-vframes 1',
63 `-ss ${seek}`,
64 scaleArg,
65 output
66 ]
67}
68
69/**
70 * Spawn an instance of ffmpeg and generate a thumbnail
71 * @func ffmpegExecute
72 * @param {String} path The path of the ffmpeg binary
73 * @param {Array<string>} args An array of arguments for ffmpeg
74 * @param {stream.Readable} [rstream] A readable stream to pipe data to
75 * the standard input of ffmpeg
76 * @param {stream.Writable} [wstream] A writable stream to receive data from
77 * the standard output of ffmpeg
78 * @returns {Promise} Promise that resolves once thumbnail is generated
79 */
80function ffmpegExecute (path, args, rstream, wstream) {
81 const ffmpeg = spawn(path, args, { shell: true })
82 let stderr = ''
83
84 return new Promise((resolve, reject) => {
85 if (rstream) {
86 rstream.pipe(ffmpeg.stdin)
87 }
88 if (wstream) {
89 ffmpeg.stdout.pipe(wstream)
90 }
91
92 ffmpeg.stderr.on('data', (data) => {
93 stderr += data.toString()
94 })
95 ffmpeg.stderr.on('error', (err) => {
96 reject(err)
97 })
98 ffmpeg.on('exit', (code, signal) => {
99 if (code !== 0) {
100 const err = new Error(`ffmpeg exited ${code}\nffmpeg stderr:\n\n${stderr}`)
101 reject(err)
102 }
103 })
104 ffmpeg.on('close', resolve)
105 })
106}
107
108/**
109 * Spawn an instance of ffmpeg and generate a thumbnail
110 * @func ffmpegStreamExecute
111 * @param {String} path The path of the ffmpeg binary
112 * @param {Array<string>} args An array of arguments for ffmpeg
113 * @param {stream.Readable} [rstream] A readable stream to pipe data to
114 * the standard input of ffmpeg
115 * @returns {Promise} Promise that resolves to ffmpeg stdout
116 */
117function ffmpegStreamExecute (path, args, rstream) {
118 const ffmpeg = spawn(path, args, { shell: true })
119
120 if (rstream) {
121 rstream.pipe(ffmpeg.stdin)
122 }
123
124 return Promise.resolve(ffmpeg.stdout)
125}
126
127/**
128 * Generates a thumbnail from the first frame of a video file
129 * @func genThumbnail
130 * @param {String|stream.Readable} input Path to video, or a read stream
131 * @param {String|stream.Writeable} output Output path of the thumbnail
132 * @param {String} size The size of the thumbnail, eg. '240x240'
133 * @param {Object} [config={}] A configuration object
134 * @param {String} [config.path='ffmpeg'] Path of the ffmpeg binary
135 * @param {String} [config.seek='00:00:00'] Time to seek for videos
136 * @returns {Promise} Resolves on completion, or rejects on error
137 */
138function genThumbnail (input, output, size, config = {}) {
139 const ffmpegPath = config.path || process.env.FFMPEG_PATH || 'ffmpeg'
140 const seek = config.seek || '00:00:00'
141 const rstream = typeof input === 'string' ? null : input
142 const wstream = typeof output === 'string' ? null : output
143
144 const parsedSize = parseSize(size)
145 const args = buildArgs(
146 typeof input === 'string' ? `"${input}"` : 'pipe:0',
147 typeof output === 'string' ? `"${output}"` : '-f singlejpeg pipe:1',
148 parsedSize,
149 seek
150 )
151
152 if (output === null) {
153 return ffmpegStreamExecute(ffmpegPath, args, rstream)
154 }
155
156 return ffmpegExecute(ffmpegPath, args, rstream, wstream)
157}
158
159module.exports = genThumbnail