Created
April 24, 2023 23:48
-
-
Save pjobson/149b722df675f44362f9911632353eb6 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env node | |
// Script for converting hevc to avc (h265 to h264) | |
const fs = require('fs'); | |
const os = require('os'); | |
const path = require('path'); | |
const process = require('process'); | |
const shellescape = require('shell-escape'); | |
const child_process = require('child_process'); | |
const cliProgress = require('cli-progress'); | |
const FFMPEG = '/usr/local/bin/ffmpeg -hide_banner'; | |
const FFPROBE = '/usr/local/bin/ffprobe -hide_banner' | |
const MKVMERGE = '/usr/bin/mkvmerge'; | |
const MKVEXTRACT = '/usr/bin/mkvextract'; | |
const hevc = process.argv[2]; | |
const outpath = path.resolve(process.argv[3] || path.parse(hevc).dir); | |
const tmppath = `${outpath}/${Date.now()}`; | |
const outfile = `${path.parse(hevc).name}.H264.mkv`; | |
const inp = shellescape([hevc]); | |
const out = shellescape([`${outpath}/${outfile}`]); | |
const tempSE = shellescape([tmppath]); | |
const videoTracks = []; | |
let frameCount = 0; | |
const init = async () => { | |
if (fs.existsSync(`${outpath}/${outfile}`)) { | |
console.log(`Exiting, file exists: ${outpath}/${outfile}`); | |
process.exit(0); | |
} | |
const mkdirExec = `mkdir -p ${tempSE}`; | |
child_process.execSync(mkdirExec); | |
const infoExec = `${MKVMERGE} -J ${inp}`; | |
const infoJSON = child_process.execSync(infoExec); | |
const tracks = JSON.parse(infoJSON.toString()).tracks; | |
let mkvExtExec = `${MKVEXTRACT} tracks ${inp} `; | |
tracks.forEach(trk => { | |
mkvExtExec += `${trk.id}:`; | |
let trackPath = `${tmppath}/`; | |
switch (trk.type) { | |
case 'video': | |
trackPath += `${trk.id}.video`; | |
break; | |
case 'subtitles': | |
trackPath += `${trk.id}.${trk.properties.language}.subtitle`; | |
break; | |
default: | |
trackPath += `${trk.id}.${trk.type}.${trk.properties.language}` | |
} | |
if (trk.type === 'video') { | |
videoTracks.push(trackPath); | |
} | |
trackPath = shellescape([trackPath]); | |
mkvExtExec += `${trackPath} `; | |
}); | |
console.log(`Starting: ${hevc}`); | |
console.log(`Extracting tracks...`); | |
const extractExec = child_process.exec(mkvExtExec); | |
const trackBar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic); | |
trackBar.start(100, 0); | |
extractExec.stdout.on('data', (txt) => { | |
if (/Progress:/.test(txt)) { | |
const n = parseInt(txt.match(/\d+/)[0], 10); | |
trackBar.update(n); | |
} | |
}); | |
extractExec.on('exit', () => { | |
trackBar.update(100) | |
trackBar.stop(); | |
calculateFrames(); | |
}); | |
}; | |
const calculateFrames = () => { | |
console.log('Getting frame count.'); | |
const ffprobeCmd = `${FFPROBE} ${inp}`; | |
const probeExec = child_process.exec(ffprobeCmd); | |
let idx=0; | |
let probOut = ''; | |
let durationSec = 0; | |
probeExec.stderr.on('data', (txt) => { | |
probOut += txt; | |
}); | |
probeExec.on('close', (data) => { | |
[...probOut.split('\n')].forEach(line => { | |
if (/Duration:/.test(line)) { | |
let [t,hh,mm,ss] = line.match(/(\d+):(\d+):(\d+\.\d+)/); | |
durationSec = (parseInt(hh, 10)*60*60) + (parseInt(mm, 10)*60) + (parseFloat(ss)); | |
} | |
if (/Video:/.test(line)) { | |
const [f,fps] = line.match(/(\d+(?:\.\d+){0,1}) fps/); | |
const frames = Math.ceil(fps * durationSec); | |
videoTracks[idx++] = [videoTracks[idx-1], frames]; | |
} | |
}); | |
MP4Conversion(); | |
}); | |
}; | |
const MP4Conversion = () => { | |
if (videoTracks.length === 0) { | |
buildMkv(); | |
return; | |
} | |
// convert video to mp4 | |
console.log('Transcoding video track...'); | |
const mp4Bar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic); | |
const videoTrack = videoTracks.pop(); | |
const vidIn = shellescape([videoTrack[0]]); | |
const vidOut = shellescape([`${tmppath}/${path.parse(videoTrack[0]).base}\.mp4`]); | |
const ffmpegCmd = `${FFMPEG} -y -i ${vidIn} -c:v libx264 -preset ultrafast -crf 22 ${vidOut}`; | |
mp4Bar.start(videoTrack[1], 0); | |
const convertExec = child_process.exec(ffmpegCmd); | |
convertExec.stderr.on('data', (txt) => { | |
const frame = txt.match(/frame=\s*(\d+)/); | |
if (frame) { | |
mp4Bar.update(parseInt(frame[1], 10)); | |
} | |
}); | |
convertExec.on('exit', () => { | |
mp4Bar.update(videoTrack[1]); | |
mp4Bar.stop(); | |
fs.unlinkSync(`${tmppath}/${path.parse(videoTrack[0]).base}`); | |
MP4Conversion(); | |
}); | |
}; | |
const buildMkv = () => { | |
console.log('Building MKV...'); | |
const mergeCmd = `${MKVMERGE} -o ${out} ${tempSE}/*`; | |
const mkvBar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic); | |
mkvBar.start(100, 0); | |
const toMKVExec = child_process.exec(mergeCmd); | |
toMKVExec.stdout.on('data', (txt) => { | |
if (/Progress:/.test(txt)) { | |
const n = parseInt(txt.match(/\d+/)[0], 10); | |
mkvBar.update(n); | |
} | |
}); | |
toMKVExec.on('exit', () => { | |
mkvBar.update(100); | |
mkvBar.stop(); | |
console.log('Removing temp files.'); | |
fs.rmSync(tmppath, { recursive: true, force: true }); | |
}); | |
}; | |
init(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment