Created
November 11, 2020 12:37
-
-
Save pjobson/b46585c13e30f5e95872d405adfe3c07 to your computer and use it in GitHub Desktop.
Convert hevc to avc (h.265 to h.264)
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 | |
const fs = require('fs'); | |
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 = '/home/pjobson/bin/ffmpeg -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}/tmp`; | |
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(`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 FFP = '/home/pjobson/bin/ffprobe -hide_banner'; | |
const ffprobeCmd = `${FFP} ${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.rmdirSync(shellescape([tmppath]), { recursive: true }); | |
}); | |
}; | |
init(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment