'use strict'; /* eslint-disable no-param-reassign */ /** * audioSelectorClamping * Selects the best ITA and ENG audio tracks using codec+bitrate scoring, * then calculates clamped Opus target bitrates. * * Scoring: score = (codecWeight * 10000) + bitrateKbps * Weights: TrueHD=100, DTS-HD MA=95, FLAC=90, EAC3=75, DTS=60, * AC3=45, AAC=35, Opus=30 * * Clamping: * Multichannel (ch > 2): * Film: min(filmMultiChannelBitrate, sourceBitrateKbps) * Series: sourceBitrateKbps >= threshold ? 384 : 256 * Stereo (ch <= 2): * min(stereoBitrateMax, sourceBitrateKbps) [no upcoding] */ const CODEC_WEIGHTS = { truehd: 100, flac: 90, eac3: 75, dts: 60, ac3: 45, aac: 35, opus: 30, mp3: 20, vorbis: 20, }; const getCodecWeight = (stream) => { const codec = (stream.codec_name || '').toLowerCase(); const profile = (stream.profile || '').toLowerCase(); // DTS-HD MA detection via profile field if (codec === 'dts' && (profile.includes('ma') || profile.includes('dts-hd'))) return 95; return CODEC_WEIGHTS[codec] || 5; }; const getSourceBitrateKbps = (stream) => { const br = parseInt(stream.bit_rate || '0', 10); return br > 0 ? Math.round(br / 1000) : 0; }; const calcOpusBitrate = (stream, isMovie, inputs) => { const srcKbps = getSourceBitrateKbps(stream); const ch = parseInt(stream.channels || '2', 10); const stereoMax = parseInt(inputs.stereoBitrateMax || '128', 10); const filmTarget = parseInt(inputs.filmMultiChannelBitrate || '640', 10); const serThreshold = parseInt(inputs.seriesMultiChannelThreshold || '384', 10); if (ch <= 2) { // Stereo: min(max, source) — no upcoding return srcKbps > 0 ? Math.min(stereoMax, srcKbps) : stereoMax; } // Multichannel if (isMovie) { return srcKbps > 0 ? Math.min(filmTarget, srcKbps) : filmTarget; } // Series // If bitrate is unknown (0), assume high-quality source -> use 384k const effectiveKbps = srcKbps > 0 ? srcKbps : serThreshold; return effectiveKbps >= serThreshold ? 384 : 256; }; const details = () => ({ name: 'Audio Selector & Opus Bitrate Clamping', description: 'Selects the best ITA and ENG audio tracks using weighted codec+bitrate scoring. ' + 'Calculates clamped Opus target bitrates. ' + 'Output 1: tracks found. Output 2: no ITA or ENG tracks found.', style: { borderColor: '#4CAF50' }, tags: 'audio', isStartPlugin: false, pType: '', requiresVersion: '2.11.01', sidebarPosition: 2, icon: 'faVolumeUp', inputs: [ { label: 'Film Multichannel Bitrate (kbps)', name: 'filmMultiChannelBitrate', type: 'string', defaultValue: '640', inputUI: { type: 'text' }, tooltip: 'Max Opus bitrate in kbps for multichannel (>2ch) Film audio. Source bitrate is used if lower.', }, { label: 'Series Multichannel Threshold (kbps)', name: 'seriesMultiChannelThreshold', type: 'string', defaultValue: '384', inputUI: { type: 'text' }, tooltip: 'For series: if source bitrate >= this value, Opus target is 384k, otherwise 256k.', }, { label: 'Stereo Max Bitrate (kbps)', name: 'stereoBitrateMax', type: 'string', defaultValue: '128', inputUI: { type: 'text' }, tooltip: 'Max Opus bitrate for stereo tracks. Source bitrate is used if lower (no upcoding).', }, ], outputs: [ { number: 1, tooltip: 'ITA or ENG track found — continue pipeline' }, { number: 2, tooltip: 'No ITA or ENG tracks found — handle separately' }, ], }); const plugin = async (args) => { const { jobLog, inputFileObj, variables } = args; const filePath = inputFileObj._id; jobLog(`[audioSelectorClamping] Processing: ${filePath}\n`); const isMovie = variables.isMovie === 'true'; jobLog(`[audioSelectorClamping] isMovie=${isMovie}\n`); const streams = (inputFileObj.ffProbeData && inputFileObj.ffProbeData.streams) || []; const audioStreams = streams.filter((s) => s.codec_type === 'audio'); jobLog(`[audioSelectorClamping] Found ${audioStreams.length} audio stream(s).\n`); let bestIta = null; let bestEng = null; let bestItaScore = -1; let bestEngScore = -1; audioStreams.forEach((stream) => { const lang = ((stream.tags && stream.tags.language) || '').toLowerCase(); if (lang !== 'ita' && lang !== 'eng' && lang !== 'it' && lang !== 'en') return; const normLang = (lang === 'it') ? 'ita' : (lang === 'en') ? 'eng' : lang; const weight = getCodecWeight(stream); const srcKbps = getSourceBitrateKbps(stream); const score = weight * 10000 + srcKbps; jobLog( `[audioSelectorClamping] Stream #${stream.index} lang=${normLang} ` + `codec=${stream.codec_name} profile="${stream.profile || ''}" ` + `src=${srcKbps}kbps ch=${stream.channels} score=${score}\n`, ); if (normLang === 'ita' && score > bestItaScore) { bestItaScore = score; bestIta = { stream, normLang }; } if (normLang === 'eng' && score > bestEngScore) { bestEngScore = score; bestEng = { stream, normLang }; } }); if (!bestIta && !bestEng) { jobLog('[audioSelectorClamping] WARNING: No ITA or ENG tracks found. Routing to output 2.\n'); variables.audioHasIta = 'false'; variables.audioHasEng = 'false'; return { outputNumber: 2, outputFileObj: inputFileObj, variables }; } // Export ITA if (bestIta) { const { stream } = bestIta; const opusBitrate = calcOpusBitrate(stream, isMovie, args.inputs); variables.audioHasIta = 'true'; variables.audioItaStreamIndex = String(stream.index); variables.audioItaOpusBitrate = String(opusBitrate); variables.audioItaChannels = String(stream.channels || '2'); variables.audioItaCodecName = (stream.codec_name || '').toLowerCase(); jobLog( `[audioSelectorClamping] ITA selected: #${stream.index} ${stream.codec_name} ` + `${getSourceBitrateKbps(stream)}kbps -> Opus ${opusBitrate}kbps\n`, ); } else { variables.audioHasIta = 'false'; jobLog('[audioSelectorClamping] No ITA track found.\n'); } // Export ENG if (bestEng) { const { stream } = bestEng; const opusBitrate = calcOpusBitrate(stream, isMovie, args.inputs); variables.audioHasEng = 'true'; variables.audioEngStreamIndex = String(stream.index); variables.audioEngOpusBitrate = String(opusBitrate); variables.audioEngChannels = String(stream.channels || '2'); variables.audioEngCodecName = (stream.codec_name || '').toLowerCase(); jobLog( `[audioSelectorClamping] ENG selected: #${stream.index} ${stream.codec_name} ` + `${getSourceBitrateKbps(stream)}kbps -> Opus ${opusBitrate}kbps\n`, ); } else { variables.audioHasEng = 'false'; jobLog('[audioSelectorClamping] No ENG track found.\n'); } jobLog('[audioSelectorClamping] Done.\n'); return { outputNumber: 1, outputFileObj: inputFileObj, variables }; }; module.exports.details = details; module.exports.plugin = plugin;