From 70f065a3a246f3119f3e0b5788bb9e4ab1e1150a Mon Sep 17 00:00:00 2001 From: PickleRick Date: Wed, 30 Sep 2026 23:58:28 +0200 Subject: [PATCH] feat(transcode): scale 2-pass progress to 50% per pass for total 100% --- .../video/transcode2PassX265/1.0.0/index.js | 84 +++++++++++++++++-- tests/quick-logic-test.js | 10 ++- 2 files changed, 87 insertions(+), 7 deletions(-) diff --git a/LocalFlowPlugins/video/transcode2PassX265/1.0.0/index.js b/LocalFlowPlugins/video/transcode2PassX265/1.0.0/index.js index 0a3e8a1..d1f7bd9 100644 --- a/LocalFlowPlugins/video/transcode2PassX265/1.0.0/index.js +++ b/LocalFlowPlugins/video/transcode2PassX265/1.0.0/index.js @@ -156,6 +156,17 @@ const shouldCopyVideo = (videoStream, copyHevcBelowKbps, streams, format) => { && bitrateKbps > 0 && bitrateKbps < copyHevcBelowKbps; }; +const scalePassPercentage = (pass, percentage) => { + const p = Math.min(100, Math.max(0, Number(percentage) || 0)); + if (pass === 1) { + return +(p * 0.5).toFixed(1); + } + if (pass === 2) { + return +(50 + p * 0.5).toFixed(1); + } + return p; +}; + // ─── Plugin ─────────────────────────────────────────────────────────────────── const plugin = async (args) => { @@ -297,9 +308,62 @@ const plugin = async (args) => { } const { CLI } = require('../../../../FlowHelpers/1.0.0/cliUtils'); + const { updateProgress } = args; + + // Scale 2-pass progress so Pass 1 counts for 0-50% and Pass 2 counts for 50-100% + const pass1UpdateWorker = (progressData) => { + if (typeof updateWorker !== 'function') return; + if (progressData && typeof progressData.percentage === 'number') { + updateWorker({ + ...progressData, + percentage: scalePassPercentage(1, progressData.percentage), + }); + } else { + updateWorker(progressData); + } + }; + + const pass1UpdateProgress = (progressData) => { + if (typeof updateProgress !== 'function') return; + if (progressData && typeof progressData.progress === 'number') { + updateProgress({ + ...progressData, + progress: scalePassPercentage(1, progressData.progress), + }); + } else { + updateProgress(progressData); + } + }; + + const pass2UpdateWorker = (progressData) => { + if (typeof updateWorker !== 'function') return; + if (progressData && typeof progressData.percentage === 'number') { + updateWorker({ + ...progressData, + percentage: scalePassPercentage(2, progressData.percentage), + }); + } else { + updateWorker(progressData); + } + }; + + const pass2UpdateProgress = (progressData) => { + if (typeof updateProgress !== 'function') return; + if (progressData && typeof progressData.progress === 'number') { + updateProgress({ + ...progressData, + progress: scalePassPercentage(2, progressData.progress), + }); + } else { + updateProgress(progressData); + } + }; // ══ PASS 1 ════════════════════════════════════════════════════════════════ jobLog('[transcode2PassX265] Starting Pass 1...\n'); + pass1UpdateWorker({ percentage: 0 }); + pass1UpdateProgress({ progress: 0 }); + const pass1Cli = new CLI({ cli: args.ffmpegPath, spawnArgs: [ @@ -317,16 +381,18 @@ const plugin = async (args) => { WorkDir: workDir, jobLog, inputFileObj, - updateWorker, + updateWorker: pass1UpdateWorker, logFullCliOutput: args.logFullCliOutput || false, - args, + args: { ...args, updateWorker: pass1UpdateWorker, updateProgress: pass1UpdateProgress }, }); const { cliExitCode: pass1Exit } = await pass1Cli.runCli(); if (pass1Exit !== 0) { throw new Error(`[transcode2PassX265] Pass 1 failed with exit code ${pass1Exit}`); } - jobLog('[transcode2PassX265] Pass 1 complete.\n'); + pass1UpdateWorker({ percentage: 100 }); + pass1UpdateProgress({ progress: 100 }); + jobLog('[transcode2PassX265] Pass 1 complete (50% total progress).\n'); // ══ PASS 2 ════════════════════════════════════════════════════════════════ const audioArgs = buildAudioArgs(variables); @@ -335,6 +401,9 @@ const plugin = async (args) => { } jobLog('[transcode2PassX265] Starting Pass 2...\n'); + pass2UpdateWorker({ percentage: 0 }); + pass2UpdateProgress({ progress: 0 }); + const pass2Cli = new CLI({ cli: args.ffmpegPath, spawnArgs: [ @@ -355,16 +424,18 @@ const plugin = async (args) => { jobLog, outputFilePath: outFile, inputFileObj, - updateWorker, + updateWorker: pass2UpdateWorker, logFullCliOutput: args.logFullCliOutput || false, - args, + args: { ...args, updateWorker: pass2UpdateWorker, updateProgress: pass2UpdateProgress }, }); const { cliExitCode: pass2Exit } = await pass2Cli.runCli(); if (pass2Exit !== 0) { throw new Error(`[transcode2PassX265] Pass 2 failed with exit code ${pass2Exit}`); } - jobLog('[transcode2PassX265] Pass 2 complete.\n'); + pass2UpdateWorker({ percentage: 100 }); + pass2UpdateProgress({ progress: 100 }); + jobLog('[transcode2PassX265] Pass 2 complete (100% total progress).\n'); // ── Cleanup 2-pass log files and temporary input copy ──────────────────── [statsFile, `${statsFile}.mbtree`, `${statsFile}.cutree`].forEach((f) => { @@ -389,3 +460,4 @@ const plugin = async (args) => { module.exports.details = details; module.exports.plugin = plugin; +module.exports.scalePassPercentage = scalePassPercentage; diff --git a/tests/quick-logic-test.js b/tests/quick-logic-test.js index 0d1cbf4..d67c79a 100644 --- a/tests/quick-logic-test.js +++ b/tests/quick-logic-test.js @@ -236,8 +236,16 @@ const AUDIO_INPUTS = { ok(transcodeCode.includes("normalizedKey === 'bps' || normalizedKey === 'bps-eng'"), 'HEVC copy check falls back to Matroska BPS metadata'); ok(transcodeCode.includes('containerBitrate - otherStreamBitrate'), 'HEVC copy check estimates video bitrate from container and companion streams'); ok(!transcodeCode.includes("'-map_metadata -1'"), 'pass 1 separates -map_metadata from its value'); - ok(!transcodeCode.includes("'-map_chapters -1'"), 'pass 1 separates -map_chapters from its value'); ok(transcodeCode.includes('const statsForX265 = path.basename(statsFile)'), 'x265 stats file is relative to workDir on Windows'); + const { scalePassPercentage } = require(path.join(ROOT, 'LocalFlowPlugins/video/transcode2PassX265/1.0.0/index.js')); + eq(scalePassPercentage(1, 0), 0, 'pass 1 at 0% maps to 0%'); + eq(scalePassPercentage(1, 50), 25, 'pass 1 at 50% maps to 25%'); + eq(scalePassPercentage(1, 100), 50, 'pass 1 at 100% maps to 50%'); + eq(scalePassPercentage(2, 0), 50, 'pass 2 at 0% maps to 50%'); + eq(scalePassPercentage(2, 50), 75, 'pass 2 at 50% maps to 75%'); + eq(scalePassPercentage(2, 100), 100, 'pass 2 at 100% maps to 100%'); + ok(transcodeCode.includes('scalePassPercentage(1,'), 'pass 1 wraps updateWorker with 50% scale'); + ok(transcodeCode.includes('scalePassPercentage(2,'), 'pass 2 wraps updateWorker with 50% scale'); // ── copyToWorkDir & replaceOriginalFile v1.0.0 checks ───────────────────── const copyPluginCode = require('fs').readFileSync(path.join(ROOT, 'LocalFlowPlugins/utility/copyToWorkDir/1.0.0/index.js'), 'utf8');