/** * @author Sascha (Port von Tdarr_Plugin_Custom_MKV_H265_DE_v400) * @description HEVC NVENC VBR CQ fuer 1080p Material mit deutschen Audiospuren. * Slot 0 = EAC3-Kompat bei DTS, sonst copy. Alle Untertitel bleiben erhalten. * CUDA HW-Decode. HDR/DV-aware. Idempotent (kein Re-Re-Encode). * GROESSEN-SICHERHEITSNETZ: Output <= Original + 5% Toleranz. * @revision 3 * @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 * @param {int} RcLookahead NVENC rc-lookahead Frames (Default 32) */ function Script(TargetBitrateMbit, MaxrateMbit, SkipThresholdMbit, Preset, CQ, RcLookahead) { TargetBitrateMbit = TargetBitrateMbit || 5; MaxrateMbit = MaxrateMbit || 8; SkipThresholdMbit = SkipThresholdMbit || 5; Preset = Preset || 'p7'; CQ = CQ || 24; RcLookahead = RcLookahead || 32; 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', '' + RcLookahead, '-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; } // ── GROESSEN-SICHERHEITSNETZ ────────────────────────────────── // Output darf nicht wesentlich groesser sein als die Quelle. // Toleranz: 5% (fuer Audio-Stream-Unterschiede / Metadaten). // Bei Ueberschreitung: Encode verwerfen, Original behalten, Warnung. // Ausnahme: copyVideo (nur Audio-Remux) kann durch EAC3 etwas wachsen. if (!copyVideo) { let origSizeStat = Flow.Execute({ command: '/usr/bin/stat', argumentList: ['-c', '%s', inputFile] }); let origSize = parseInt((origSizeStat && origSizeStat.standardOutput) ? origSizeStat.standardOutput.trim() : '0', 10) || 0; let newSizeStat = Flow.Execute({ command: '/usr/bin/stat', argumentList: ['-c', '%s', outFile] }); let newSize = parseInt((newSizeStat && newSizeStat.standardOutput) ? newSizeStat.standardOutput.trim() : '0', 10) || 0; if (origSize > 0 && newSize > 0) { let growthPct = ((newSize - origSize) / origSize) * 100; Logger.ILog('Groesse: orig=' + origSize + ' bytes, output=' + newSize + ' bytes, growth=' + growthPct.toFixed(1) + '%'); if (growthPct > 5) { Logger.WLog('GROESSEN-WARNUNG: Output ' + growthPct.toFixed(1) + '% groesser als Quelle -> Encode verworfen, Original bleibt.'); Flow.Execute({ command: '/bin/rm', argumentList: ['-f', outFile] }); return 2; } } } Flow.SetWorkingFile(outFile); Logger.ILog('HEVC Encode erfolgreich -> ' + outFile); return 1; }