tdarr/fileflows/scripts/01_Detect_4K_Remux.js

127 lines
6.9 KiB
JavaScript

/**
* @name 01_Detect_4K_Remux
* @description Erkennt 4K-HEVC-Kandidaten fuer AV1: Remux-Dateien oder Quellen mit mehr als 35 Mbit/s.
* @author Sascha (Port von Tdarr_Plugin_AV1_BestQuality_v100)
* @revision 2
* @output Ja - 4K HEVC -> AV1 Encode
* @output Nein - kein 4K AV1-Kandidat
* @param {int} MinWidth Minimale Breite in Pixel, ab der eine Datei als 4K gilt
* @param {int} MinSourceMbps Minimale Quellenbitrate fuer klassische Remux-Kandidaten
*/
function Script(MinWidth, MinSourceMbps)
{
MinWidth = MinWidth || 3000;
MinSourceMbps = MinSourceMbps || 15;
// Titel sind unzuverlaessig: WEB-DL/UHD-BluRay-Releases tragen oft kein "Remux".
// Deshalb ist jede 4K-HEVC-Quelle oberhalb dieser VIDEO-Bitrate ebenfalls Kandidat.
const HighBitrateMbps = 35;
// ─────────────────────────────────────────────────────────────────────────
// WICHTIG (verifiziert 2026-07-06): Variables.vi.VideoInfo wird in dieser
// FileFlows-Version im ScriptNode NICHT befuellt (0/7 Laeufe hatten vi-Daten,
// die "loadVi"-Warteschleife war wirkungslos). Der native VideoInfo-Zugriff
// laeuft nur ueber den FFmpeg-Builder-Node, nicht im Script.
// -> Verlaesslicher Weg: ffprobe direkt aufrufen und JSON parsen.
// Liefert echte Kanalzahlen (6/8) statt Layout-Strings ("5.1").
// ─────────────────────────────────────────────────────────────────────────
function probeInfo() {
var probe = null;
try { probe = Flow.GetToolPath('ffprobe'); } catch (e) {}
if (!probe) probe = Variables['ffprobe'] || 'ffprobe';
var input = Flow.WorkingFile;
var res = Flow.Execute({
command: probe,
argumentList: ['-v', 'quiet', '-print_format', 'json',
'-show_format', '-show_streams', input]
});
if (!res || res.exitCode !== 0) { Logger.WLog('ffprobe fehlgeschlagen (exit=' + (res ? res.exitCode : '?') + ')'); return null; }
var raw = res.standardOutput || res.output || '';
var j;
try { j = JSON.parse(raw); } catch (e) { Logger.WLog('ffprobe JSON parse-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') {
// cover/poster (attached_pic, mjpeg, png) ignorieren
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,
// P7 hat HDR10-Base-Layer (smpte2084), P5 ist ICtCp (kein smpte2084)
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), Bitrate: tagBps(s) || 0
});
}
}
return { Bitrate: fmtBr, VideoStreams: vids, AudioStreams: auds };
}
let vi = probeInfo();
let v = (vi && vi.VideoStreams && vi.VideoStreams.length > 0) ? vi.VideoStreams[0] : null;
if (!v) {
Logger.ELog('Kein Video-Stream ermittelbar (ffprobe) - kann 4K-Remux nicht bewerten');
return 2;
}
let width = v.Width || 0;
let codec = (v.Codec || '').toLowerCase();
let bitrateBps = v.Bitrate || 0;
// Video-Bitrate fehlt (kein BPS-Tag) -> aus Container-Gesamtbitrate schaetzen (~92%)
if (bitrateBps <= 0 && vi.Bitrate) bitrateBps = Math.round(vi.Bitrate * 0.92);
let bitrateMbps = bitrateBps > 0 ? (bitrateBps / 1000000) : 0;
// Dateiname aus den flachen Punkt-Keys (Variables.file als Objekt existiert NICHT in dieser Version)
let fileName = Variables['file.Orig.FullName'] || Variables['file.FullName']
|| Variables['file.Name'] || '';
if (!fileName) { try { fileName = Flow.WorkingFile || ''; } catch (e) {} }
let is4K = width >= MinWidth;
let isRemux = /remux/i.test(fileName);
let isHEVC = (codec === 'hevc' || codec === 'h265');
let isAV1 = (codec === 'av1');
// Bei unbekannter Bitrate klassische Remuxe weiterhin zulassen; Nicht-Remuxe
// brauchen dagegen einen belastbaren Messwert oberhalb von 35 Mbit/s.
let brKnown = bitrateMbps > 0;
let highBR = brKnown ? (bitrateMbps >= MinSourceMbps) : true;
let overHighBitrate = brKnown && bitrateMbps > HighBitrateMbps;
let sourceCandidate = isRemux || overHighBitrate;
Logger.ILog('Datei: ' + fileName);
Logger.ILog('Video: codec=' + codec + ' width=' + width + ' bitrate=' +
(brKnown ? bitrateMbps.toFixed(1) + ' Mbit/s' : 'unbekannt'));
Logger.ILog('Checks: 4K=' + is4K + ' Remux=' + isRemux + ' HEVC=' + isHEVC +
' >=' + MinSourceMbps + 'Mbit=' + highBR + ' >' + HighBitrateMbps +
'Mbit=' + overHighBitrate + (brKnown ? '' : ' (Bitrate unbekannt)'));
if (isAV1) { Logger.ILog('Bereits AV1 - kein Re-Encode.'); return 2; }
if (!is4K) { Logger.ILog('Nicht 4K (<' + MinWidth + 'px).'); return 2; }
if (!isHEVC) { Logger.ILog('Codec ist nicht HEVC/H265.'); return 2; }
if (!sourceCandidate) { Logger.ILog('Weder Remux noch Video-Bitrate > ' + HighBitrateMbps + ' Mbit/s.'); return 2; }
if (isRemux && !highBR) { Logger.ILog('Remux-Bitrate < ' + MinSourceMbps + ' Mbit/s - lohnt nicht.'); return 2; }
Logger.ILog('-> 4K HEVC Kandidat fuer AV1 Encode (' +
(isRemux ? 'Remux' : 'hohe Bitrate ' + bitrateMbps.toFixed(1) + ' Mbit/s') + ')');
return 1;
}