-
Notifications
You must be signed in to change notification settings - Fork 3
/
index.js
169 lines (149 loc) · 5.28 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
const ffmpeg = require('fluent-ffmpeg')
const assgen = require('./utils/generateAssFile')
const filters = require('./utils/filters')
function renderVideo(vid, output, verbose) {
return new Promise((resolve, reject) => {
vid.on('start', (cmd) => {
if (verbose) console.log('FFMPEG Command: ' + cmd)
})
vid.on('end', () => {
resolve()
})
vid.on('error', (error) => {
reject(error)
})
vid.save(output)
})
}
async function generate(input) {
const audioFile = input.audio
const slides = input.slides
const duration = input.duration
const captions = input.captions
const output = input.output || 'output.mp4'
const assFile = input.assOutput || 'subs.ass'
const videoWidth = input.width || 640
const videoHeight = input.height || 480
const forceScale = !!input.forceScale
const videoCodec = input.videoCodec || 'libx264'
const genpalette = !!input.genpalette
const gifLoop = input.gifLoop === undefined ? true : !!input.gifLoop
const otherOutputOptions = input.otherOutputOptions || null
const hardSub = input.hardSub === undefined || !!input.hardSub
const verbose = input.verbose === undefined || !!input.verbose
const watermark = input.watermark || false
const startTime = Date.now()
const isImageOutput = output.endsWith('.gif') || output.endsWith('.webp')
const hasCaptions = captions && captions.length > 0
if (hasCaptions && assFile) {
if (!assgen.generate(videoWidth, videoHeight, assFile, captions)) {
console.log('Failed to gen ass file')
return false
}
}
const vid = ffmpeg()
let numSlides = slides.length
for (let i = 0; i < slides.length; i++) {
const slide = slides[i]
if (!slide.duration || isNaN(slide.duration)) {
slide.duration = 1
} else {
slide.duration = Number(slide.duration)
}
}
let { totalDuration, complexFilters, lastTransitionDuration, lastOutputTag } = filters.generateFilters(slides, forceScale, videoWidth, videoHeight)
for (let i = 0; i < slides.length; i++) {
const slide = slides[i]
vid.input(slide.path)
const inputOptions = []
if (!slide.isVideo) {
inputOptions.push('-loop 1')
}
if (i === slides.length - 1) {
inputOptions.push('-t ' + (slide.duration + lastTransitionDuration))
}
if (inputOptions.length > 0) {
vid.inputOptions(inputOptions)
}
}
let _scale = ''
if (input.width && input.height) {
_scale = `,scale=${input.width}:${input.height}`
}
let inputTag = lastOutputTag ? `[${lastOutputTag}]` : ''
let scaleFormatFilter = `${inputTag}format=yuv420p${_scale}[v]`
let lastOutput = 'v'
complexFilters.push(scaleFormatFilter)
// Color Palette gen for gif
if (genpalette && isImageOutput) {
complexFilters.push('[v]split[a][b]')
complexFilters.push('[a]palettegen[p]')
complexFilters.push('[b][p]paletteuse[vp]')
lastOutput = 'vp'
}
// Scale and overlay watermark
let watermarkAdded = false
if (watermark && watermark.path) {
vid.input(watermark.path)
const indexOfWatermark = numSlides
const width = watermark.width && !isNaN(watermark.width) ? watermark.width : 100
const height = watermark.height && !isNaN(watermark.height) ? watermark.height : -1
const x = watermark.x && !isNaN(watermark.x) ? watermark.x : 10
const y = watermark.y && !isNaN(watermark.y) ? watermark.y : 10
const alpha = watermark.alpha && !isNaN(watermark.alpha) ? Number(watermark.alpha) : 1
let alphaFilter = ''
if (alpha !== 1) {
alphaFilter = `format=argb,geq=r='r(X,Y)':a='${alpha}*alpha(X,Y)',`
}
complexFilters.push(`[${indexOfWatermark}]${alphaFilter}scale=${width}:${height}[watermark]`)
complexFilters.push(`[${lastOutput}][watermark]overlay=${x}:${y}[vw]`)
lastOutput = 'vw'
watermarkAdded = true
}
// Audio Input
if (audioFile && !isImageOutput) {
vid.input(audioFile)
vid.audioCodec('aac')
vid.audioBitrate('192k')
}
// Subtitle Filter
if (hasCaptions && assFile && hardSub) {
let subtitleFilt = `[${lastOutput}]ass=${assFile}[vf]`
complexFilters.push(subtitleFilt)
lastOutput = 'vf'
}
vid.complexFilter(complexFilters)
if (!isImageOutput) { // Only add vcodec for videos
vid.videoCodec(videoCodec)
}
const outputOptions = []
outputOptions.push(`-map [${lastOutput}]`)
if (audioFile && !isImageOutput) {
const audioIndexOffset = watermarkAdded ? 1 : 0
const audioIndex = numSlides + audioIndexOffset
outputOptions.push(`-map ${audioIndex}:a`)
}
let videoDuration = Number(duration) || totalDuration
outputOptions.push('-t ' + videoDuration)
outputOptions.push('-y')
if (isImageOutput) {
outputOptions.push('-loop ' + (gifLoop ? 0 : 1))
}
if (otherOutputOptions && Object.prototype.toString.call(otherOutputOptions) === "[object String]") {
outputOptions.push(otherOutputOptions)
} else if (otherOutputOptions && otherOutputOptions.length) {
outputOptions.push(...otherOutputOptions)
}
vid.outputOptions(outputOptions)
let success = true
try {
await renderVideo(vid, output, verbose)
} catch (error) {
console.error('Failed to render', error)
success = false
}
const renderDur = Date.now() - startTime
if (verbose) console.log('Render Time: ' + (renderDur / 1000).toFixed(2) + 's')
return success
}
exports.render = generate