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
|
||||
* 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}`,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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); }
|
||||
|
||||
|
||||
@@ -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)');
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in new issue
Block a user