fix: update GOP parameters for mediaAnalyzerGOP and transcode2PassX265 to enhance encoding efficiency and documentation
Build / Lint & Test (push) Successful in 16s
Build / Lint & Test (push) Successful in 16s
This commit is contained in:
1 parent
36184fbf32
commit
3f0b38d513
7 files changed
+38
-19
No files matched your search
@@ -6,7 +6,9 @@
|
|||||||
* mediaAnalyzerGOP
|
* mediaAnalyzerGOP
|
||||||
* Analyzes the video stream and populates args.variables with encoding
|
* Analyzes the video stream and populates args.variables with encoding
|
||||||
* parameters consumed by downstream plugins:
|
* 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
|
* isAnimation -> detected from animeSeries path list
|
||||||
* isMovie -> detected from moviePathPatterns
|
* isMovie -> detected from moviePathPatterns
|
||||||
* vBitrate / vMaxRate / vBufSize
|
* vBitrate / vMaxRate / vBufSize
|
||||||
@@ -17,7 +19,7 @@
|
|||||||
const details = () => ({
|
const details = () => ({
|
||||||
name: 'Media Analyzer & GOP Calculator',
|
name: 'Media Analyzer & GOP Calculator',
|
||||||
description:
|
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). '
|
+ 'and VBV parameters based on content type (film/series/anime). '
|
||||||
+ 'Exports all values to args.variables for downstream plugins.',
|
+ 'Exports all values to args.variables for downstream plugins.',
|
||||||
style: { borderColor: '#2196F3' },
|
style: { borderColor: '#2196F3' },
|
||||||
@@ -123,8 +125,13 @@ const plugin = async (args) => {
|
|||||||
}
|
}
|
||||||
jobLog(`[mediaAnalyzerGOP] FPS: ${fps.toFixed(3)}\n`);
|
jobLog(`[mediaAnalyzerGOP] FPS: ${fps.toFixed(3)}\n`);
|
||||||
|
|
||||||
const gopKeyint = String(Math.round(fps * 2));
|
// A four-second maximum GOP is a compromise for remote streaming: it
|
||||||
jobLog(`[mediaAnalyzerGOP] keyint=${gopKeyint}\n`);
|
// 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
|
// Normalize to forward slashes for cross-platform path matching
|
||||||
const normalizedPath = filePath.replace(/\\/g, '/');
|
const normalizedPath = filePath.replace(/\\/g, '/');
|
||||||
@@ -149,7 +156,8 @@ const plugin = async (args) => {
|
|||||||
jobLog(`[mediaAnalyzerGOP] vBitrate=${vBitrate} vMaxRate=${vMaxRate} vBufSize=${vBufSize}\n`);
|
jobLog(`[mediaAnalyzerGOP] vBitrate=${vBitrate} vMaxRate=${vMaxRate} vBufSize=${vBufSize}\n`);
|
||||||
|
|
||||||
variables.gopKeyint = gopKeyint;
|
variables.gopKeyint = gopKeyint;
|
||||||
variables.gopMinKeyint = gopKeyint;
|
variables.gopMinKeyint = gopMinKeyint;
|
||||||
|
variables.gopSceneCut = gopSceneCut;
|
||||||
variables.isAnimation = String(isAnimation);
|
variables.isAnimation = String(isAnimation);
|
||||||
variables.isMovie = String(isMovie);
|
variables.isMovie = String(isMovie);
|
||||||
variables.vBitrate = vBitrate;
|
variables.vBitrate = vBitrate;
|
||||||
|
|||||||
@@ -166,8 +166,9 @@ const plugin = async (args) => {
|
|||||||
|
|
||||||
// ── Read variables ────────────────────────────────────────────────────────
|
// ── Read variables ────────────────────────────────────────────────────────
|
||||||
const {
|
const {
|
||||||
gopKeyint = '48',
|
gopKeyint = '96',
|
||||||
gopMinKeyint = '48',
|
gopMinKeyint = '24',
|
||||||
|
gopSceneCut = '40',
|
||||||
vBitrate = '3000k',
|
vBitrate = '3000k',
|
||||||
vMaxRate = '6000k',
|
vMaxRate = '6000k',
|
||||||
vBufSize = '12000k',
|
vBufSize = '12000k',
|
||||||
@@ -189,7 +190,8 @@ const plugin = async (args) => {
|
|||||||
|
|
||||||
jobLog(
|
jobLog(
|
||||||
`[transcode2PassX265] Params: bitrate=${vBitrate} maxrate=${maxRateNum}k `
|
`[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);
|
const sourceBitrateKbps = getVideoBitrateKbps(videoStream, streams, format).toFixed(0);
|
||||||
jobLog(
|
jobLog(
|
||||||
@@ -219,7 +221,7 @@ const plugin = async (args) => {
|
|||||||
const x265ParamsBase = [
|
const x265ParamsBase = [
|
||||||
`keyint=${gopKeyint}`,
|
`keyint=${gopKeyint}`,
|
||||||
`min-keyint=${gopMinKeyint}`,
|
`min-keyint=${gopMinKeyint}`,
|
||||||
`scenecut=0`,
|
`scenecut=${gopSceneCut}`,
|
||||||
`open-gop=0`,
|
`open-gop=0`,
|
||||||
`vbv-maxrate=${maxRateNum}`,
|
`vbv-maxrate=${maxRateNum}`,
|
||||||
`vbv-bufsize=${bufSizeNum}`,
|
`vbv-bufsize=${bufSizeNum}`,
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ Reads video stream metadata and populates `args.variables` for all downstream pl
|
|||||||
|
|
||||||
**Outputs**: `1` → continue
|
**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
|
**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
|
## 🔗 Flow Variable Map
|
||||||
|
|
||||||
```
|
```
|
||||||
mediaAnalyzerGOP ──► gopKeyint, gopMinKeyint, vBitrate, vMaxRate, vBufSize
|
mediaAnalyzerGOP ──► gopKeyint, gopMinKeyint, gopSceneCut, vBitrate, vMaxRate, vBufSize
|
||||||
──► isAnimation, isMovie, x265Tune, videoHeight, videoFps
|
──► isAnimation, isMovie, x265Tune, videoHeight, videoFps
|
||||||
│
|
│
|
||||||
(optional) setAnimationFlag ──► overrides vBitrate, vMaxRate, vBufSize, x265Tune
|
(optional) setAnimationFlag ──► overrides vBitrate, vMaxRate, vBufSize, x265Tune
|
||||||
|
|||||||
@@ -165,6 +165,7 @@ Questa tabella mostra tutte le variabili che i plugin si passano a vicenda:
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `gopKeyint` | mediaAnalyzerGOP | transcode2PassX265 |
|
| `gopKeyint` | mediaAnalyzerGOP | transcode2PassX265 |
|
||||||
| `gopMinKeyint` | mediaAnalyzerGOP | transcode2PassX265 |
|
| `gopMinKeyint` | mediaAnalyzerGOP | transcode2PassX265 |
|
||||||
|
| `gopSceneCut` | mediaAnalyzerGOP | transcode2PassX265 |
|
||||||
| `isAnimation` | mediaAnalyzerGOP / setAnimationFlag | audioSelectorClamping |
|
| `isAnimation` | mediaAnalyzerGOP / setAnimationFlag | audioSelectorClamping |
|
||||||
| `isMovie` | mediaAnalyzerGOP | audioSelectorClamping |
|
| `isMovie` | mediaAnalyzerGOP | audioSelectorClamping |
|
||||||
| `vBitrate` | mediaAnalyzerGOP / setAnimationFlag | transcode2PassX265 |
|
| `vBitrate` | mediaAnalyzerGOP / setAnimationFlag | transcode2PassX265 |
|
||||||
|
|||||||
@@ -52,7 +52,9 @@ const AUDIO_INPUTS = {
|
|||||||
{ const a = makeArgs('/media/toshiba/Radarr/Film (2023)/Film.mkv',
|
{ const a = makeArgs('/media/toshiba/Radarr/Film (2023)/Film.mkv',
|
||||||
[makeStream({ r_frame_rate:'24000/1001', height:1080 })], ANALYZER_INPUTS);
|
[makeStream({ r_frame_rate:'24000/1001', height:1080 })], ANALYZER_INPUTS);
|
||||||
const o = await analyzerPlugin(a);
|
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.isMovie, 'true', '/Radarr -> isMovie=true');
|
||||||
eq(o.variables.isAnimation, 'false', 'no anime -> isAnimation=false');
|
eq(o.variables.isAnimation, 'false', 'no anime -> isAnimation=false');
|
||||||
eq(o.variables.vBitrate, '3000k', 'film bitrate=3000k');
|
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',
|
{ const a = makeArgs('/media/toshiba/Sonarr/Naruto S01/S01E01.mkv',
|
||||||
[makeStream({ r_frame_rate:'25/1', height:720 })], ANALYZER_INPUTS);
|
[makeStream({ r_frame_rate:'25/1', height:720 })], ANALYZER_INPUTS);
|
||||||
const o = await analyzerPlugin(a);
|
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.isMovie, 'false', '/Sonarr -> isMovie=false');
|
||||||
eq(o.variables.isAnimation, 'true', 'Naruto -> isAnimation=true');
|
eq(o.variables.isAnimation, 'true', 'Naruto -> isAnimation=true');
|
||||||
eq(o.variables.x265Tune, 'animation', 'anime -> tune=animation');
|
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',
|
{ const a = makeArgs('/media/toshiba/Sonarr/Naruto S01/S01E01.mkv',
|
||||||
[makeStream({ r_frame_rate:'30000/1001', height:480 })], ANALYZER_INPUTS);
|
[makeStream({ r_frame_rate:'30000/1001', height:480 })], ANALYZER_INPUTS);
|
||||||
const o = await analyzerPlugin(a);
|
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');
|
eq(o.variables.vBitrate, '1000k', 'anime SD 480p -> 1000k');
|
||||||
cleanup(a); }
|
cleanup(a); }
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
* File: Radarr path, AC3 ITA 448k 5.1, EAC3 ENG 640k 5.1
|
* File: Radarr path, AC3 ITA 448k 5.1, EAC3 ENG 640k 5.1
|
||||||
*
|
*
|
||||||
* Expected:
|
* Expected:
|
||||||
* gopKeyint = "48"
|
* gopKeyint = "96"
|
||||||
|
* gopMinKeyint = "24"
|
||||||
* isMovie = "true"
|
* isMovie = "true"
|
||||||
* isAnimation = "false"
|
* isAnimation = "false"
|
||||||
* vBitrate = "3000k"
|
* vBitrate = "3000k"
|
||||||
@@ -44,7 +45,9 @@ async function run() {
|
|||||||
const vars = out1.variables;
|
const vars = out1.variables;
|
||||||
|
|
||||||
assertEqual(out1.outputNumber, 1, 'outputNumber');
|
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.isMovie, 'true', 'isMovie (Radarr path)');
|
||||||
assertEqual(vars.isAnimation, 'false', 'isAnimation (no anime match)');
|
assertEqual(vars.isAnimation, 'false', 'isAnimation (no anime match)');
|
||||||
assertEqual(vars.vBitrate, '3000k', 'vBitrate (film default)');
|
assertEqual(vars.vBitrate, '3000k', 'vBitrate (film default)');
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
* Note: DTS detection logic is fully covered in unit tests (quick-logic-test.js).
|
* Note: DTS detection logic is fully covered in unit tests (quick-logic-test.js).
|
||||||
*
|
*
|
||||||
* Expected:
|
* Expected:
|
||||||
* gopKeyint = "50"
|
* gopKeyint = "100"
|
||||||
|
* gopMinKeyint = "25"
|
||||||
* isAnimation = "true"
|
* isAnimation = "true"
|
||||||
* isMovie = "false"
|
* isMovie = "false"
|
||||||
* x265Tune = "animation"
|
* x265Tune = "animation"
|
||||||
@@ -40,7 +41,9 @@ async function run() {
|
|||||||
});
|
});
|
||||||
const out1 = await analyzerPlugin(args1);
|
const out1 = await analyzerPlugin(args1);
|
||||||
const vars = out1.variables;
|
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.isMovie, 'false', 'isMovie=false (Sonarr path)');
|
||||||
assertEqual(vars.isAnimation, 'true', 'isAnimation=true (Naruto in path)');
|
assertEqual(vars.isAnimation, 'true', 'isAnimation=true (Naruto in path)');
|
||||||
assertEqual(vars.x265Tune, 'animation', 'x265Tune=animation');
|
assertEqual(vars.x265Tune, 'animation', 'x265Tune=animation');
|
||||||
|
|||||||
Reference in new issue
Block a user