423 lines
No EOL
13 KiB
JavaScript
423 lines
No EOL
13 KiB
JavaScript
const fs = require('fs')
|
|
const { exec, execSync } = require('child_process')
|
|
const cliProgress = require('cli-progress')
|
|
const term = require("terminal-kit").terminal
|
|
|
|
/**
|
|
* TODO : Adapt audio quality as well to accomodate long videos(Currently 5m is too much)
|
|
* FIND A WAY TO COMPILE THIS:..
|
|
*/
|
|
|
|
//Parse file inputs (Drag n drop or arguments)
|
|
inputList = process.argv.slice(2)
|
|
input = inputList[0]
|
|
let file, fileType, bar1
|
|
//if launched without params
|
|
if (!input) {
|
|
startMenu()
|
|
}
|
|
else {
|
|
file = input.split("\\")
|
|
file = file[file.length - 1]
|
|
|
|
fileType = file.split(".")[1]
|
|
|
|
if (!fs.existsSync(input)) {
|
|
term.italic(`${input}`).bold.red(" <- Path or File doesn't exist\n")
|
|
term.grey("press enter to exit...")
|
|
term.inputField(function () { process.exit() })
|
|
}
|
|
else {
|
|
bar1 = new cliProgress.SingleBar({
|
|
synchronousUpdate: true,
|
|
align: "left",
|
|
hideCursor: true
|
|
}, cliProgress.Presets.shades_classic)
|
|
|
|
if (fileType == "jpg" || fileType == "JPG" || fileType == "png" || fileType == "PNG" || fileType == "webp") {
|
|
encodePicture(input, file.split(".")[0])
|
|
}
|
|
else if (fileType == "webm" || fileType == "mp4" || fileType == "mov" || fileType == "mkv" || fileType == "avi") {
|
|
encodeVideo(input, file.split(".")[0])
|
|
}
|
|
else if ("ogg" || "mp3" || "aiff" || "wav" || "flac") {
|
|
encodeAudio(input, file.split(".")[0])
|
|
}
|
|
else {
|
|
term.italic(`${file}`).bold.red(` <- Unsupported format\n`)
|
|
term.grey("press enter to exit...")
|
|
term.inputField(function () { process.exit() })
|
|
}
|
|
}
|
|
}
|
|
|
|
async function encodeVideo(path, out) {
|
|
//create progress bar
|
|
|
|
const [command, presetName, duration, isTwoPass] = await constructVideoCommand(path, out)
|
|
bar1.start(duration, 0, { speed: "N/A" })
|
|
let isPastHalf = false
|
|
let encoder = exec(command)
|
|
encoder.stderr.on("data", (chunk) => {
|
|
currentTime = chunk.split("time=")[1]?.split(" ")[0]
|
|
if (currentTime) {
|
|
const arr = currentTime.split(":") // splitting the string by colon
|
|
let seconds = Number.parseFloat(arr[0] * 3600 + arr[1] * 60 + (+arr[2])) // converting to s
|
|
|
|
console.clear()
|
|
//If 2nd pass add that portion in
|
|
console.log(`Encoding ${out}.webm with "${presetName}" preset...`)
|
|
if (isTwoPass) {
|
|
if (seconds / 2 >= (duration - 0.2) / 2) isPastHalf = true
|
|
isPastHalf ? bar1.update(Math.round(seconds * 50) / 100 + (duration / 2)) : bar1.update(Math.round(seconds * 50) / 100)
|
|
}
|
|
else {
|
|
bar1.update(Math.round(seconds * 100) / 100)
|
|
}
|
|
}
|
|
})
|
|
|
|
encoder.on("close", () => {
|
|
console.clear()
|
|
bar1.stop()
|
|
fs.rm("ffmpeg2pass-0.log", (error) => { error })
|
|
term.bold.green("Finished!\n")
|
|
term.grey("press enter to exit...\n")
|
|
term.inputField(() => { process.exit() })
|
|
})
|
|
}
|
|
async function encodeAudio(path, out) {
|
|
let [duration, resolution] = await getDurationAndResolution(path)
|
|
const bitrateLimit = Math.round(62000 / duration)
|
|
|
|
bar1.start(duration, 0, { speed: "N/A" })
|
|
|
|
const encoder = exec(`ffmpeg -y -i "${path}" -c:a libvorbis -b:a ${bitrateLimit}k ${out}.ogg`)
|
|
|
|
encoder.stderr.on("data", (chunk) => {
|
|
currentTime = chunk.split("time=")[1]?.split(" ")[0]
|
|
if (currentTime) {
|
|
const arr = currentTime.split(":")
|
|
let seconds = Number.parseFloat(arr[0] * 3600 + arr[1] * 60 + (+arr[2])) // converting to s
|
|
console.clear()
|
|
console.log(`Encoding ${out}.ogg`)
|
|
bar1.update(Math.round(seconds * 100) / 100)
|
|
}
|
|
})
|
|
encoder.on("close", () => {
|
|
console.clear()
|
|
term.bold.green("Finished!\n")
|
|
term.grey("press enter to exit...\n")
|
|
term.inputField(() => { process.exit() })
|
|
})
|
|
}
|
|
|
|
async function encodePicture(path, out) {
|
|
const encoder = exec(`ffmpeg -y -i "${path}" -qscale 80 -compression_level 6 ${out}.webp`)
|
|
encoder.stderr.on("data", (chunk) => {
|
|
console.clear()
|
|
term.yellow(`Encoding ${out}.webp...`)
|
|
|
|
})
|
|
encoder.on("close", () => {
|
|
console.clear()
|
|
term.bold.green("Finished!\n")
|
|
term.grey("press enter to exit...\n")
|
|
term.inputField(() => { process.exit() })
|
|
})
|
|
}
|
|
|
|
async function constructVideoCommand(path, out) {
|
|
|
|
//gets settings file, if doesnt exist makes a new file and uses those defaults
|
|
let settings = await getSettings().catch(async (err) => {
|
|
settings = undefined
|
|
})
|
|
if (!settings) settings = await makeNewSettingsFile()
|
|
settings = JSON.parse(settings.toString())
|
|
settings = settings.presets[settings.currentSetting]
|
|
|
|
let [duration, resolutionHeight] = await getDurationAndResolution(path)
|
|
//Calculates video bitrate to fit right under 8mb @224kb vorbis audio bitrate
|
|
const bitrateLimit = Math.round((62000 - (224 * duration)) / duration)
|
|
|
|
let command = ""
|
|
let crfIndex = 0
|
|
let isTwoPass = true
|
|
while (resolutionHeight > settings.crfMap[crfIndex].resolution) {
|
|
crfIndex++
|
|
//if the resolution is still higher, just use highest res
|
|
if (!settings.crfMap[crfIndex]?.resolution) {
|
|
crfIndex--
|
|
break
|
|
}
|
|
}
|
|
|
|
for (pass = 1; pass <= 2; pass++) {
|
|
command += `ffmpeg -y -i "${path}" -vcodec libvpx-vp9 -acodec libvorbis -qscale:a 7 `
|
|
command += `-deadline ${settings.deadline} `
|
|
command += `-cpu-used ${settings.cpuUsed} `
|
|
if (settings?.minrate) {
|
|
command += `-b:v ${Math.round(bitrateLimit * 0.95)}k `
|
|
command += `-minrate ${Math.round(bitrateLimit / 100 * settings.minrate)}k `
|
|
command += `-maxrate ${bitrateLimit}k `
|
|
}
|
|
else {
|
|
command += `-b:v ${bitrateLimit}k `
|
|
command += `-crf ${settings.crfMap[crfIndex].crf} `
|
|
}
|
|
//realtime doesnt support two pass
|
|
if (settings.deadline == "realtime") {
|
|
command += `-row-mt 1 "${out}.webm"`
|
|
isTwoPass = false
|
|
break
|
|
}
|
|
pass == 1 ? command += `-pass 1 -row-mt 1 -f webm NUL && ` : command += `-pass 2 -row-mt 1 "${out}.webm" `
|
|
}
|
|
return [command, settings.name, duration, isTwoPass]
|
|
}
|
|
|
|
async function getDurationAndResolution(file) {
|
|
let query = await ffprobe(file)
|
|
//duration in seconds
|
|
duration = query.split("Duration: ")[1].split(",")[0]
|
|
const arr = duration.split(":") // splitting the string by colon
|
|
const seconds = arr[0] * 3600 + arr[1] * 60 + (+arr[2]) // converting to s
|
|
|
|
//resolution height
|
|
resolutionHeight = query.split("Stream #0:0")[1]?.split(",")[2].split(" ")[1].split("x")[1]
|
|
|
|
return [Number.parseFloat(seconds), resolutionHeight]
|
|
}
|
|
|
|
function ffprobe(file) {
|
|
return new Promise((resolve, reject) => {
|
|
exec(`ffprobe "${file}"`, (error, stdout, stderr) => {
|
|
resolve(stderr)
|
|
})
|
|
})
|
|
}
|
|
|
|
function getSettings() {
|
|
return new Promise((resolve, reject) => {
|
|
getSettings = fs.readFile("settings.json", (err, data) => {
|
|
resolve(data)
|
|
reject(err)
|
|
})
|
|
})
|
|
}
|
|
|
|
async function startMenu() {
|
|
console.clear()
|
|
//gets settings file, if doesnt exist makes a new file and uses those defaults
|
|
let settings = await getSettings().catch(async (err) => {
|
|
settings = undefined
|
|
})
|
|
if (!settings) settings = await makeNewSettingsFile()
|
|
|
|
settings = JSON.parse(settings.toString())
|
|
let menu = []
|
|
for (i = 0; i < settings.presets.length; i++) {
|
|
menu.push(`${i}. ${settings.presets[i].name}`)
|
|
}
|
|
term.italic("How to convert: [app] [filename.extension]\n")
|
|
term.yellow("Hello! This menu is for selecting performance/speed preset.\n")
|
|
term.yellow("Currently using ").bgMagenta(`"${settings.presets[settings.currentSetting].name}"`).yellow(" preset")
|
|
term.singleColumnMenu(menu, (error, response) => {
|
|
settings.currentSetting = response.selectedIndex
|
|
fs.writeFileSync("settings.json", JSON.stringify(settings))
|
|
term.green("\n Using").green.bold(` ${settings.presets[settings.currentSetting].name} `).green("setting\n")
|
|
term.grey("Press enter to exit...")
|
|
term.inputField(() => { process.exit() })
|
|
})
|
|
}
|
|
|
|
function makeNewSettingsFile() {
|
|
const settings = `
|
|
{
|
|
"currentSetting": 2,
|
|
"presets": [{
|
|
"name": "Most efficient 8 megabytes of your life",
|
|
"cpuUsed": 0,
|
|
"deadline": "best",
|
|
"minrate": 90,
|
|
"crfMap": [{
|
|
"resolution": 240,
|
|
"crf": 1
|
|
},
|
|
{
|
|
"resolution": 360,
|
|
"crf": 1
|
|
},
|
|
{
|
|
"resolution": 480,
|
|
"crf": 1
|
|
},
|
|
{
|
|
"resolution": 720,
|
|
"crf": 1
|
|
},
|
|
{
|
|
"resolution": 1080,
|
|
"crf": 1
|
|
},
|
|
{
|
|
"resolution": 1440,
|
|
"crf": 1
|
|
},
|
|
{
|
|
"resolution": 2160,
|
|
"crf": 1
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "I have some time to kill",
|
|
"cpuUsed": 1,
|
|
"deadline": "good",
|
|
"minrate": 75,
|
|
"crfMap": [{
|
|
"resolution": 240,
|
|
"crf": 20
|
|
},
|
|
{
|
|
"resolution": 360,
|
|
"crf": 20
|
|
},
|
|
{
|
|
"resolution": 480,
|
|
"crf": 20
|
|
},
|
|
{
|
|
"resolution": 720,
|
|
"crf": 20
|
|
},
|
|
{
|
|
"resolution": 1080,
|
|
"crf": 17
|
|
},
|
|
{
|
|
"resolution": 1440,
|
|
"crf": 15
|
|
},
|
|
{
|
|
"resolution": 2160,
|
|
"crf": 10
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "Mid",
|
|
"cpuUsed": 3,
|
|
"deadline": "good",
|
|
"minrate":75,
|
|
"crfMap": [{
|
|
"resolution": 240,
|
|
"crf": 30
|
|
},
|
|
{
|
|
"resolution": 360,
|
|
"crf": 30
|
|
},
|
|
{
|
|
"resolution": 480,
|
|
"crf": 30
|
|
},
|
|
{
|
|
"resolution": 720,
|
|
"crf": 25
|
|
},
|
|
{
|
|
"resolution": 1080,
|
|
"crf": 20
|
|
},
|
|
{
|
|
"resolution": 1440,
|
|
"crf": 15
|
|
},
|
|
{
|
|
"resolution": 2160,
|
|
"crf": 10
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "I don't like waiting",
|
|
"cpuUsed": 4,
|
|
"deadline": 100,
|
|
"minrate": 90,
|
|
"crfMap": [{
|
|
"resolution": 240,
|
|
"crf": 45
|
|
},
|
|
{
|
|
"resolution": 360,
|
|
"crf": 42
|
|
},
|
|
{
|
|
"resolution": 480,
|
|
"crf": 40
|
|
},
|
|
{
|
|
"resolution": 720,
|
|
"crf": 35
|
|
},
|
|
{
|
|
"resolution": 1080,
|
|
"crf": 30
|
|
},
|
|
{
|
|
"resolution": 1440,
|
|
"crf": 25
|
|
},
|
|
{
|
|
"resolution": 2160,
|
|
"crf": 20
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "I want it, NOW!",
|
|
"cpuUsed": 4,
|
|
"deadline": "realtime",
|
|
"minrate": 50,
|
|
"crfMap": [{
|
|
"resolution": 240,
|
|
"crf": 40
|
|
},
|
|
{
|
|
"resolution": 360,
|
|
"crf": 35
|
|
},
|
|
{
|
|
"resolution": 480,
|
|
"crf": 30
|
|
},
|
|
{
|
|
"resolution": 720,
|
|
"crf": 25
|
|
},
|
|
{
|
|
"resolution": 1080,
|
|
"crf": 20
|
|
},
|
|
{
|
|
"resolution": 1440,
|
|
"crf": 15
|
|
},
|
|
{
|
|
"resolution": 2160,
|
|
"crf": 10
|
|
}
|
|
]
|
|
}
|
|
|
|
]
|
|
}
|
|
`
|
|
return new Promise((resolve, reject) => {
|
|
fs.writeFile("settings.json", settings, () => {
|
|
resolve(settings)
|
|
})
|
|
})
|
|
} |