Skip to content

Instantly share code, notes, and snippets.

@pjobson
Created November 11, 2020 12:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pjobson/b46585c13e30f5e95872d405adfe3c07 to your computer and use it in GitHub Desktop.
Save pjobson/b46585c13e30f5e95872d405adfe3c07 to your computer and use it in GitHub Desktop.
Convert hevc to avc (h.265 to h.264)
#!/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