-
-
Notifications
You must be signed in to change notification settings - Fork 64
/
wackywebm.js
586 lines (523 loc) · 23.3 KB
/
wackywebm.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
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
'use strict'
/*
Make WebM files with changing aspect ratios
By OIRNOIR (oirnoir on Discord)
*/
const path = require('path')
const os = require('os')
const fs = require('fs')
// Synchronous execution via promisify.
const util = require('util')
// I'll admit, inconvenient naming here.
const { getFileName, findMinimumNonErrorSize } = require('./util')
const { localizeString, setLocale } = require('./localization')
const execAsync = util.promisify(require('child_process').exec)
if (!fs.existsSync(path.join(__dirname, "node_modules"))) {
console.log("You haven't installed the nececary dependencies yet!\nPlease run \"npm install\" in the same folder as wackywebm.js and try again.")
// Alternatively, we could have it automatically install dependencies for the user.
/*(async () => {
await execAsync(`cd ${__dirname} && npm i`)
// Restart the program somehow
})();*/
return
}
const UPNG = require("upng-js")
const modes = {}
const modesDir = path.join(__dirname, 'modes')
for (const modeFile of fs.readdirSync(modesDir).filter((file) => file.endsWith('.js'))) {
try {
let modeName = getFileName(modeFile).toLowerCase()
modes[modeName] = require(path.join(modesDir, modeFile))
} catch (e) {
console.warn(localizeString('load_mode_failed', { mode: getFileName(modeFile) }))
}
}
let selectedModes = [],
videoPath = [],
fileName = undefined,
filePath = undefined,
outputPath = undefined,
keyFrameFile = undefined,
bitrate = undefined,
maxThread = undefined,
tempo = undefined,
angle = undefined,
compressionLevel = undefined,
updateCheck = true,
transparencyThreshold = undefined,
smoothingLevel = undefined
// NOTE! if you add a new option, please check out if anything needs to be added for it into terminal-ui.js
// TODO localize the arguments' descriptions (and add translation keys for them)
// along with this, probably localize the usage help
// this would be especially helpful for terminal-ui users.
const argsConfig = [
{
keys: ['-h', '--help'],
noValueAfter: true,
call: () => {
displayUsage()
process.exit(1)
},
description: 'displays this syntax guide',
},
{
keys: ['-k', '--keyframes'],
call: (val) => (keyFrameFile = val),
getValue: () => keyFrameFile,
description: "only used with 'Keyframes' mode; sets the keyframe file to use",
},
{
keys: ['-b', '--bitrate'],
default: () => (bitrate = null),
call: (val) => (bitrate = val),
getValue: () => bitrate,
description: 'sets the maximum bitrate of the video. Lowering this might reduce file size.',
},
{
keys: ['--thread'],
default: () => (maxThread = 2),
call: (val) => (maxThread = parseInt(val)),
getValue: () => maxThread,
description: 'sets maximum allowed number of threads to use',
},
{
keys: ['-t', '--tempo'],
default: () => (tempo = 2),
call: (val) => (tempo = val),
getValue: () => tempo,
description: 'regulates speed of bouncing',
},
{
keys: ['-a', '--angle'],
default: () => (angle = 360),
call: (val) => (angle = parseInt(val)),
getValue: () => angle,
description: "angle to rotate per second when using 'Rotate' mode",
},
{
keys: ['-o', '--output'],
// no "-o" argument, use default path in the format "chungus_Bounce.webm"
default: () => (outputPath = path.join(filePath, `${fileName}_${selectedModes.join('_')}.webm`)),
call: (val) => (outputPath = val),
getValue: () => outputPath,
description: 'sets output file.',
},
{
keys: ['-c', '--compression'],
default: () => (compressionLevel = 0),
call: (c) => (compressionLevel = c),
getValue: () => compressionLevel,
description: 'sets compression level (higher value means more compression)',
},
{
keys: ['-l', '--language'],
default: () => { },
call: (l) => (setLocale(l)),
// allows for more than one -l flag, but i don't think it's worth exposing "currentLocale" just for this
getValue: () => undefined,
description: 'Sets the used language',
},
{
keys: ['--no-update-check'],
noValueAfter: true,
call: () => ( updateCheck = false ),
description: 'disables the automatic update check',
},
{
keys: ['--transparency'],
// by default, ignore frames with an alpha value of 1 (or 0)
default: () => { transparencyThreshold = 1 },
call: (v) => { transparencyThreshold = parseInt(v) },
getValue: () => transparencyThreshold,
description: 'sets the transparency threshold for use with the "transparency" mode'
},
{
keys: ['-s', '--smoothing'],
default: () => { smoothingLevel = 0 },
call: (v) => { smoothingLevel = parseInt(v) },
getValue: () => smoothingLevel,
description: 'Sets the level of smoothing to apply'
}
]
function parseCommandArguments() {
for (let i = 2; i < process.argv.length; i++) {
const arg = process.argv[i]
// named arguments
if (arg.startsWith('-')) {
let argFound = false
for (const j of argsConfig) {
if (j.keys.includes(arg)) {
// need value but no argument after || set argument value twice
if ((!j.noValueAfter && i === process.argv.length - 1) || (j.getValue && j.getValue() !== undefined)) {
console.error(localizeString("arg_cannot_be_set", { arg }))
return displayUsage()
}
if (j.noValueAfter)
j.call(null)
else
j.call(++i === process.argv.length ? null : process.argv[i])
argFound = true
break
}
}
if (!argFound) {
console.error(localizeString('illegal_argument', { arg }))
return displayUsage()
}
continue
}
// positional arguments
//
// basically, first positional argument is inputType, second one
// (and every one after that) is video path, except when the first one doesn't
// match any of the input types, in which case its also part of the path.
// split by + before trying to match to modes in order to support using multiple modes.
const inputModes = arg.toLowerCase().split('+')
// eslint-disable-next-line no-prototype-builtins
if (selectedModes.length === 0 && inputModes.every((x) => modes.hasOwnProperty(x))) selectedModes = inputModes
else videoPath.push(arg)
}
// not a single positional argument, we need at least 1
if (selectedModes.length === 0) {
selectedModes = ['bounce']
console.warn(localizeString('no_mode_selected', { default: selectedModes.join('+')}))
}
// Keyframes mode selected without providing keyframe file
if (selectedModes.includes('keyframes') && (keyFrameFile === undefined || !fs.existsSync(keyFrameFile))) {
if (keyFrameFile) console.error(localizeString('kf_file_not_found', { file: keyFrameFile }))
else console.error(localizeString('kf_file_required'))
return displayUsage()
}
// got 1 positional argument, which was the mode to use - no file path!
if (videoPath.length === 0) {
console.error(localizeString('no_video_file'))
return displayUsage()
}
else videoPath = videoPath.join(' ')
fileName = getFileName(videoPath)
filePath = path.dirname(videoPath)
// check if value not given, use default
for (const i of argsConfig) if (i.default && i.getValue() === undefined) i.default()
return true
}
// Build an index of temporary locations so they do not need to be repeatedly rebuilt.
// All temporary files are within one parent folder for cleanliness and ease of removal.
const workLocations = {}
function buildLocations() {
workLocations.tempFolder = path.join(os.tmpdir(), 'WackyWebM')
workLocations.tempAudio = path.join(workLocations.tempFolder, 'tempAudio.webm')
//workLocations.tempVideo = path.join(workLocations.tempFolder, 'tempVideo.webm')
workLocations.tempConcatList = path.join(workLocations.tempFolder, 'tempConcatList.txt')
workLocations.tempFrames = path.join(workLocations.tempFolder, 'tempFrames')
workLocations.tempFrameFiles = path.join(workLocations.tempFrames, '%d.png')
workLocations.tempResizedFrames = path.join(workLocations.tempFolder, 'tempResizedFrames')
}
async function cleanupLocations() {
// always call on exit, even if erroneous, hopefully
await fs.promises.rm(workLocations.tempFolder, { recursive: true })
}
function displayUsage() {
// for appropriately indenting all the argument aliases so they line up nicely
const longestKeys = argsConfig.map((a) => a.keys.join(',')).sort((a, b) => b.length - a.length)[0].length
const Usage =
'WackyWebM by OIRNOIR\n' +
'Usage: node wackywebm.js [arguments] [mode] <input_file>\n' +
argsConfig.map((arg) => `\t${(arg.keys.join(',') + ':').padEnd(longestKeys + 1, ' ')} ${arg.description}`).join('\n') +
'\nRecognized Modes:\n' +
Object.keys(modes)
.map((m) => `\t${m}`)
.join('\n') +
'\nIf no mode is specified, "Bounce" is used.'
console.log(Usage)
}
function ffmpegErrorHandler(e) {
// tl;dr of this mess of a regex, sort out all lines that are either:
// - "ffmpeg version (.*)"
// - "built with (.*)"
// - "lib(.*)" (for example "libavutil", "libavcodec", plus version info for them)
// - "configuration: (.*)"
// and then match everything after and including the first line that does not start in any of those ways (which is typically the "real" error)
const newMessage = /ffmpeg version [^\n]+\n(?:\s*built with [^\n]+\n|\s*lib[^\n]+\n|\s*configuration:[^\n]+\n)*([\s\S]*)/.exec(e.message)[1]
// throw again to let the wrapper (either `main()` at the bottom of this file, or the terminal-ui file) know we crashed
throw new Error(newMessage)
}
async function main(selectedModes, videoPath, args, outputPath) {
let { keyframes: keyFrameFile, bitrate, thread: maxThread, tempo, angle, compression: compressionLevel, transparency: transparencyThreshold, smoothing } = args;
// mainly for the terminal-ui, where arguments are not parsed and are passed as strings
maxThread = parseInt(maxThread)
tempo = parseFloat(tempo)
angle = parseFloat(angle)
compressionLevel = parseInt(compressionLevel)
transparencyThreshold = parseInt(transparencyThreshold)
smoothing = parseInt(smoothing)
if (updateCheck) {
// before doing anything else, so it gets displayed even in case of error, check if we have the latest version.
let ourVersion = fs.existsSync(path.join(__dirname, 'hash')) ? fs.readFileSync(path.join(__dirname, 'hash')).toString().trim() : 'string that never matches any git commit hash.\ngithub user that does not exist!"§%$&';
ourVersion = ourVersion.replace(/\r/g, '').split('\n')
const https = require('https')
https.get[util.promisify.custom] = (URL) => {
return new Promise((res, rej) => {
https.get(URL, response => {
let data = ''
response.on('data', (chunk) => {
data += chunk;
})
response.on('end', () => {
res(data)
})
}).on('error', (e) => {
rej(e)
}).setTimeout(5000, () => {
rej(new Error("Timeout"))
})
})
}
const promiseGet = util.promisify(https.get)
try {
const upStreamHash = (await promiseGet(`https://raw.githubusercontent.com/${ourVersion[1]}/WackyWebM/main/hash`)).split('\n')
if (upStreamHash[0].trim() !== ourVersion[0].trim()) {
console.log(localizeString('newer_version_available'));
}
} catch (e) {
console.warn(localizeString('error_during_update', { error: e }))
}
}
// Verify the given path is accessible.
if (!videoPath || !fs.existsSync(videoPath)) {
if (videoPath) console.error(localizeString('video_file_not_found', { file: videoPath }))
else console.error(localizeString('no_video_file'))
return displayUsage()
}
// Only build the path if temporary location index if the code can move forward. Less to do.
buildLocations()
// Use one call to ffprobe to obtain framerate, width, and height, returned as JSON.
const videoInfo = await execAsync(`ffprobe -v error -select_streams v -of json -count_frames -show_entries stream=r_frame_rate,width,height,nb_read_frames,bit_rate "${videoPath}"`, { maxBuffer: 1024 * 1000 * 8 /* 8mb */ })
// Deconstructor extracts these values and renames them.
let {
streams: [{ width: maxWidth, height: maxHeight, r_frame_rate: framerate, nb_read_frames: frameCount, bit_rate: originalBitrate }]
} = JSON.parse(videoInfo.stdout.trim())
maxWidth = Number(maxWidth)
maxHeight = Number(maxHeight)
frameCount = Number(frameCount)
const decimalFramerate = framerate.includes('/') ? Number(framerate.split('/')[0]) / Number(framerate.split('/')[1]) : Number(framerate)
if (bitrate == null) bitrate = Math.min(originalBitrate ?? 500000, 1000000)
const delta = findMinimumNonErrorSize(maxWidth, maxHeight)
console.log(localizeString('info1', { delta, video: videoPath }))
console.log(localizeString('info2', { w: maxWidth, h: maxHeight, framerate, decframerate: decimalFramerate, bitrate: originalBitrate}))
// Print config
console.log(localizeString('config_header'))
console.log(localizeString('config_mode_list', { modes: selectedModes.map(m => m[0].toUpperCase() + m.slice(1))}))
if (selectedModes.includes('bounce') || selectedModes.includes('shutter')) console.log(localizeString('bounce_speed', { tempo }))
else if (selectedModes.includes('rotate')) console.log(localizeString('rotate_speed', { angle }))
else if (selectedModes.includes('keyframes')) console.log(localizeString('keyframe_file', { file: keyFrameFile }))
if (bitrate !== originalBitrate) console.log(localizeString('output_bitrate', { bitrate }))
console.log(localizeString('config_footer'))
// Create temp folder
console.log(localizeString('creating_temp_dirs', { path: workLocations.tempFolder }))
await fs.promises.mkdir(workLocations.tempFrames, { recursive: true })
await fs.promises.mkdir(workLocations.tempResizedFrames, { recursive: true })
// Separates the audio to be re-applied at the end of the process.
console.log(localizeString('splitting_audio'))
// If the file has no audio, flag it to it is not attempted.
let audioFlag = true
try {
await execAsync(`ffmpeg -y -i "${videoPath}" -vn -c:a libvorbis "${workLocations.tempAudio}"`, { maxBuffer: 1024 * 1000 * 8 /* 8mb */ })
} catch {
console.warn(localizeString('no_audio'))
audioFlag = false
}
// Extracts the frames to be modified for the wackiness.
console.log(localizeString('splitting_frames'))
try {
await execAsync(`ffmpeg -threads ${maxThread} -y${selectedModes.includes("transparency") ? " -vcodec libvpx" : ""} -i "${videoPath}" "${workLocations.tempFrameFiles}"`, { maxBuffer: 1024 * 1000 * 8 /* 8mb */ })
} catch (e) {
ffmpegErrorHandler(e)
}
// Sorts with a map so extraction of information only happens once per entry.
const tempFramesFiles = fs.readdirSync(workLocations.tempFrames)
const tempFramesFrames = tempFramesFiles
.filter((f) => f.endsWith('png'))
.map((f) => ({ file: f, n: Number(getFileName(f)) }))
.sort((a, b) => a.n - b.n)
const setupInfo = {
videoPath,
keyFrameFile,
maxWidth,
maxHeight,
frameCount,
frameRate: decimalFramerate
}, baseInfoObject = {
maxWidth,
maxHeight,
frameCount,
frameRate: decimalFramerate,
tempo,
angle,
transparencyThreshold,
}
// Setup modes
for (const modeToSetUp of selectedModes)
if (modes[modeToSetUp].setup.constructor.name === 'AsyncFunction') await modes[modeToSetUp].setup(setupInfo)
else modes[modeToSetUp].setup(setupInfo)
// Frame tracked from outside. Width and/or height initialize as the maximum and are not modified if unchanged.
let frame = 0,
tempFiles = [],
subProcess = [],
threadUseCount = 0,
lastWidth = -1,
lastHeight = -1,
sameSizeCount = 0,
totalFramesDone = 0
const startTime = Date.now()
console.log(localizeString('starting_conversion'))
// dont let individual segments (partial webm files) get *too* long (half the file and more, sometimes), otherwise we have almost all threads idling and 1 doing all the work.
const maxSegmentLength = Math.floor(frameCount / maxThread)
const loadFrameData = selectedModes.filter(mode => modes[mode].requiresFrameData).length !== 0
const frameSizeSmoothingBuffer = Array(smoothing)
frameSizeSmoothingBuffer.fill([maxWidth, maxHeight], 0, smoothing)
let fssbIndex = 0
// Creates the respective resized frame based on the selected mode.
for (const { file } of tempFramesFrames) {
// since it is quite an expensive operation, only load frame data if it's required
let frameData = undefined
if (loadFrameData) {
frameData = new Int32Array(UPNG.toRGBA8(UPNG.decode(fs.readFileSync(path.join(workLocations.tempFrames, file))))[0])
}
const infoObject = Object.assign({ frame, frameData, frameFilePath: path.join(workLocations.tempFrames, file) }, baseInfoObject)
const frameBounds = { width: maxWidth, height: maxHeight }
for (const mode of selectedModes)
Object.assign(frameBounds, modes[mode].getFrameBounds(infoObject))
frameBounds.width = Math.max(Math.min(frameBounds.width, maxWidth), delta)
frameBounds.height = Math.max(Math.min(frameBounds.height, maxHeight), delta)
if (smoothing !== 0) {
frameSizeSmoothingBuffer[fssbIndex++] = [frameBounds.width, frameBounds.height]
fssbIndex %= smoothing
frameBounds.width = Math.floor(frameSizeSmoothingBuffer.reduce((s, e) => s + e[0], 0) / smoothing)
frameBounds.height = Math.floor(frameSizeSmoothingBuffer.reduce((s, e) => s + e[1], 0) / smoothing)
}
if (frame === 0) {
lastWidth = frameBounds.width
lastHeight = frameBounds.height
}
// only make new partial webm if frame size changed.
if (Math.abs(frameBounds.width - lastWidth) + Math.abs(frameBounds.height - lastHeight) > compressionLevel || frame === frameCount - 1 || sameSizeCount > maxSegmentLength) {
// Creates the respective resized frame based on the above.
try {
// The part of command can be change
const vfCommand = frameBounds.command ?? `-vf scale=${lastWidth}x${lastHeight} -aspect ${lastWidth}:${lastHeight}`
// 10 frames for one thread
const threadUse = Math.min(maxThread, Math.ceil(sameSizeCount / 10))
const startFrame = frame - sameSizeCount + 1
const inputFile = path.join(workLocations.tempFrames, '%d.png')
const outputFileName = path.join(workLocations.tempResizedFrames, file + '.webm')
const command = `ffmpeg -y -r ${framerate} -start_number ${startFrame} -i "${inputFile}" -frames:v ${sameSizeCount} -c:v vp8 -b:v ${bitrate} -crf 10 ${vfCommand} -threads ${threadUse} -f webm -auto-alt-ref 0 "${outputFileName}"`
// Remove if process done
subProcess = subProcess.filter((process) => {
if (process.done) {
threadUseCount -= process.threadUse
totalFramesDone += process.assignedFrames
return false
}
return true
})
// Wait if subProcess is full
//TODO: figure out a smarter way to just wait for *any* thread to finish, instead of just the 1st (since the later ones might finish before the 1st in the list)
if (threadUseCount >= maxThread) {
// this is a little awkward, but we added the "assignedFrames" attribute to the promise, not the result, so we have to "get it out" before we await.
const processToAwait = subProcess.shift()
const doneFrames = processToAwait.assignedFrames
const threadUse = processToAwait.threadUse
await processToAwait
threadUseCount -= threadUse
totalFramesDone += doneFrames
}
// Add task to subProcess, set done as true if process done so we can remove it without awaiting first task done
const newProcess = execAsync(command, { maxBuffer: 1024 * 1000 * 8 }).then(() => newProcess.done = true)
newProcess.assignedFrames = sameSizeCount
newProcess.threadUse = threadUse
threadUseCount += threadUse
subProcess.push(newProcess)
tempFiles.push(`file '${outputFileName}'`)
// we save when current's width/height change > compressionLevel, so we are not saving the current frame
// so when we reach the last frame (frame === frameCount - 1), we have to include current frame
if (frame === frameCount - 1) {
const lastFrameOutputName = path.join(workLocations.tempResizedFrames, 'end.webm')
const vfCommand = frameBounds.command ?? `-vf scale=${frameBounds.width}x${frameBounds.height} -aspect ${frameBounds.width}:${frameBounds.height}`
//console.log(vfCommand)
const newProcess = execAsync(
`ffmpeg -y -r ${framerate} -start_number ${frame + 1} -i "${inputFile}" -frames:v 1 -c:v vp8 -b:v ${bitrate} -crf 10 ${vfCommand} -threads 1 -f webm -auto-alt-ref 0 "${lastFrameOutputName}"`,
{ maxBuffer: 1024 * 1000 * 8 }).then(() => newProcess.done = true)
newProcess.assignedFrames = sameSizeCount
newProcess.threadUse = threadUse
threadUseCount += threadUse
subProcess.push(newProcess)
tempFiles.push(`file '${lastFrameOutputName}'`)
}
// Output log
const framePad = String(sameSizeCount).padStart((Math.log10(frameCount) + 1) | 0)
process.stdout.clearLine()
process.stdout.cursorTo(0)
process.stdout.write(localizeString('convert_progress', { framecount: framePad, startframe: frame, endframe: frame + sameSizeCount - 1, batch_size: frameCount, percent: Math.floor((1000 * totalFramesDone) / frameCount) / 10.0}))
sameSizeCount = 1
lastWidth = frameBounds.width
lastHeight = frameBounds.height
} catch (e) {
ffmpegErrorHandler(e)
}
} else {
sameSizeCount++
}
frame++
if (frame >= frameCount) {
for (const process of subProcess) await process
// Clean up
subProcess.length = 0
process.stdout.write(`\n${localizeString('done_conversion', { time: Date.now() - startTime, frameCount})}`)
break
}
}
process.stdout.write('\n')
// Writes the concatenation file for the next step.
console.log(localizeString('writing_concat_file'))
await fs.promises.writeFile(workLocations.tempConcatList, tempFiles.join('\n'))
// Concatenates the resized files.
//console.log('Combining webm files into a single webm...')
//await execSync(`ffmpeg -y -f concat -safe 0 -i "${workLocations.tempConcatList}" -c copy "${workLocations.tempVideo}"`)
// Applies the audio to the new file to form the final file.
//console.log('Applying audio to create final webm file...')
//await execSync(`ffmpeg -y -i "${workLocations.tempVideo}" -i "${workLocations.tempAudio}" -c copy "${path.join(filePath, `${fileName}_${inputType}.webm`)}"`)
// Congatenates segments and applies te original audio to the new file.
console.log(localizeString('concatenating' + (audioFlag ? '_audio' : '')))
//if(audioFlag) await execSync(`ffmpeg -y -f concat -safe 0 -i "${workLocations.tempConcatList}" -i "${workLocations.tempAudio}" -c copy "${workLocations.outputFile}"`)
//else await execSync(`ffmpeg -y -f concat -safe 0 -i "${workLocations.tempConcatList}" -c copy "${workLocations.outputFile}"`)
try {
await execAsync(`ffmpeg -y -f concat -safe 0 -i "${workLocations.tempConcatList}"${audioFlag ? ` -i "${workLocations.tempAudio}" ` : ' '}-c copy -auto-alt-ref 0 "${outputPath}"`, { maxBuffer: 1024 * 1000 * 8 /* 8mb */ })
} catch (e) {
ffmpegErrorHandler(e)
}
// Recursive removal of temporary files via the main temporary folder.
console.log(localizeString('done_removing_temp'))
await cleanupLocations()
}
module.exports = { modes, main, args: argsConfig, run: main }
// recommended way to check if this file is the entry point, as per
// https://nodejs.org/api/deprecations.html#DEP0144
if (require.main !== module) return
if (parseCommandArguments() !== true) return
// we're ignoring a promise (the one returned by main) here. this is by design and not harmful, so ignore the warning
// noinspection JSIgnoredPromiseFromCall
main(selectedModes, videoPath, {
keyframes: keyFrameFile,
bitrate,
thread: maxThread,
tempo,
angle,
compression: compressionLevel,
transparency: transparencyThreshold,
smoothing: smoothingLevel
}, outputPath).catch(e => {
// nothing SHOULD ever go wrong with removing the directory, unless we somehow manage to error *before* it is created
cleanupLocations().catch(()=>{})
console.error(localizeString('cli_crash'))
console.error(e)
})