Add VP9 Plugin (#154)
* add my plugin * work out foreign audio stream bugs, remove eslint * eslint pass Co-authored-by: Zach Myers <zachmyers@woosterbrush.com>make-only-subtitle-default
parent
ec5ce34a47
commit
253a0a4d05
@ -0,0 +1,478 @@
|
||||
/* eslint max-classes-per-file: ["error", 2] */
|
||||
function details() {
|
||||
return {
|
||||
id: 'Tdarr_Plugin_VP92_VP9_Match_Bitrate_One_Pass',
|
||||
Stage: 'Pre-processing',
|
||||
Name: 'VP9 Encoding Match Bitrate 1 Pass System',
|
||||
Type: 'Video',
|
||||
Operation: 'Transcode',
|
||||
Description: `Will run through linvpx-vp9 and follow the contrained quality contraints. Will also encode audio to
|
||||
opus using libopus. Allows user-input on the desired constrained quality amount for each video resolution with
|
||||
defaults if none are given.`,
|
||||
Version: '1.00',
|
||||
Link: 'https://github.com/HaveAGitGat/Tdarr_Plugins/blob/master/Community/Tdarr_Plugin_075a_FFMPEG_HEVC_Generic.js',
|
||||
Tags: 'pre-processing,ffmpeg,vp9',
|
||||
Inputs: [
|
||||
{
|
||||
name: 'CQ_240p',
|
||||
tooltip:
|
||||
'The CQ number (recommended 15-35) for this resolution, default 32',
|
||||
},
|
||||
{
|
||||
name: 'CG_360p',
|
||||
tooltip:
|
||||
'The CQ number (recommended 15-35) for this resolution, default 31',
|
||||
},
|
||||
{
|
||||
name: 'CQ_480p',
|
||||
tooltip:
|
||||
'The CQ number (recommended 15-35) for this resolution, default 28',
|
||||
},
|
||||
{
|
||||
name: 'CQ_720p',
|
||||
tooltip:
|
||||
'The CQ number (recommended 15-35) for this resolution, default 27',
|
||||
},
|
||||
{
|
||||
name: 'CQ_1080p',
|
||||
tooltip:
|
||||
'The CQ number (recommended 15-35) for this resolution, default 26',
|
||||
},
|
||||
{
|
||||
name: 'CQ_4KUHD',
|
||||
tooltip:
|
||||
'The CQ number (recommended 15-35) for this resolution, default 15',
|
||||
},
|
||||
{
|
||||
name: 'CQ_8KUHD',
|
||||
tooltip:
|
||||
'The CQ number (recommended 15-35) for this resolution, default 15',
|
||||
},
|
||||
{
|
||||
name: 'audio_language',
|
||||
tooltip: `
|
||||
Specify language tag/s here for the audio tracks you'd like to keep, recommended to keep "und" as this\\n
|
||||
stands for undertermined, some files may not have the language specified. Must follow ISO-639-2 3 letter\\n
|
||||
format. https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes
|
||||
\\nExample:\\n
|
||||
eng
|
||||
\\nExample:\\n
|
||||
eng,und
|
||||
\\nExample:\\n
|
||||
eng,und,jap`,
|
||||
},
|
||||
{
|
||||
name: 'audio_commentary',
|
||||
tooltip: `Specify if audio tracks that contain commentary/description should be removed.
|
||||
\\nExample:\\n
|
||||
true
|
||||
\\nExample:\\n
|
||||
false`,
|
||||
},
|
||||
{
|
||||
name: 'subtitle_language',
|
||||
tooltip: `Specify language tag/s here for the subtitle tracks you'd like to keep. Must follow ISO-639-2 3 \\n
|
||||
letter format. https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes
|
||||
\\nExample:\\n
|
||||
eng
|
||||
\\nExample:\\n
|
||||
eng,jap`,
|
||||
},
|
||||
{
|
||||
name: 'subtitle_commentary',
|
||||
tooltip: `Specify if subtitle tracks that contain commentary/description should be removed.
|
||||
\\nExample:\\n
|
||||
true
|
||||
\\nExample:\\n
|
||||
false`,
|
||||
},
|
||||
{
|
||||
name: 'remove_mjpeg',
|
||||
tooltip: `Specify if mjpeg codecs should be removed.
|
||||
\\nExample:\\n
|
||||
true
|
||||
\\nExample:\\n
|
||||
false`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// #region Helper Classes/Modules
|
||||
|
||||
/**
|
||||
* Handles logging in a standardised way.
|
||||
*/
|
||||
class Log {
|
||||
constructor() {
|
||||
this.entries = [];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {String} entry the log entry string
|
||||
*/
|
||||
Add(entry) {
|
||||
this.entries.push(entry);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {String} entry the log entry string
|
||||
*/
|
||||
AddSuccess(entry) {
|
||||
this.entries.push(`☑ ${entry}`);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {String} entry the log entry string
|
||||
*/
|
||||
AddError(entry) {
|
||||
this.entries.push(`☒ ${entry}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the log lines separated by new line delimiter.
|
||||
*/
|
||||
GetLogData() {
|
||||
return this.entries.join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the storage of FFmpeg configuration.
|
||||
*/
|
||||
class Configurator {
|
||||
constructor(defaultOutputSettings = null) {
|
||||
this.shouldProcess = false;
|
||||
this.outputSettings = defaultOutputSettings || [];
|
||||
this.inputSettings = [];
|
||||
}
|
||||
|
||||
AddInputSetting(configuration) {
|
||||
this.inputSettings.push(configuration);
|
||||
}
|
||||
|
||||
AddOutputSetting(configuration) {
|
||||
this.shouldProcess = true;
|
||||
this.outputSettings.push(configuration);
|
||||
}
|
||||
|
||||
ResetOutputSetting(configuration) {
|
||||
this.shouldProcess = false;
|
||||
this.outputSettings = configuration;
|
||||
}
|
||||
|
||||
RemoveOutputSetting(configuration) {
|
||||
const index = this.outputSettings.indexOf(configuration);
|
||||
|
||||
if (index === -1) return;
|
||||
this.outputSettings.splice(index, 1);
|
||||
}
|
||||
|
||||
RemoveAllConfigurationsBySearchString(search_string) {
|
||||
for (let i = this.outputSettings.length - 1; i >= 0; i -= 1) {
|
||||
if (this.outputSettings[i].includes(search_string)) {
|
||||
this.outputSettings.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GetOutputSettings() {
|
||||
return this.outputSettings.join(' ');
|
||||
}
|
||||
|
||||
GetInputSettings() {
|
||||
return this.inputSettings.join(' ');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loops over the file streams and executes the given method on
|
||||
* each stream when the matching codec_type is found.
|
||||
* @param {Object} file the file.
|
||||
* @param {string} type the typeo of stream.
|
||||
* @param {function} method the method to call.
|
||||
*/
|
||||
function loopOverStreamsOfType(file, type, method) {
|
||||
let id = 0;
|
||||
for (let i = 0; i < file.ffProbeData.streams.length; i += 1) {
|
||||
if (file.ffProbeData.streams[i].codec_type.toLowerCase() === type) {
|
||||
method(file.ffProbeData.streams[i], id);
|
||||
id += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildAudioConfiguration(inputs, file, logger) {
|
||||
const configuration = new Configurator(['-c:a copy']);
|
||||
let stream_count = 0;
|
||||
let streams_removing = 0;
|
||||
const languages = inputs.audio_language.split(',');
|
||||
let opusFormat = false;
|
||||
let mappingFamily = false;
|
||||
|
||||
loopOverStreamsOfType(file, 'audio', (stream, id) => {
|
||||
stream_count += 1;
|
||||
|
||||
if (stream.codec_name !== 'opus' && !opusFormat) {
|
||||
logger.AddError('Audio is not in proper codec, will format');
|
||||
configuration.RemoveOutputSetting('-c:a copy');
|
||||
configuration.AddOutputSetting('-c:a libopus');
|
||||
opusFormat = true;
|
||||
}
|
||||
|
||||
if (
|
||||
(stream.channel_layout === '5.1(side)' || (stream.codec_name === 'eac3' && stream.channels === 6)) && opusFormat
|
||||
) {
|
||||
logger.AddSuccess(
|
||||
`Determined audio to be ${stream.channel_layout}, adding mapping configuration for proper conversion`,
|
||||
);
|
||||
configuration.AddOutputSetting(
|
||||
`-filter_complex "[0:a:${id}]channelmap=channel_layout=5.1"`,
|
||||
);
|
||||
if (!mappingFamily) {
|
||||
configuration.AddOutputSetting('-mapping_family 1');
|
||||
mappingFamily = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (stream.channel_layout === '6.1(back)' && opusFormat) {
|
||||
logger.AddSuccess(
|
||||
`Determined audio to be ${stream.channel_layout}, adding mapping configuration for proper conversion`,
|
||||
);
|
||||
configuration.AddOutputSetting(
|
||||
`-filter_complex "[0:a:${id}]channelmap=channel_layout=6.1"`,
|
||||
);
|
||||
if (!mappingFamily) {
|
||||
configuration.AddOutputSetting('-mapping_family 1');
|
||||
mappingFamily = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
'tags' in stream && 'title' in stream.tags && inputs.audio_commentary.toLowerCase() === 'true'
|
||||
) {
|
||||
if (
|
||||
stream.tags.title.toLowerCase().includes('commentary')
|
||||
|| stream.tags.title.toLowerCase().includes('description')
|
||||
|| stream.tags.title.toLowerCase().includes('sdh')
|
||||
) {
|
||||
streams_removing += 1;
|
||||
configuration.AddOutputSetting(`-map -0:a:${id}`);
|
||||
logger.AddError(
|
||||
`Removing Commentary or Description audio track: ${stream.tags.title}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ('tags' in stream) {
|
||||
// Remove unwanted languages
|
||||
if ('language' in stream.tags) {
|
||||
if (languages.indexOf(stream.tags.language.toLowerCase()) === -1) {
|
||||
configuration.AddOutputSetting(`-map -0:a:${id}`);
|
||||
streams_removing += 1;
|
||||
logger.AddError(
|
||||
`Removing audio track in language ${stream.tags.language}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (stream_count === streams_removing) {
|
||||
logger.AddError(
|
||||
'*** All audio tracks would have been removed, removing all delete entries',
|
||||
);
|
||||
configuration.RemoveAllConfigurationsBySearchString('-map -0');
|
||||
}
|
||||
|
||||
if (!configuration.shouldProcess) {
|
||||
logger.AddSuccess('No audio processing necessary');
|
||||
}
|
||||
|
||||
return configuration;
|
||||
}
|
||||
|
||||
function buildVideoConfiguration(inputs, file, logger) {
|
||||
const configuration = new Configurator(['-map 0', '-map -0:d', '-c:v copy']);
|
||||
|
||||
loopOverStreamsOfType(file, 'video', (stream, id) => {
|
||||
if (stream.codec_name === 'mjpeg') {
|
||||
if (inputs.remove_mjpeg.toLowerCase() === 'true') {
|
||||
logger.AddError('Removing mjpeg');
|
||||
configuration.AddOutputSetting(`-map -0:v:${id}`);
|
||||
} else {
|
||||
configuration.AddOutputSetting(`-map -v:${id}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (stream.codec_name === 'vp9' && file.container === 'webm') {
|
||||
logger.AddSuccess('File is in proper video format');
|
||||
return;
|
||||
}
|
||||
|
||||
if (stream.codec_name === 'vp9' && file.container !== 'webm') {
|
||||
configuration.AddOutputSetting('-c:v copy');
|
||||
logger.AddError(
|
||||
'File is in proper codec but not write container. Will remux',
|
||||
);
|
||||
}
|
||||
|
||||
let speed = 1;
|
||||
let targetQuality = 32;
|
||||
let tileColumns = 0;
|
||||
const threadCount = 64;
|
||||
if (file.video_resolution === '240p') {
|
||||
targetQuality = inputs.CQ_240p || 32;
|
||||
tileColumns = 0;
|
||||
speed = 1;
|
||||
} else if (file.video_resolution === '360p' || file.video_resolution === '576p') {
|
||||
targetQuality = inputs.CQ_360p || 31;
|
||||
tileColumns = 1;
|
||||
speed = 1;
|
||||
} else if (file.video_resolution === '480p') {
|
||||
targetQuality = inputs.CQ_480p || 28;
|
||||
tileColumns = 1;
|
||||
speed = 1;
|
||||
} else if (file.video_resolution === '720p') {
|
||||
targetQuality = inputs.CQ_720p || 27;
|
||||
tileColumns = 2;
|
||||
speed = 2;
|
||||
} else if (file.video_resolution === '1080p') {
|
||||
targetQuality = inputs.CQ_1080p || 26;
|
||||
tileColumns = 2;
|
||||
speed = 2;
|
||||
} else if (
|
||||
file.video_resolution === '1440p' || file.video_resolution === '2560p' || file.video_resolution === '4KUHD'
|
||||
) {
|
||||
targetQuality = inputs.CQ_4KUHD || 15;
|
||||
tileColumns = 3;
|
||||
speed = 2;
|
||||
} else if (file.video_resolution === '8KUHD') {
|
||||
targetQuality = inputs.CQ_8KUHD || 15;
|
||||
tileColumns = 3;
|
||||
speed = 2;
|
||||
}
|
||||
|
||||
configuration.RemoveOutputSetting('-c:v copy');
|
||||
configuration.AddOutputSetting(
|
||||
`-pix_fmt yuv420p10le -c:v libvpx-vp9 -b:v 0 -crf ${targetQuality} -threads ${threadCount} -speed ${speed}
|
||||
-quality good -static-thresh 0 -tile-columns ${tileColumns} -tile-rows 0 -frame-parallel 0 -row-mt 1
|
||||
-aq-mode 0 -g 240`,
|
||||
);
|
||||
|
||||
logger.AddError('Transcoding file to VP9');
|
||||
});
|
||||
|
||||
if (!configuration.shouldProcess) {
|
||||
logger.AddSuccess('No video processing necessary');
|
||||
}
|
||||
|
||||
return configuration;
|
||||
}
|
||||
|
||||
function buildSubtitleConfiguration(inputs, file, logger) {
|
||||
const configuration = new Configurator(['-c:s copy']);
|
||||
// webvtt
|
||||
|
||||
const languages = inputs.subtitle_language.split(',');
|
||||
let webvttFormat = false;
|
||||
// if (languages.length === 0) return configuration;
|
||||
|
||||
loopOverStreamsOfType(file, 'subtitle', (stream, id) => {
|
||||
if (
|
||||
stream.codec_name === 'hdmv_pgs_subtitle'
|
||||
|| stream.codec_name === 'eia_608'
|
||||
|| stream.codec_name === 'dvd_subtitle'
|
||||
) {
|
||||
logger.AddError(
|
||||
`Removing subtitle in invalid codec ${stream.codec_name}`,
|
||||
);
|
||||
configuration.AddOutputSetting(`-map -0:s:${id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if ('tags' in stream) {
|
||||
// Remove unwated languages
|
||||
if ('language' in stream.tags) {
|
||||
if (languages.indexOf(stream.tags.language.toLowerCase()) === -1) {
|
||||
configuration.AddOutputSetting(`-map -0:s:${id}`);
|
||||
logger.AddError(
|
||||
`Removing subtitle in language ${stream.tags.language}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove commentary subtitles
|
||||
if (
|
||||
'title' in stream.tags
|
||||
&& inputs.subtitle_commentary.toLowerCase() === 'true'
|
||||
) {
|
||||
if (
|
||||
stream.tags.title.toLowerCase().includes('commentary')
|
||||
|| stream.tags.title.toLowerCase().includes('description')
|
||||
|| stream.tags.title.toLowerCase().includes('sdh')
|
||||
) {
|
||||
configuration.AddOutputSetting(`-map -0:s:${id}`);
|
||||
logger.AddError(
|
||||
`Removing Commentary or Description subtitle: ${stream.tags.title}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (stream.codec_name !== 'webvtt' && !webvttFormat) {
|
||||
logger.AddError('Formatting subtitles to webvtt format');
|
||||
configuration.RemoveOutputSetting('-c:s copy');
|
||||
configuration.AddOutputSetting('-c:s webvtt');
|
||||
webvttFormat = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!configuration.shouldProcess) {
|
||||
logger.AddSuccess('No subtitle processing necessary');
|
||||
}
|
||||
|
||||
return configuration;
|
||||
}
|
||||
|
||||
function plugin(file, librarySettings, inputs) {
|
||||
// Must return this object
|
||||
const response = {
|
||||
container: '.webm',
|
||||
FFmpegMode: true,
|
||||
handBrakeMode: false,
|
||||
infoLog: '',
|
||||
processFile: false,
|
||||
preset: '',
|
||||
reQueueAfter: true,
|
||||
};
|
||||
|
||||
const logger = new Log();
|
||||
|
||||
const audioSettings = buildAudioConfiguration(inputs, file, logger);
|
||||
const videoSettings = buildVideoConfiguration(inputs, file, logger);
|
||||
const subtitleSettings = buildSubtitleConfiguration(inputs, file, logger);
|
||||
|
||||
response.processFile = audioSettings.shouldProcess
|
||||
|| videoSettings.shouldProcess
|
||||
|| subtitleSettings.shouldProcess;
|
||||
|
||||
if (!response.processFile) {
|
||||
logger.AddSuccess('No need to process file');
|
||||
}
|
||||
|
||||
response.preset = `${videoSettings.GetInputSettings()},${videoSettings.GetOutputSettings()}
|
||||
${audioSettings.GetOutputSettings()} ${subtitleSettings.GetOutputSettings()}`;
|
||||
response.infoLog += logger.GetLogData();
|
||||
return response;
|
||||
}
|
||||
|
||||
module.exports.details = details;
|
||||
module.exports.plugin = plugin;
|
||||
Loading…
Reference in new issue