diff --git a/fileflows/scripts/03_Encode_AV1_QSV.js b/fileflows/scripts/03_Encode_AV1_QSV.js new file mode 100644 index 0000000..a4da543 --- /dev/null +++ b/fileflows/scripts/03_Encode_AV1_QSV.js @@ -0,0 +1,204 @@ +function Script(CQ, Preset, SizePercent, BufsizeMultiplier, AQStrength, RcLookahead) +{ + CQ = CQ || 24; + Preset = Preset || 'p7'; + SizePercent = SizePercent || 60; + BufsizeMultiplier = BufsizeMultiplier || 2; + AQStrength = AQStrength || 15; + RcLookahead = RcLookahead || 64; + + let SizeFactor = SizePercent / 100; + + // ───────────────────────────────────────────────────────────────────────── + // Variables.vi.VideoInfo ist in dieser FileFlows-Version im ScriptNode LEER + // (verifiziert 2026-07-06). Daher Video-/Audio-Info direkt per ffprobe holen. + // ffprobe liefert echte Kanalzahlen (6/8) -> kein "5.1"-Layout-Umrechnen noetig. + // ───────────────────────────────────────────────────────────────────────── + 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 bitrateBps = v.Bitrate || 0; + if (bitrateBps <= 0 && vi.Bitrate) bitrateBps = Math.round(vi.Bitrate * 0.92); + if (bitrateBps <= 0) { Logger.ELog('Bitrate nicht ermittelbar - Skip'); return 2; } + + let srcMbps = bitrateBps / 1000000; + let maxrateMbps = Math.round(srcMbps * SizeFactor * 10) / 10; + let bufsizeMbps = Math.round(maxrateMbps * BufsizeMultiplier * 10) / 10; + + // HDR / Dolby Vision Detection + let colorTrc = (v.ColorTransfer || '').toLowerCase(); + let colorPrim = (v.ColorPrimaries || '').toLowerCase(); + let colorSpace = (v.ColorSpace || '').toLowerCase(); + let isHDR10 = colorTrc.indexOf('smpte2084') !== -1; + let fileName = ('' + (Variables['file.Orig.FullName'] || Variables['file.FullName'] || '')).toLowerCase(); + let dvByName = (fileName.indexOf('.dv.') !== -1 || fileName.indexOf('dolby.vision') !== -1 + || fileName.indexOf('dolbyvision') !== -1 || fileName.indexOf('.dovi.') !== -1); + let isDV = !!(v.DolbyVision || dvByName); + let dvProfile = v.DolbyVisionProfile || (isDV ? 7 : null); + let forceSDR = (isDV && dvProfile === 5); + + Logger.ILog('Quelle: ' + srcMbps.toFixed(1) + ' Mbit/s'); + Logger.ILog('Cap : maxrate=' + maxrateMbps + ' Mbit/s bufsize=' + bufsizeMbps + ' Mbit/s (<= ' + + SizePercent + '% der Quelle)'); + Logger.ILog('Color : HDR10=' + isHDR10 + ' DV=' + isDV + ' Profile=' + dvProfile + ' forceSDR=' + forceSDR); + + // ── ffmpeg-Argumente + 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]; + + // Video (nur der erste Video-Stream -> AV1; cover/poster werden mit -map 0:v:0 ignoriert) + args.push('-map', '0:v:0', + '-c:v', 'av1_nvenc', + '-pix_fmt', 'p010le', + '-rc', 'vbr', + '-cq', '' + CQ, + '-b:v', '0', + '-preset', Preset, + '-tune', 'hq', + '-multipass', 'fullres', + '-spatial-aq', '1', + '-temporal-aq', '1', + '-aq-strength', '' + AQStrength, + '-rc-lookahead', '' + RcLookahead, + '-maxrate', maxrateMbps + 'M', + '-bufsize', bufsizeMbps + 'M'); + + if (isDV && dvProfile === 7) { + args.push('-bsf:v', 'filter_units=remove_types=62'); + } + + if (forceSDR) { + // DV Profil 5: keine HDR10-Fallback-Metadaten -> ohne Tonemapping Gruen-/Lilastich. + 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 (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); + } + + // ── Audio: EAC3-Kompatspur anhaengen, WENN beste deutsche Spur DTS ist (Denon-HDMI-Sync). + // Sonst (TrueHD/AC3/EAC3 oder keine DE-Spur) alle Audiospuren 1:1 kopieren. + // Channels kommt jetzt als echte Kanalzahl von ffprobe (6/8), kein Umrechnen noetig. + 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; } + function atitle(a) { return '' + (a.Title || a.Name || ''); } + // von UNS frueher erzeugte EAC3-Kompatspur (Titel-Tag) -> beim Neu-Mux verwerfen (idempotent). + function isOldCompat(a) { return acodec(a).indexOf('eac3') !== -1 && atitle(a).indexOf('AVR-kompatibel') !== -1; } + + let audioStreams = (vi.AudioStreams || []); + let excludeIdx = -1; // relativer a-Index unserer alten Kompatspur + let bestGer = null, bestGerAIdx = -1; + for (let i = 0; i < audioStreams.length; i++) { + if (isOldCompat(audioStreams[i])) { excludeIdx = i; continue; } // NICHT als beste DE-Spur werten + if (isGerman(audioStreams[i]) && (!bestGer || channels(audioStreams[i]) > channels(bestGer))) { + bestGer = audioStreams[i]; bestGerAIdx = i; + } + } + + if (audioStreams.length === 0) { + Logger.WLog('Keine Audio-Info verfuegbar -> Audio 1:1 kopieren (kein EAC3)'); + args.push('-map', '0:a', '-c:a', 'copy'); + } else 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); // alte Kompatspur raus + 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'); + } + + // Subtitles + Kapitel 1:1 + args.push('-map', '0:s?', '-c:s', 'copy'); + + args.push('-dn', + '-max_muxing_queue_size', '9999', + '-avoid_negative_ts', 'make_zero'); + + // Output-Datei + 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('AV1 Encode fehlgeschlagen (exitCode=' + (result ? result.exitCode : '?') + ')'); + if (result && result.standardError) Logger.ELog(result.standardError); + return 2; + } + + Flow.SetWorkingFile(outFile); + Logger.ILog('AV1 Encode erfolgreich -> ' + outFile); + return 1; +}