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