221 lines
7.4 KiB
JavaScript
221 lines
7.4 KiB
JavaScript
'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);
|
|
if (br > 0) return Math.round(br / 1000);
|
|
|
|
// Check tags for BPS (common in Matroska containers)
|
|
const tags = stream.tags || {};
|
|
const bpsEntry = Object.entries(tags).find(([key]) => {
|
|
const k = String(key).toLowerCase();
|
|
return k === 'bps' || k === 'bps-eng' || k === 'bps-ita' || k.startsWith('bps');
|
|
});
|
|
if (bpsEntry) {
|
|
const bps = parseInt(bpsEntry[1], 10);
|
|
if (bps > 0) return Math.round(bps / 1000);
|
|
}
|
|
|
|
return 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) — strict no upcoding
|
|
return srcKbps > 0 ? Math.min(stereoMax, srcKbps) : stereoMax;
|
|
}
|
|
|
|
// Multichannel target cap
|
|
const maxTarget = isMovie ? filmTarget : serThreshold;
|
|
|
|
// Strict no upcoding: if source bitrate is known, never exceed source bitrate
|
|
if (srcKbps > 0) {
|
|
return Math.min(maxTarget, srcKbps);
|
|
}
|
|
return maxTarget;
|
|
};
|
|
|
|
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';
|
|
const isAnimation = variables.isAnimation === 'true';
|
|
jobLog(`[audioSelectorClamping] isMovie=${isMovie} isAnimation=${isAnimation}\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;
|