chore: add 04_Encode_HEVC_QSV.js from live FileFlows export
This commit is contained in:
parent
ba6b18d600
commit
58f72719e1
1 changed files with 225 additions and 0 deletions
225
fileflows/scripts/04_Encode_HEVC_QSV.js
Normal file
225
fileflows/scripts/04_Encode_HEVC_QSV.js
Normal file
|
|
@ -0,0 +1,225 @@
|
||||||
|
/**
|
||||||
|
* @author Sascha (Port von Tdarr_Plugin_Custom_MKV_H265_DE_v400)
|
||||||
|
* @description HEVC NVENC VBR CQ fuer 1080p Material mit deutschen Audiospuren.
|
||||||
|
* Slot 0 = AAC 2.0 192k DE (DEFAULT), Slot 1+ = beste DE copy, dann
|
||||||
|
* weitere DE, dann alle EN. Alle Untertitel bleiben erhalten.
|
||||||
|
* CUDA HW-Decode. HDR/DV-aware. Idempotent (kein Re-Re-Encode).
|
||||||
|
* @revision 2
|
||||||
|
* @uid 4f1c2a90-1111-4b00-8a01-0000000000a4
|
||||||
|
* @output Encode erfolgreich
|
||||||
|
* @output Skip / Fehler
|
||||||
|
* @param {int} TargetBitrateMbit Ziel-Bitrate in Mbit/s (b:v)
|
||||||
|
* @param {int} MaxrateMbit Maximale Bitrate in Mbit/s
|
||||||
|
* @param {int} SkipThresholdMbit Wenn Quelle <= dieser Wert (Mbit/s): HEVC copy, sonst encode
|
||||||
|
* @param {string} Preset NVENC Preset p1..p7
|
||||||
|
* @param {int} CQ Constant Quality
|
||||||
|
*/
|
||||||
|
function Script(TargetBitrateMbit, MaxrateMbit, SkipThresholdMbit, Preset, CQ)
|
||||||
|
{
|
||||||
|
TargetBitrateMbit = TargetBitrateMbit || 5;
|
||||||
|
MaxrateMbit = MaxrateMbit || 8;
|
||||||
|
SkipThresholdMbit = SkipThresholdMbit || 5;
|
||||||
|
Preset = Preset || 'p7';
|
||||||
|
CQ = CQ || 24;
|
||||||
|
|
||||||
|
let TARGET = TargetBitrateMbit * 1000000;
|
||||||
|
let MAXRATE = MaxrateMbit * 1000000;
|
||||||
|
let SKIP = SkipThresholdMbit * 1000000;
|
||||||
|
let BUFSIZE = MAXRATE * 2;
|
||||||
|
|
||||||
|
// Variables.vi.VideoInfo ist im ScriptNode leer (verifiziert 2026-07-06) -> ffprobe direkt.
|
||||||
|
function probeInfo() {
|
||||||
|
var probe = null;
|
||||||
|
try { probe = Flow.GetToolPath('ffprobe'); } catch (e) {}
|
||||||
|
if (!probe) probe = Variables['ffprobe'] || 'ffprobe';
|
||||||
|
var res = Flow.Execute({
|
||||||
|
command: probe,
|
||||||
|
argumentList: ['-v', 'quiet', '-print_format', 'json',
|
||||||
|
'-show_format', '-show_streams', Flow.WorkingFile]
|
||||||
|
});
|
||||||
|
if (!res || res.exitCode !== 0) { Logger.WLog('ffprobe fehlgeschlagen'); return null; }
|
||||||
|
var j;
|
||||||
|
try { j = JSON.parse(res.standardOutput || res.output || ''); } catch (e) { Logger.WLog('ffprobe JSON-Fehler: ' + e); return null; }
|
||||||
|
var streams = j.streams || [];
|
||||||
|
var fmtBr = parseInt((j.format && j.format.bit_rate) || 0, 10) || 0;
|
||||||
|
function tagBps(s) { var t = s.tags || {}; return parseInt(t.BPS || t['BPS-eng'] || t.bps || 0, 10) || 0; }
|
||||||
|
var vids = [], auds = [];
|
||||||
|
for (var i = 0; i < streams.length; i++) {
|
||||||
|
var s = streams[i], disp = s.disposition || {};
|
||||||
|
if (s.codec_type === 'video') {
|
||||||
|
if (disp.attached_pic === 1 || s.codec_name === 'mjpeg' || s.codec_name === 'png') continue;
|
||||||
|
var dovi = (s.side_data_list || []).some(function (x) {
|
||||||
|
var t = ('' + (x.side_data_type || '')).toLowerCase();
|
||||||
|
return t.indexOf('dovi') !== -1 || t.indexOf('dolby vision') !== -1;
|
||||||
|
});
|
||||||
|
var trc = ('' + (s.color_transfer || ''));
|
||||||
|
vids.push({
|
||||||
|
Width: s.width || 0, Height: s.height || 0, Codec: (s.codec_name || ''),
|
||||||
|
Bitrate: tagBps(s) || 0, ColorTransfer: trc,
|
||||||
|
ColorPrimaries: s.color_primaries || '', ColorSpace: s.color_space || '',
|
||||||
|
PixelFormat: s.pix_fmt || '', DolbyVision: dovi,
|
||||||
|
DolbyVisionProfile: dovi ? (trc.indexOf('smpte2084') !== -1 ? 7 : 5) : null
|
||||||
|
});
|
||||||
|
} else if (s.codec_type === 'audio') {
|
||||||
|
var t = s.tags || {};
|
||||||
|
auds.push({
|
||||||
|
Language: t.language || '', Codec: (s.codec_name || ''),
|
||||||
|
Channels: s.channels || 0, ChannelLayout: s.channel_layout || '',
|
||||||
|
Title: t.title || '', Profile: s.profile || '', Default: (disp.default === 1)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { Bitrate: fmtBr, VideoStreams: vids, AudioStreams: auds };
|
||||||
|
}
|
||||||
|
|
||||||
|
let vi = probeInfo();
|
||||||
|
if (!vi || !vi.VideoStreams || vi.VideoStreams.length === 0) {
|
||||||
|
Logger.ELog('Kein Video-Stream (ffprobe)'); return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
let v = vi.VideoStreams[0];
|
||||||
|
let codec = (v.Codec || '').toLowerCase();
|
||||||
|
let isHEVC = (codec === 'hevc' || codec === 'h265');
|
||||||
|
let isH264 = (codec === 'h264' || codec === 'avc');
|
||||||
|
let srcBR = v.Bitrate || 0;
|
||||||
|
if (srcBR <= 0 && vi.Bitrate) srcBR = Math.round(vi.Bitrate * 0.92);
|
||||||
|
|
||||||
|
// ─── Audio-Helfer (identisch zum 4K/AV1-Zweig) ───────────────
|
||||||
|
function langOf(a) {
|
||||||
|
return ('' + (a.Language || '')).toLowerCase().trim();
|
||||||
|
}
|
||||||
|
function isGerman(a) { let l = langOf(a); return (l === 'ger' || l === 'deu' || l === 'de'); }
|
||||||
|
function acodec(a) { return ('' + (a.Codec || '')).toLowerCase(); }
|
||||||
|
function isDts(a) { return acodec(a).indexOf('dts') === 0; }
|
||||||
|
function channels(a) { return parseInt(a.Channels, 10) || 0; } // ffprobe: echte Kanalzahl
|
||||||
|
function atitle(a) { return '' + (a.Title || a.Name || ''); }
|
||||||
|
// von UNS frueher erzeugte EAC3-Kompatspur -> beim Neu-Mux verwerfen (idempotent).
|
||||||
|
function isOldCompat(a) { return acodec(a).indexOf('eac3') !== -1 && atitle(a).indexOf('AVR-kompatibel') !== -1; }
|
||||||
|
|
||||||
|
let allAudio = (vi.AudioStreams || []);
|
||||||
|
if (allAudio.filter(isGerman).length === 0) { Logger.WLog('Keine deutsche Audiospur - Skip'); return 2; }
|
||||||
|
|
||||||
|
if (isH264 && srcBR > 0 && srcBR <= SKIP) {
|
||||||
|
Logger.ILog('H264 ' + (srcBR/1000000).toFixed(2) + ' Mbit/s <= ' + SkipThresholdMbit + ' - Re-Encode lohnt nicht');
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
let copyVideo = (isHEVC && srcBR > 0 && srcBR <= SKIP);
|
||||||
|
|
||||||
|
// ─── Video-Args ──────────────────────────────────────────────
|
||||||
|
let ffmpeg = Flow.GetToolPath('ffmpeg');
|
||||||
|
if (!ffmpeg) { Logger.ELog('ffmpeg-Tool nicht gefunden (Flow.GetToolPath)'); return 2; }
|
||||||
|
let inputFile = Flow.WorkingFile;
|
||||||
|
let args = ['-hide_banner', '-y', '-hwaccel', 'cuda', '-i', inputFile];
|
||||||
|
|
||||||
|
args.push('-map', '0:v:0');
|
||||||
|
if (copyVideo) {
|
||||||
|
args.push('-c:v', 'copy');
|
||||||
|
Logger.ILog('Video: HEVC ' + (srcBR/1000000).toFixed(2) + ' Mbit/s -> copy, nur Audio');
|
||||||
|
} else {
|
||||||
|
let colorTrc = ('' + (v.ColorTransfer || '')).toLowerCase();
|
||||||
|
let colorPrim = ('' + (v.ColorPrimaries || '')).toLowerCase();
|
||||||
|
let pixFmt = ('' + (v.PixelFormat || '')).toLowerCase();
|
||||||
|
let isHDR = (colorTrc === 'smpte2084' || colorTrc === 'arib-std-b67' || colorPrim === 'bt2020');
|
||||||
|
let is10bit = (pixFmt.indexOf('10') !== -1 || pixFmt.indexOf('p010') !== -1);
|
||||||
|
let origName2 = '' + (Variables['file.Orig.FullName'] || Variables['file.FullName'] || '');
|
||||||
|
let dvByName = /(\.dv\.|dolby\.?vision|\.dovi\.)/i.test(origName2);
|
||||||
|
let isDV = !!(v.DolbyVision || dvByName);
|
||||||
|
let dvProfile = v.DolbyVisionProfile || (isDV ? 7 : null);
|
||||||
|
let forceSDR = (isDV && dvProfile === 5);
|
||||||
|
|
||||||
|
args.push('-c:v', 'hevc_nvenc',
|
||||||
|
'-rc', 'vbr',
|
||||||
|
'-cq', '' + CQ,
|
||||||
|
'-b:v', '' + TARGET,
|
||||||
|
'-maxrate', '' + MAXRATE,
|
||||||
|
'-bufsize', '' + BUFSIZE,
|
||||||
|
'-multipass', 'fullres',
|
||||||
|
'-preset', Preset,
|
||||||
|
'-spatial_aq', '1',
|
||||||
|
'-temporal_aq', '1',
|
||||||
|
'-rc-lookahead', '32',
|
||||||
|
'-tag:v', 'hvc1');
|
||||||
|
|
||||||
|
if (!forceSDR && (isHDR || is10bit)) {
|
||||||
|
args.push('-pix_fmt', 'p010le');
|
||||||
|
} else {
|
||||||
|
args.push('-pix_fmt', 'yuv420p');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forceSDR) {
|
||||||
|
args.push('-vf', 'tonemapx=tonemap=bt2390:transfer=bt709:matrix=bt709:primaries=bt709:format=yuv420p',
|
||||||
|
'-color_trc', 'bt709', '-color_primaries', 'bt709', '-colorspace', 'bt709');
|
||||||
|
Logger.ILog('DV P5 -> Software-Tonemap nach SDR (bt709)');
|
||||||
|
} else if (isHDR) {
|
||||||
|
if (v.ColorTransfer) args.push('-color_trc', v.ColorTransfer);
|
||||||
|
if (v.ColorPrimaries) args.push('-color_primaries', v.ColorPrimaries);
|
||||||
|
if (v.ColorSpace) args.push('-colorspace', v.ColorSpace);
|
||||||
|
args.push('-color_range', 'tv');
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.ILog('Video: ' + codec + ' ' + (srcBR/1000000).toFixed(2) +
|
||||||
|
' Mbit/s -> HEVC NVENC CQ' + CQ + ' maxrate ' + MaxrateMbit + ' Mbit/s' +
|
||||||
|
(isHDR ? ' (HDR)' : '') + (isDV ? ' (DV P' + dvProfile + ')' : ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Audio-Args (identisch zum 4K/AV1-Zweig) ─────────────────
|
||||||
|
// EAC3-Kompatspur als Default anhaengen, WENN beste deutsche Spur DTS ist
|
||||||
|
// (Denon-HDMI-Sync). Sonst alle Audiospuren 1:1 kopieren.
|
||||||
|
let excludeIdx = -1; // relativer a-Index unserer alten Kompatspur
|
||||||
|
let bestGer = null, bestGerAIdx = -1;
|
||||||
|
for (let i = 0; i < allAudio.length; i++) {
|
||||||
|
if (isOldCompat(allAudio[i])) { excludeIdx = i; continue; } // alte Kompatspur ignorieren
|
||||||
|
if (isGerman(allAudio[i]) && (!bestGer || channels(allAudio[i]) > channels(bestGer))) {
|
||||||
|
bestGer = allAudio[i]; bestGerAIdx = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestGer !== null && isDts(bestGer)) {
|
||||||
|
let ch = channels(bestGer) || 6;
|
||||||
|
let outCh = (ch >= 6) ? 6 : (ch >= 2 ? ch : 2); // EAC3 max 5.1
|
||||||
|
Logger.ILog('Audio: beste DE-Spur ist DTS (' + acodec(bestGer) + ' ' + ch + 'ch, Idx ' +
|
||||||
|
bestGerAIdx + ') -> EAC3 ' + outCh + 'ch als Default davor (Rest 1:1).' +
|
||||||
|
(excludeIdx >= 0 ? ' Alte EAC3-Kompatspur (a:' + excludeIdx + ') verworfen.' : ''));
|
||||||
|
args.push('-map', '0:a:' + bestGerAIdx, '-map', '0:a');
|
||||||
|
if (excludeIdx >= 0) args.push('-map', '-0:a:' + excludeIdx);
|
||||||
|
args.push('-c:a', 'copy', '-c:a:0', 'eac3', '-ac:a:0', '' + outCh,
|
||||||
|
'-b:a:0', (outCh >= 6 ? '640k' : '256k'),
|
||||||
|
'-metadata:s:a:0', 'title=Deutsch (EAC3 AVR-kompatibel)',
|
||||||
|
'-metadata:s:a:0', 'language=ger',
|
||||||
|
'-disposition:a', '0', '-disposition:a:0', 'default');
|
||||||
|
} else {
|
||||||
|
if (bestGer === null) Logger.ILog('Audio: keine DE-Spur -> 1:1 kopieren.');
|
||||||
|
else Logger.ILog('Audio: beste DE-Spur ist ' + acodec(bestGer) + ' (kein DTS) -> 1:1 kopieren.' +
|
||||||
|
(excludeIdx >= 0 ? ' Alte EAC3-Kompatspur (a:' + excludeIdx + ') verworfen.' : ''));
|
||||||
|
args.push('-map', '0:a');
|
||||||
|
if (excludeIdx >= 0) args.push('-map', '-0:a:' + excludeIdx);
|
||||||
|
args.push('-c:a', 'copy');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Untertitel + Rest ───────────────────────────────────────
|
||||||
|
args.push('-map', '0:s?', '-c:s', 'copy');
|
||||||
|
args.push('-dn',
|
||||||
|
'-max_muxing_queue_size', '9999',
|
||||||
|
'-avoid_negative_ts', 'make_zero');
|
||||||
|
|
||||||
|
let outFile = Flow.TempPath + '/' + Flow.NewGuid() + '.mkv';
|
||||||
|
args.push(outFile);
|
||||||
|
|
||||||
|
Logger.ILog('FFmpeg: ' + args.join(' '));
|
||||||
|
|
||||||
|
let result = Flow.Execute({
|
||||||
|
command: ffmpeg,
|
||||||
|
argumentList: args
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result || result.exitCode !== 0) {
|
||||||
|
Logger.ELog('HEVC Encode fehlgeschlagen (exitCode=' + (result ? result.exitCode : '?') + ')');
|
||||||
|
if (result && result.standardError) Logger.ELog(result.standardError);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
Flow.SetWorkingFile(outFile);
|
||||||
|
Logger.ILog('HEVC Encode erfolgreich -> ' + outFile);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue