fix: update GOP parameters for mediaAnalyzerGOP and transcode2PassX265 to enhance encoding efficiency and documentation
Build / Lint & Test (push) Successful in 16s

This commit is contained in:
PickleRick committed 2026-09-20 19:37:31 +02:00
1 parent 36184fbf32
commit 3f0b38d513
7 files changed
+38 -19

No files matched your search

@@ -6,7 +6,9 @@
* mediaAnalyzerGOP
* Analyzes the video stream and populates args.variables with encoding
* parameters consumed by downstream plugins:
* gopKeyint / gopMinKeyint -> 2 x FPS (rounded)
* gopKeyint -> 4 x FPS (rounded; maximum 4-second GOP)
* gopMinKeyint -> 1 x FPS (rounded; permits scene cuts)
* gopSceneCut -> x265 scene-cut threshold (40)
* isAnimation -> detected from animeSeries path list
* isMovie -> detected from moviePathPatterns
* vBitrate / vMaxRate / vBufSize
@@ -17,7 +19,7 @@
const details = () => ({
name: 'Media Analyzer & GOP Calculator',
description:
'Analyzes video metadata, calculates GOP keyint (2xFPS), target bitrate '
'Analyzes video metadata, calculates a streaming-friendly 4-second GOP, target bitrate '
+ 'and VBV parameters based on content type (film/series/anime). '
+ 'Exports all values to args.variables for downstream plugins.',
style: { borderColor: '#2196F3' },
@@ -123,8 +125,13 @@ const plugin = async (args) => {
}
jobLog(`[mediaAnalyzerGOP] FPS: ${fps.toFixed(3)}\n`);
const gopKeyint = String(Math.round(fps * 2));
jobLog(`[mediaAnalyzerGOP] keyint=${gopKeyint}\n`);
// A four-second maximum GOP is a compromise for remote streaming: it
// reduces regular I-frame overhead while keeping seek recovery short.
// A one-second minimum leaves x265 free to insert an I-frame at a real cut.
const gopKeyint = String(Math.round(fps * 4));
const gopMinKeyint = String(Math.round(fps));
const gopSceneCut = '40';
jobLog(`[mediaAnalyzerGOP] keyint=${gopKeyint} min-keyint=${gopMinKeyint} scenecut=${gopSceneCut}\n`);
// Normalize to forward slashes for cross-platform path matching
const normalizedPath = filePath.replace(/\\/g, '/');
@@ -149,7 +156,8 @@ const plugin = async (args) => {
jobLog(`[mediaAnalyzerGOP] vBitrate=${vBitrate} vMaxRate=${vMaxRate} vBufSize=${vBufSize}\n`);
variables.gopKeyint = gopKeyint;
variables.gopMinKeyint = gopKeyint;
variables.gopMinKeyint = gopMinKeyint;
variables.gopSceneCut = gopSceneCut;
variables.isAnimation = String(isAnimation);
variables.isMovie = String(isMovie);
variables.vBitrate = vBitrate;
@@ -166,8 +166,9 @@ const plugin = async (args) => {
// ── Read variables ────────────────────────────────────────────────────────
const {
gopKeyint = '48',
gopMinKeyint = '48',
gopKeyint = '96',
gopMinKeyint = '24',
gopSceneCut = '40',
vBitrate = '3000k',
vMaxRate = '6000k',
vBufSize = '12000k',
@@ -189,7 +190,8 @@ const plugin = async (args) => {
jobLog(
`[transcode2PassX265] Params: bitrate=${vBitrate} maxrate=${maxRateNum}k `
+ `bufsize=${bufSizeNum}k keyint=${gopKeyint} tune=${x265Tune || 'none'} preset=${x265Preset}\n`,
+ `bufsize=${bufSizeNum}k keyint=${gopKeyint} min-keyint=${gopMinKeyint} scenecut=${gopSceneCut} `
+ `tune=${x265Tune || 'none'} preset=${x265Preset}\n`,
);
const sourceBitrateKbps = getVideoBitrateKbps(videoStream, streams, format).toFixed(0);
jobLog(
@@ -219,7 +221,7 @@ const plugin = async (args) => {
const x265ParamsBase = [
`keyint=${gopKeyint}`,
`min-keyint=${gopMinKeyint}`,
`scenecut=0`,
`scenecut=${gopSceneCut}`,
`open-gop=0`,
`vbv-maxrate=${maxRateNum}`,
`vbv-bufsize=${bufSizeNum}`,
+3 -3
View File
@@ -126,7 +126,7 @@ Reads video stream metadata and populates `args.variables` for all downstream pl
**Outputs**: `1` → continue
**Variables set**: `gopKeyint`, `gopMinKeyint`, `isAnimation`, `isMovie`, `vBitrate`, `vMaxRate`, `vBufSize`, `x265Tune`, `videoHeight`, `videoFps`
**Variables set**: `gopKeyint` (4 s max GOP), `gopMinKeyint` (1 s), `gopSceneCut` (40), `isAnimation`, `isMovie`, `vBitrate`, `vMaxRate`, `vBufSize`, `x265Tune`, `videoHeight`, `videoFps`
---
@@ -237,14 +237,14 @@ Performs 2-pass libx265 encoding using VBV parameters from `args.variables`.
**Outputs**: `1` → transcode complete
**Reads from variables**: `gopKeyint`, `gopMinKeyint`, `vBitrate`, `vMaxRate`, `vBufSize`, `x265Tune`, `audioHasIta`, `audioItaStreamIndex`, `audioItaOpusBitrate`, `audioHasEng`, `audioEngStreamIndex`, `audioEngOpusBitrate`
**Reads from variables**: `gopKeyint`, `gopMinKeyint`, `gopSceneCut`, `vBitrate`, `vMaxRate`, `vBufSize`, `x265Tune`, `audioHasIta`, `audioItaStreamIndex`, `audioItaOpusBitrate`, `audioHasEng`, `audioEngStreamIndex`, `audioEngOpusBitrate`
---
## 🔗 Flow Variable Map
```
mediaAnalyzerGOP ──► gopKeyint, gopMinKeyint, vBitrate, vMaxRate, vBufSize
mediaAnalyzerGOP ──► gopKeyint, gopMinKeyint, gopSceneCut, vBitrate, vMaxRate, vBufSize
──► isAnimation, isMovie, x265Tune, videoHeight, videoFps
│
(optional) setAnimationFlag ──► overrides vBitrate, vMaxRate, vBufSize, x265Tune
+1
View File
@@ -165,6 +165,7 @@ Questa tabella mostra tutte le variabili che i plugin si passano a vicenda:
|---|---|---|
| `gopKeyint` | mediaAnalyzerGOP | transcode2PassX265 |
| `gopMinKeyint` | mediaAnalyzerGOP | transcode2PassX265 |
| `gopSceneCut` | mediaAnalyzerGOP | transcode2PassX265 |
| `isAnimation` | mediaAnalyzerGOP / setAnimationFlag | audioSelectorClamping |
| `isMovie` | mediaAnalyzerGOP | audioSelectorClamping |
| `vBitrate` | mediaAnalyzerGOP / setAnimationFlag | transcode2PassX265 |
+5 -3
View File
@@ -52,7 +52,9 @@ const AUDIO_INPUTS = {
{ const a = makeArgs('/media/toshiba/Radarr/Film (2023)/Film.mkv',
[makeStream({ r_frame_rate:'24000/1001', height:1080 })], ANALYZER_INPUTS);
const o = await analyzerPlugin(a);
eq(o.variables.gopKeyint, '48', '23.976fps -> keyint=48');
eq(o.variables.gopKeyint, '96', '23.976fps -> keyint=96 (4 seconds)');
eq(o.variables.gopMinKeyint, '24', '23.976fps -> min-keyint=24 (1 second)');
eq(o.variables.gopSceneCut, '40', 'scene-cut threshold is 40');
eq(o.variables.isMovie, 'true', '/Radarr -> isMovie=true');
eq(o.variables.isAnimation, 'false', 'no anime -> isAnimation=false');
eq(o.variables.vBitrate, '3000k', 'film bitrate=3000k');
@@ -64,7 +66,7 @@ const AUDIO_INPUTS = {
{ const a = makeArgs('/media/toshiba/Sonarr/Naruto S01/S01E01.mkv',
[makeStream({ r_frame_rate:'25/1', height:720 })], ANALYZER_INPUTS);
const o = await analyzerPlugin(a);
eq(o.variables.gopKeyint, '50', '25fps -> keyint=50');
eq(o.variables.gopKeyint, '100', '25fps -> keyint=100 (4 seconds)');
eq(o.variables.isMovie, 'false', '/Sonarr -> isMovie=false');
eq(o.variables.isAnimation, 'true', 'Naruto -> isAnimation=true');
eq(o.variables.x265Tune, 'animation', 'anime -> tune=animation');
@@ -74,7 +76,7 @@ const AUDIO_INPUTS = {
{ const a = makeArgs('/media/toshiba/Sonarr/Naruto S01/S01E01.mkv',
[makeStream({ r_frame_rate:'30000/1001', height:480 })], ANALYZER_INPUTS);
const o = await analyzerPlugin(a);
eq(o.variables.gopKeyint, '60', '29.97fps -> keyint=60');
eq(o.variables.gopKeyint, '120', '29.97fps -> keyint=120 (4 seconds)');
eq(o.variables.vBitrate, '1000k', 'anime SD 480p -> 1000k');
cleanup(a); }
+5 -2
View File
@@ -6,7 +6,8 @@
* File: Radarr path, AC3 ITA 448k 5.1, EAC3 ENG 640k 5.1
*
* Expected:
* gopKeyint = "48"
* gopKeyint = "96"
* gopMinKeyint = "24"
* isMovie = "true"
* isAnimation = "false"
* vBitrate = "3000k"
@@ -44,7 +45,9 @@ async function run() {
const vars = out1.variables;
assertEqual(out1.outputNumber, 1, 'outputNumber');
assertEqual(vars.gopKeyint, '48', 'gopKeyint (2 x 23.976 = 47.95 -> 48)');
assertEqual(vars.gopKeyint, '96', 'gopKeyint (4 x 23.976 = 95.90 -> 96)');
assertEqual(vars.gopMinKeyint,'24', 'gopMinKeyint (1 x 23.976 = 23.98 -> 24)');
assertEqual(vars.gopSceneCut, '40', 'gopSceneCut (x265 default threshold)');
assertEqual(vars.isMovie, 'true', 'isMovie (Radarr path)');
assertEqual(vars.isAnimation, 'false', 'isAnimation (no anime match)');
assertEqual(vars.vBitrate, '3000k', 'vBitrate (film default)');
+5 -2
View File
@@ -6,7 +6,8 @@
* Note: DTS detection logic is fully covered in unit tests (quick-logic-test.js).
*
* Expected:
* gopKeyint = "50"
* gopKeyint = "100"
* gopMinKeyint = "25"
* isAnimation = "true"
* isMovie = "false"
* x265Tune = "animation"
@@ -40,7 +41,9 @@ async function run() {
});
const out1 = await analyzerPlugin(args1);
const vars = out1.variables;
assertEqual(vars.gopKeyint, '50', 'gopKeyint (2 x 25 = 50)');
assertEqual(vars.gopKeyint, '100', 'gopKeyint (4 x 25 = 100)');
assertEqual(vars.gopMinKeyint,'25', 'gopMinKeyint (1 x 25 = 25)');
assertEqual(vars.gopSceneCut, '40', 'gopSceneCut (x265 default threshold)');
assertEqual(vars.isMovie, 'false', 'isMovie=false (Sonarr path)');
assertEqual(vars.isAnimation, 'true', 'isAnimation=true (Naruto in path)');
assertEqual(vars.x265Tune, 'animation', 'x265Tune=animation');