fix: enhance copyToWorkDir to log progress and improve video bitrate estimation in transcoding
Build / Lint & Test (push) Successful in 16s

This commit is contained in:
PickleRick committed 2026-09-19 23:10:20 +02:00
1 parent 0c8b713325
commit 36184fbf32
3 files changed
+68 -7

No files matched your search

@@ -107,12 +107,48 @@ const buildAudioArgs = (variables) => {
return [...mapArgs, ...codecArgs];
};
const shouldCopyVideo = (videoStream, copyHevcBelowKbps) => {
const getStreamBitrateKbps = (stream) => {
const streamBitrate = parseInt(stream.bit_rate, 10);
if (streamBitrate > 0) return streamBitrate / 1000;
// Matroska files commonly expose the per-stream bitrate as BPS metadata
// rather than ffprobe's bit_rate field (for example: tags.BPS = "1800356").
const tags = stream.tags || {};
const bpsEntry = Object.entries(tags).find(([key]) => {
const normalizedKey = String(key).toLowerCase();
return normalizedKey === 'bps' || normalizedKey === 'bps-eng';
});
const metadataBitrate = bpsEntry ? parseInt(bpsEntry[1], 10) : 0;
return metadataBitrate > 0 ? metadataBitrate / 1000 : 0;
};
const getVideoBitrateKbps = (videoStream, streams, format) => {
const directBitrate = getStreamBitrateKbps(videoStream);
if (directBitrate > 0) return directBitrate;
// Last resort: derive video bitrate from the container bitrate after
// subtracting any known audio/data stream bitrates. This is deliberately
// conservative if a companion stream does not report a bitrate.
let containerBitrate = parseInt(format && format.bit_rate, 10) / 1000;
if (!(containerBitrate > 0)) {
const size = parseInt(format && format.size, 10);
const duration = parseFloat(format && format.duration);
containerBitrate = size > 0 && duration > 0 ? (size * 8 / duration) / 1000 : 0;
}
if (!(containerBitrate > 0)) return 0;
const otherStreamBitrate = (streams || [])
.filter((stream) => stream !== videoStream)
.reduce((total, stream) => total + getStreamBitrateKbps(stream), 0);
return Math.max(0, containerBitrate - otherStreamBitrate);
};
const shouldCopyVideo = (videoStream, copyHevcBelowKbps, streams, format) => {
if (!videoStream || String(videoStream.codec_name || '').toLowerCase() !== 'hevc') return false;
const width = parseInt(videoStream.width, 10) || 0;
const height = parseInt(videoStream.height, 10) || 0;
const bitrateKbps = (parseInt(videoStream.bit_rate, 10) || 0) / 1000;
const bitrateKbps = getVideoBitrateKbps(videoStream, streams, format);
return copyHevcBelowKbps > 0
&& width > 0 && width <= 1920
@@ -142,9 +178,10 @@ const plugin = async (args) => {
const container = args.inputs.outputContainer || 'mkv';
const copyHevcBelowKbps = Math.max(0, parseInt(args.inputs.copyHevcBelowKbps || '3000', 10) || 0);
const streams = (inputFileObj.ffProbeData && inputFileObj.ffProbeData.streams) || [];
const format = (inputFileObj.ffProbeData && inputFileObj.ffProbeData.format) || {};
const videoStream = streams.find((stream) => stream.codec_type === 'video');
if (!videoStream) throw new Error('[transcode2PassX265] No video stream found.');
const copyVideo = shouldCopyVideo(videoStream, copyHevcBelowKbps);
const copyVideo = shouldCopyVideo(videoStream, copyHevcBelowKbps, streams, format);
// Parse numeric VBV values (parseInt handles trailing 'k')
const maxRateNum = parseInt(vMaxRate, 10);
@@ -154,7 +191,7 @@ const plugin = async (args) => {
`[transcode2PassX265] Params: bitrate=${vBitrate} maxrate=${maxRateNum}k `
+ `bufsize=${bufSizeNum}k keyint=${gopKeyint} tune=${x265Tune || 'none'} preset=${x265Preset}\n`,
);
const sourceBitrateKbps = ((parseInt(videoStream.bit_rate, 10) || 0) / 1000).toFixed(0);
const sourceBitrateKbps = getVideoBitrateKbps(videoStream, streams, format).toFixed(0);
jobLog(
`[transcode2PassX265] Video: codec=${videoStream.codec_name || 'unknown'} `
+ `${videoStream.width || '?'}x${videoStream.height || '?'} bitrate=${sourceBitrateKbps}k `
@@ -165,8 +202,9 @@ const plugin = async (args) => {
const baseName = path.basename(inputFile, path.extname(inputFile));
const outFile = path.join(workDir, `${baseName}_x265.${container}`);
const statsFile = path.join(workDir, 'x265_2pass.log');
// x265 expects forward slashes in x265-params on all platforms
const statsForX265 = statsFile.replace(/\\/g, '/');
// The CLI runs with workDir as its current directory. Keeping this relative
// avoids the Windows drive colon being parsed as an x265-params separator.
const statsForX265 = path.basename(statsFile);
const nullDev = process.platform === 'win32' ? 'NUL' : '/dev/null';
jobLog(`[transcode2PassX265] Output: ${outFile}\n`);