You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Tdarr_Plugins/Community/Tdarr_Plugin_JB69_JBHEVCQSV...

802 lines
30 KiB

// allow isNaN
/* eslint no-restricted-globals: 0 */
/* eslint no-template-curly-in-string: 0 */
/* eslint global-require: 0 */
/* eslint eqeqeq: 1 */
/*
/// ///////////////////////////////////////////////////////////////////////////////////////////////////
Author: JarBinks, Zachg99, Jeff47
Date: 03/22/2022
This is my attempt to create an all in one routine that will maintain my library in optimal format
!!!!FOR MY REQUIREMENTS!!!! Chances are very good you will need to make some changes to this routine
and it's partner in order to make it work for you.
Chances are also very good you will need to run linux commands and learn about ffmpeg, vaapi, Tdarr
and this script.
With that out of the way...Tdarr is awesome because it allowed me to create data driven code that
makes my library the best it could be. Thanks to everyone involved. Especially HaveAGitGat and Migz
whos existing code and assistance were invaluable
My belief is that given enough information about the video file an optimal configuration can be determined
specific to that file
This is based on what my goals are and uses external programs to gather as much useful information as possible
to make decisions.
There is a lot that goes into the gather and analysis part because:
It is the basis of the decisions and "garbage in, garbage out"
The video files are far from perfect when we get them and we need to make sure we learn as much as possible
The script adds metatags to the media files to control processing, or better yet not doing extra processing
on the same file.
This is especially useful when resetting Tdarr because each file gets touched again, so this expedites a full
library scan.
Tdarr does not seem to handle when a plugin code takes a while to run so all effort has been made minimize time
within the plugin code.
This is especially noticeable on a library reset and these scripts because of the extra time spent analyzing the
media files
Video: (Only one video stream is used!!)
The script computes a desired bitrate based on the following equation
(videoHeight * videoWidth * videoFPS) * targetCodecCompression
The first 3 give a raw number of bits that the stream requires, however with encoding there is a certain amount
of acceptable loss, this is targetCodecCompression
This number is pretty low for hevc. I have found 0.07 to be about the norm.
This means that for hevc only 7% of the raw bitrate is necessary to produce some decent results and actually I
have used, and seen, as low as 3.5%
If the source video is less than this rate the script will either:
Copy the existing stream, if the codec is hevc
Transcode the stream to hevc using 80% of the original streams bitrate
It could probably be less but if the source is of low bitrate we don<6F>t want to compromise too much on
the transcode
If the source media bitrate is close, within 10%, of the target bitrate and the codec is hevc, it will copy
instead of transcode to preserve quality
The script will do an on chip transcode, meaning the decode and encode is all done on chip, except for mpeg4
which must be decoded on the CPU
(TODO: Videos with a framerate higher than a threshold, lets say 30, should be changed)
Audio: (Only one audio stream is used!!)
The script will choose one audio stream in the file that has:
The desired language
The highest channel count
If the language is not set on the audio track it assumes it is in the desired language
The audio bit rate is set to a threshold, currently 64K, * number channels in AAC. This
seems to give decent results
If the source audio is less than this rate the script will either:
Copy the existing stream, if the codec is aac
Transcode the stream to aac using 100% of the original streams bitrate
It could probably be less but if the source is of low bitrate but, we don<6F>t want
to compromise too much on the transcode
Chapters:
If chapters are found the script keeps them unless...
Any chapter start time is a negative number (Yes I have seen it)
Any duplicate start times are found
(TODO: incomplete chapter info gets added to or removed...I have seen 2 chapters in the beginning and
then nothing)
The second routine will add chapters at set intervals to any file that has no chapters
Metadata:
Global metadata is cleared, I.E. title
Stream specific metadata is copied
Some requirements: (You should really really really really read this!!!)
! !!!! Docker on linux !!!!!
Intel QSV compatible processor, I run it on an i5-9400 and I know earlier models have no
HEVC capability or produce lessor results
First off the Matching pair:
Tdarr_Plugin_JB69_JBHEVCQSV_MinimalFile (JB - QSV(vaapi), H265, AAC, MKV, bitrate optimized)
Tdarr_Plugin_JB69_JBHEVCQSZ_PostFix (JB - MKV Stats, Chapters, Audio Language)
The order I run them in:
Tdarr_Plugin_JB69_JBHEVCQSV_MinimalFile (JB - H265, AAC, MKV, bitrate optimized)
Tdarr_Plugin_JB69_JBHEVCQSZ_PostFix (JB - MKV Stats, Chapters, Audio Language)
I am running the docker image provided for Tdarr
/// ///////////////////////////////////////////////////////////////////////////////////////////////////
*/
// tdarrSkipTest
const details = () => ({
id: 'Tdarr_Plugin_JB69_JBHEVCQSV_MinimalFile',
Stage: 'Pre-processing',
Name: 'JB - QSV(vaapi), H265, AAC, MKV, bitrate optimized',
Type: 'Video',
Operation: 'Transcode',
Description: `***You should not use this*** until you read the comments at the top of the code and understand
how it works **this does a lot** and is 1 of 2 routines you should to run **Part 1** \n`,
Version: '2.4',
Tags: 'pre-processing,ffmpeg,video,audio,qsv,h265,aac',
Inputs: [{
name: 'Stats_Days',
type: 'number',
defaultValue: 21,
inputUI: {
type: 'text',
},
tooltip: `If the stats date on the file are older than this it will first update them,\\n
usually for mkv only.`,
}, {
name: 'Target_Video_Codec',
type: 'string',
defaultValue: 'hevc',
inputUI: {
type: 'text',
},
tooltip: `This is the basis of the routine, if you want to change,\\n
it you probably want to use a different script`,
}, {
name: 'Use_10bit_Video',
type: 'boolean',
defaultValue: true,
inputUI: {
type: 'dropdown',
options: [
'true',
'false',
],
},
tooltip: 'This will encode in 10 bit? Some processors can not.',
}, {
name: 'Target_Framerate',
type: 'number',
defaultValue: 25,
inputUI: {
type: 'text',
},
tooltip: 'Any frame rate greater than this will be adjusted.',
}, {
name: 'Min_Size_Difference_to_Transcode',
type: 'number',
defaultValue: 1.2,
inputUI: {
type: 'text',
},
tooltip: `If the existing bitrate is this much more than the target bitrate\\n
it is ok to transcode, otherwise there might not be enough extra\\n
to get decent quality.`,
}, {
name: 'Target_Reduction_for_Code_Switch',
type: 'number',
defaultValue: 0.8,
inputUI: {
type: 'text',
},
tooltip: `When a video codec change happens and the source bitrate is lower\\n
than optimal, we still lower the bitrate by this since hevc is ok\\n
with a lower rate.`,
}, {
name: 'Max_Video_Height',
type: 'number',
defaultValue: 2160,
inputUI: {
type: 'dropdown',
options: [
720,
1080,
2160,
4320,
],
},
tooltip: 'Any thing over this size, I.E. 8K, will be reduced to this.',
}, {
name: 'Target_Codec_Compression',
type: 'number',
defaultValue: 0.08,
inputUI: {
type: 'text',
},
tooltip: 'This effects the target bitrate by assuming a compression ratio.',
}, {
name: 'Target_Audio_Codec',
type: 'string',
defaultValue: 'aac',
inputUI: {
type: 'text',
},
tooltip: 'Desired Audio Codec, if you change this it might require code changes.',
}, {
name: 'Target_Audio_Language',
type: 'string',
defaultValue: 'eng',
inputUI: {
type: 'text',
},
tooltip: 'Desired Audio Language.',
}, {
name: 'Target_Audio_Bitrate_Per_Channel',
type: 'number',
defaultValue: 64000,
inputUI: {
type: 'text',
},
tooltip: '64K per channel gives you the good lossy quality out of AAC.',
}, {
name: 'Target_Audio_Channels',
type: 'number',
defaultValue: 6,
inputUI: {
type: 'text',
},
tooltip: 'Any thing above this number of channels will be reduced to it.',
}],
});
function findMediaInfoItem(file, index) {
let currMIOrder = -1;
const strStreamType = file.ffProbeData.streams[index].codec_type.toLowerCase();
for (let i = 0; i < file.mediaInfo.track.length; i += 1) {
if (file.mediaInfo.track[i].StreamOrder) {
currMIOrder = file.mediaInfo.track[i].StreamOrder;
} else if (strStreamType === 'text' || strStreamType === 'subtitle') {
currMIOrder = file.mediaInfo.track[i].ID - 1;
} else {
currMIOrder = -1;
}
if (parseInt(currMIOrder, 10) === parseInt(index, 10) || currMIOrder === `0-${index}`) {
return i;
}
}
return -1;
}
// eslint-disable-next-line no-unused-vars
const plugin = (file, librarySettings, inputs, otherArguments) => {
// eslint-disable-next-line global-require
const lib = require('../methods/lib')();
// eslint-disable-next-line no-unused-vars,no-param-reassign
inputs = lib.loadDefaultValues(inputs, details);
const response = {
processFile: false,
preset: '',
container: '.mkv',
handBrakeMode: false,
FFmpegMode: true,
reQueueAfter: true,
infoLog: '',
};
const currentFileName = file._id; // .replace(/'/g, "'\"'\"'");
// Settings
/// ///////////////////////////////////////////////////////////////////////////////////////////////////
// Process Handling
const intStatsDays = inputs.Stats_Days;
// Video
const targetVideoCodec = inputs.Target_Video_Codec;
const bolUse10bit = inputs.Use_10bit_Video;
const targetFrameRate = inputs.Target_Framerate;
const minSizeDiffForTranscode = inputs.Min_Size_Difference_to_Transcode;
const targetReductionForCodecSwitchOnly = inputs.Target_Reduction_for_Code_Switch;
const maxVideoHeight = inputs.Max_Video_Height;
const targetCodecCompression = inputs.Target_Codec_Compression;
// Since videos can have many widths and heights we need to convert to pixels (WxH) to understand what we
// are dealing with and set a minimal optimal bitrate to not go below
const minVideoPixels4K = 6500000;
const minVideoRate4K = 8500000;
const minVideoPixels2K = 1500000;
const minVideoRate2K = 2400000;
const minVideoPixelsHD = 750000;
const minVideoRateHD = 1100000;
const minVideoRateSD = 450000;
// Audio
const targetAudioCodec = inputs.Target_Audio_Codec;
const targetAudioLanguage = inputs.Target_Audio_Language;
const targetAudioBitratePerChannel = inputs.Target_Audio_Bitrate_Per_Channel;
const targetAudioChannels = inputs.Target_Audio_Channels;
/// ///////////////////////////////////////////////////////////////////////////////////////////////////
const proc = require('child_process');
let bolStatsAreCurrent = false;
if (file.fileMedium !== 'video') {
response.processFile = false;
response.infoLog += 'File is not a video. Exiting \n';
return response;
}
if (file.container === 'mkv' && (
file.mediaInfo.track[0].extra !== undefined
&& file.mediaInfo.track[0].extra.JBDONEVERSION !== undefined
&& file.mediaInfo.track[0].extra.JBDONEVERSION === '1')
) {
response.processFile = false;
response.infoLog += 'File already Processed! \n';
return response;
}
// If the existing container is mkv there is a possibility the stats were not updated during any previous transcode,
// lets make sure
if (file.container === 'mkv') {
let datStats = Date.parse(new Date(70, 1).toISOString());
if (
file.ffProbeData.streams[0].tags !== undefined
&& file.ffProbeData.streams[0].tags['_STATISTICS_WRITING_DATE_UTC-eng'] !== undefined
) {
datStats = Date.parse(`${file.ffProbeData.streams[0].tags['_STATISTICS_WRITING_DATE_UTC-eng']} GMT`);
}
if (file.mediaInfo.track[0].extra !== undefined && file.mediaInfo.track[0].extra.JBDONEDATE !== undefined) {
const JBDate = Date.parse(file.mediaInfo.track[0].extra.JBDONEDATE);
response.infoLog += `JBDate: ${JBDate}, StatsDate: ${datStats}\n`;
if (datStats >= JBDate) {
bolStatsAreCurrent = true;
}
} else {
const statsThres = Date.parse(new Date(new Date().setDate(new Date().getDate() - intStatsDays)).toISOString());
response.infoLog += `StatsThres: ${statsThres}, StatsDate: ${datStats}\n`;
if (datStats >= statsThres) {
bolStatsAreCurrent = true;
}
}
if (!bolStatsAreCurrent) {
response.infoLog += 'Stats need to be updated! \n';
try {
proc.execSync(`mkvpropedit --add-track-statistics-tags "${currentFileName}"`);
return response;
} catch (err) {
response.infoLog += 'Error Updating Status Probably Bad file, A remux will probably fix, will continue\n';
}
}
}
// Logic Controls
let bolScaleVideo = false;
let bolTranscodeVideo = false;
let bolChangeFrameRateVideo = false;
let optimalVideoBitrate = 0;
let videoNewWidth = 0;
let bolSource10bit = false;
let bolTranscodeSoftwareDecode = false;
let audioNewChannels = 0;
let bolTranscodeAudio = false;
let bolDownMixAudio = false;
let audioChannels = 0;
let audioBitrate = 0;
let audioIdxChannels = 0;
let audioIdxBitrate = 0;
let bolDoSubs = false;
let bolForceNoSubs = false;
let bolDoSubsConvert = false;
const bolDoChapters = true;
let videoIdx = -1;
let videoIdxFirst = -1;
let audioIdx = -1;
let audioIdxOther = -1;
let strStreamType = '';
let MILoc = -1;
for (let i = 0; i < file.ffProbeData.streams.length; i += 1) {
strStreamType = file.ffProbeData.streams[i].codec_type.toLowerCase();
// Looking For Video
/// ///////////////////////////////////////////////////////////////////////////////////////////////////
if (strStreamType === 'video') {
MILoc = findMediaInfoItem(file, i);
response.infoLog += `Index ${i} MediaInfo stream: ${MILoc} \n`;
if (MILoc > -1) {
const streamHeight = file.ffProbeData.streams[i].height * 1;
const streamWidth = file.ffProbeData.streams[i].width * 1;
const streamFPS = file.mediaInfo.track[MILoc].FrameRate * 1;
let streamBR = file.mediaInfo.track[MILoc].BitRate * 1;
if (isNaN(streamBR)) {
streamBR = file.mediaInfo.track[MILoc].extra.FromStats_BitRate * 1;
}
response.infoLog
+= `Video stream ${i}:${Math.floor(file.meta.Duration / 60)}:`
+ `${file.ffProbeData.streams[i].codec_name}${(bolSource10bit) ? '(10)' : ''}`;
response.infoLog += `:${streamWidth}x${streamHeight}x${streamFPS}:${streamBR}bps \n`;
if (videoIdxFirst === -1) {
videoIdxFirst = i;
}
if (videoIdx === -1) {
videoIdx = i;
} else {
const MILocC = findMediaInfoItem(file, videoIdx);
const curStreamWidth = file.ffProbeData.streams[videoIdx].width * 1;
let curStreamBR = file.mediaInfo.track[MILocC].BitRate * 1;
if (isNaN(curStreamBR)) {
curStreamBR = file.mediaInfo.track[MILocC].extra.FromStats_BitRate * 1;
}
// Only check here based on bitrate and video width
if (streamBR > curStreamBR && streamWidth >= curStreamWidth) {
videoIdx = i;
}
}
}
}
/// ///////////////////////////////////////////////////////////////////////////////////////////////////
// Looking For Audio
/// ///////////////////////////////////////////////////////////////////////////////////////////////////
if (strStreamType === 'audio') {
audioChannels = file.ffProbeData.streams[i].channels * 1;
audioBitrate = file.mediaInfo.track[findMediaInfoItem(file, i)].BitRate * 1;
if (isNaN(audioBitrate)) {
audioBitrate = file.mediaInfo.track[findMediaInfoItem(file, i)].extra.FromStats_BitRate * 1;
}
if (
file.ffProbeData.streams[i].tags !== undefined
&& file.ffProbeData.streams[i].tags.language === targetAudioLanguage
) {
response.infoLog
+= `Audio stream ${i}:${targetAudioLanguage}`
+ `:${file.ffProbeData.streams[i].codec_name}:${audioChannels}:${audioBitrate}bps:`;
if (audioIdx === -1) {
response.infoLog += 'First Audio Stream \n';
audioIdx = i;
} else {
audioIdxChannels = file.ffProbeData.streams[audioIdx].channels * 1;
audioIdxBitrate = file.mediaInfo.track[findMediaInfoItem(file, audioIdx)].BitRate;
if (audioChannels > audioIdxChannels) {
response.infoLog += 'More Audio Channels \n';
audioIdx = i;
} else if (audioChannels === audioIdxChannels && audioBitrate > audioIdxBitrate) {
response.infoLog += 'Higher Audio Rate \n';
audioIdx = i;
}
}
} else {
response.infoLog += `Audio stream ${i}:???:${file.ffProbeData.streams[i].codec_name}`
+ `:${audioChannels}:${audioBitrate}bps:`;
if (audioIdxOther === -1) {
response.infoLog += 'First Audio Stream \n';
audioIdxOther = i;
} else {
audioIdxChannels = file.ffProbeData.streams[audioIdxOther].channels * 1;
audioIdxBitrate = file.mediaInfo.track[findMediaInfoItem(file, audioIdxOther)].BitRate;
if (audioChannels > audioIdxChannels) {
response.infoLog += 'More Audio Channels \n';
audioIdxOther = i;
} else if (audioChannels === audioIdxChannels && audioBitrate > audioIdxBitrate) {
response.infoLog += 'Higher Audio Rate \n';
audioIdxOther = i;
}
}
}
}
/// ///////////////////////////////////////////////////////////////////////////////////////////////////
// Looking For Subtitles
/// ///////////////////////////////////////////////////////////////////////////////////////////////////
if (!bolForceNoSubs && !bolDoSubs && (strStreamType === 'text' || strStreamType === 'subtitle')) {
// A sub has an S_TEXT/WEBVTT codec, ffmpeg will fail with it
if (file.mediaInfo.track[findMediaInfoItem(file, i)].CodecID !== 'S_TEXT/WEBVTT') {
bolDoSubs = true;
if (file.ffProbeData.streams[i].codec_name === 'mov_text') {
bolDoSubsConvert = true;
response.infoLog += 'SubTitles Found (mov_text), will convert \n';
} else {
response.infoLog += 'SubTitles Found, will copy \n';
}
} else {
response.infoLog += 'SubTitles Found (S_TEXT/WEBVTT), will not copy \n';
bolForceNoSubs = true;
}
}
/// ///////////////////////////////////////////////////////////////////////////////////////////////////
}
// Video Decision section
/// ///////////////////////////////////////////////////////////////////////////////////////////////////
if (videoIdx === -1) {
response.processFile = false;
response.infoLog += 'No Video Track !! \n';
return response;
}
bolTranscodeVideo = true; // We will assume we will be transcoding
MILoc = findMediaInfoItem(file, videoIdx);
let videoHeight = file.ffProbeData.streams[videoIdx].height * 1;
let videoWidth = file.ffProbeData.streams[videoIdx].width * 1;
let videoFPS = file.mediaInfo.track[MILoc].FrameRate * 1;
let videoBR = file.mediaInfo.track[MILoc].BitRate * 1;
if (isNaN(videoBR)) {
videoBR = file.mediaInfo.track[MILoc].extra.FromStats_BitRate * 1;
}
if (
file.ffProbeData.streams[videoIdx].profile !== undefined
&& file.ffProbeData.streams[videoIdx].profile.includes !== undefined
&& file.ffProbeData.streams[videoIdx].profile.includes('10')) {
bolSource10bit = true;
}
// Source is Variable Frame rate but we will transcode to fixed
if (file.mediaInfo.track[MILoc].FrameRate_Mode === 'VFR') videoFPS = 9999;
if (videoFPS > targetFrameRate && file.container !== 'ts') {
bolChangeFrameRateVideo = true; // Need to fix this it does not work :-(
}
// Lets see if we need to scale down the video size
if (videoHeight > maxVideoHeight) {
bolScaleVideo = true;
videoNewWidth = Math.floor((maxVideoHeight / videoHeight) * videoWidth);
response.infoLog
+= `Video Resolution, ${videoWidth}x${videoHeight}, need to convert to ${videoNewWidth}x${maxVideoHeight} \n`;
videoHeight = maxVideoHeight;
videoWidth = videoNewWidth;
}
// Figure out the desired bitrate
optimalVideoBitrate = Math.floor((videoHeight * videoWidth * targetFrameRate) * targetCodecCompression);
response.infoLog += `Pre Video Calc: ${videoHeight}, ${videoWidth}, ${videoFPS}, ${optimalVideoBitrate} \n`;
// We need to check for a minimum bitrate
if ((videoHeight * videoWidth) > minVideoPixels4K && optimalVideoBitrate < minVideoPixels4K) {
response.infoLog
+= `Video Bitrate calulcated for 4K, ${optimalVideoBitrate}, is below minimum, ${minVideoPixels4K} \n`;
optimalVideoBitrate = minVideoRate4K;
} else if ((videoHeight * videoWidth) > minVideoPixels2K && optimalVideoBitrate < minVideoRate2K) {
response.infoLog
+= `Video Bitrate calulcated for 2K, ${optimalVideoBitrate}, is below minimum, ${minVideoRate2K} \n`;
optimalVideoBitrate = minVideoRate2K;
} else if ((videoHeight * videoWidth) > minVideoPixelsHD && optimalVideoBitrate < minVideoRateHD) {
response.infoLog
+= `Video Bitrate calulcated for HD, ${optimalVideoBitrate}, is below minimum, ${minVideoRateHD} \n`;
optimalVideoBitrate = minVideoRateHD;
} else if (optimalVideoBitrate < minVideoRateSD) {
response.infoLog
+= `Video Bitrate calulcated for SD, ${optimalVideoBitrate}, is below minimum, ${minVideoRateSD} \n`;
optimalVideoBitrate = minVideoRateSD;
}
// Check if it is already hvec, if not then we must transcode
if (file.ffProbeData.streams[videoIdx].codec_name !== targetVideoCodec) {
response.infoLog
+= `Video existing Codex is ${file.ffProbeData.streams[videoIdx].codec_name}${(bolSource10bit) ? '(10)' : ''}`;
response.infoLog += `, need to convert to ${targetVideoCodec}${(bolUse10bit) ? '(10)' : ''} \n`;
if (file.ffProbeData.streams[videoIdx].codec_name === 'mpeg4') {
bolTranscodeSoftwareDecode = true;
response.infoLog
+= `Video existing Codex is ${file.ffProbeData.streams[videoIdx].codec_name}, `
+ 'need to decode with software codec \n';
} else if (
file.ffProbeData.streams[videoIdx].codec_name === 'h264'
&& file.ffProbeData.streams[videoIdx].profile.includes('10')
) {
// If the source is 10 bit then we must software decode since qsv will not decode 264 10 bit??
bolTranscodeSoftwareDecode = true;
response.infoLog
+= `Video existing Codex is ${file.ffProbeData.streams[videoIdx].codec_name} 10 bit,`
+ ' need to decode with software codec \n';
}
}
if (videoBR < (optimalVideoBitrate * minSizeDiffForTranscode)) {
// We need to be careful here are else we could produce a bad quality
response.infoLog += 'Low source bitrate! \n';
if (file.ffProbeData.streams[videoIdx].codec_name === targetVideoCodec) {
if (bolSource10bit === bolUse10bit) {
response.infoLog
+= `Video existing Bitrate, ${videoBR}, is close to target Bitrate, `
+ `${optimalVideoBitrate}, using existing stream \n`;
bolTranscodeVideo = false;
} else {
response.infoLog
+= 'Video existing bit depth is different from target, without a codec change, using using existing bitrate \n';
optimalVideoBitrate = videoBR;
}
} else {
// We have a codec change with not much meat so we need to adjust are target rate
response.infoLog += `Video existing Bitrate, ${videoBR}, is close to, or lower than, target Bitrate, `;
response.infoLog
+= `${optimalVideoBitrate}, with a codec change, using ${Math.floor(targetReductionForCodecSwitchOnly * 100)}`
+ '% of existing \n';
optimalVideoBitrate = Math.floor(videoBR * targetReductionForCodecSwitchOnly);
bolTranscodeVideo = true;
}
} else {
// We already know the existing bitrate has enough meat for a decent transcode
response.infoLog += `Video existing Bitrate, ${videoBR}, is higher than target,`
+ ` ${optimalVideoBitrate}, transcoding \n`;
}
response.infoLog += `Post Video Calc: ${videoHeight}, ${videoWidth}, ${videoFPS}, ${optimalVideoBitrate} \n`;
/// ///////////////////////////////////////////////////////////////////////////////////////////////////
// Audio Decision section
/// ///////////////////////////////////////////////////////////////////////////////////////////////////
if (audioIdx === -1) {
if (audioIdxOther !== -1) {
response.infoLog += 'Using Unknown Audio Track !! \n';
audioIdx = audioIdxOther;
} else {
response.processFile = false;
response.infoLog += 'No Audio Track !! \n';
return response;
}
}
let audioBR = file.mediaInfo.track[findMediaInfoItem(file, audioIdx)].BitRate * 1;
if (isNaN(audioBR)) {
audioBR = file.mediaInfo.track[findMediaInfoItem(file, audioIdx)].extra.FromStats_BitRate * 1;
}
if (file.ffProbeData.streams[audioIdx].channels > targetAudioChannels) {
bolDownMixAudio = true;
audioNewChannels = targetAudioChannels;
response.infoLog
+= `Audio existing Channels, ${file.ffProbeData.streams[audioIdx].channels}, `
+ `is higher than target, ${targetAudioChannels} \n`;
} else {
audioNewChannels = file.ffProbeData.streams[audioIdx].channels;
}
let optimalAudioBitrate = audioNewChannels * targetAudioBitratePerChannel;
// Now what are we going todo with the audio part
if (audioBR > (optimalAudioBitrate * 1.1)) {
bolTranscodeAudio = true;
response.infoLog += `Audio existing Bitrate, ${audioBR}, is higher than target, ${optimalAudioBitrate} \n`;
}
// If the audio codec is not what we want then we should transcode
if (file.ffProbeData.streams[audioIdx].codec_name !== targetAudioCodec) {
bolTranscodeAudio = true;
response.infoLog
+= `Audio Codec, ${file.ffProbeData.streams[audioIdx].codec_name}, is different than target, `
+ `${targetAudioCodec}, Changing \n`;
}
// If the source bitrate is less than out target bitrate we should not ever go up
if (audioBR < optimalAudioBitrate) {
response.infoLog += `Audio existing Bitrate, ${audioBR}, is lower than target,`
+ ` ${optimalAudioBitrate}, using existing `;
optimalAudioBitrate = audioBR;
if (file.ffProbeData.streams[audioIdx].codec_name !== targetAudioCodec) {
response.infoLog += 'rate';
} else {
response.infoLog += 'stream';
}
response.infoLog += ' \n';
}
/// ///////////////////////////////////////////////////////////////////////////////////////////////////
// lets assemble our ffmpeg command
/// ///////////////////////////////////////////////////////////////////////////////////////////////////
const strTranscodeFileOptions = ' ';
let strFFcmd = '';
if (bolTranscodeVideo) {
if (bolTranscodeSoftwareDecode) {
strFFcmd += ' -vaapi_device /dev/dri/renderD128 ';
} else {
strFFcmd += ' -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format vaapi ';
}
}
strFFcmd += ` <io> -max_muxing_queue_size 8000 -map 0:${videoIdx} `;
if (bolTranscodeVideo) {
// Used to make the output 10bit, I think the quotes need to be this way for ffmpeg
strFFcmd += ' -c:v:0 hevc_vaapi ';
if (bolScaleVideo || bolUse10bit || bolTranscodeSoftwareDecode || bolChangeFrameRateVideo) {
let strOptions = '';
let strFormat = '';
if (bolScaleVideo) {
// Used when video is above our target
strOptions += `w=-1:h=${maxVideoHeight}`;
}
let strChangeVideoRateString = '';
if (bolChangeFrameRateVideo) {
// Used to change the framerate to the target framerate
strChangeVideoRateString = `fps=${targetFrameRate},`;
}
if (strFormat.length > 0) {
strFormat += '=';
}
if (bolUse10bit && !bolSource10bit) {
// Used to make the output 10bit
strFormat += 'p010';
}
if (!bolUse10bit && bolSource10bit) {
// Used to make the output 8bit
strFormat += 'p008';
}
if (bolTranscodeSoftwareDecode) {
if (bolSource10bit) {
if (strFormat.length > 0) {
strFormat += ',';
}
// Used to make it sure the software decode is in the proper pixel format
strFormat += 'nv12|vaapi';
}
if (strFormat.length > 0) {
strFormat += ',';
}
// Used to make it use software decode if necessary
strFormat += 'hwupload';
}
if (strFormat.length > 0) {
if (strOptions.length > 0) {
strOptions += ',';
}
strOptions += `format=${strFormat}`;
}
if (bolTranscodeSoftwareDecode) {
strFFcmd += ` -vf "${strChangeVideoRateString} ${strOptions}" `;
} else {
strFFcmd += ` -vf "${strChangeVideoRateString} scale_vaapi=${strOptions}" `;
}
}
// Used when video is above our target
strFFcmd += ` -b:v ${optimalVideoBitrate} `;
} else {
strFFcmd += ' -c:v:0 copy ';
}
strFFcmd += ` -map 0:${audioIdx} `;
if (bolTranscodeAudio) {
strFFcmd += ` -c:a:0 ${targetAudioCodec} -b:a ${optimalAudioBitrate} `;
} else {
strFFcmd += ' -c:a:0 copy ';
}
if (bolDownMixAudio) {
strFFcmd += ` -ac ${audioNewChannels} `;
}
if (bolForceNoSubs) {
strFFcmd += ' -map -0:s ';
} else if (bolDoSubs) {
if (bolDoSubsConvert) {
strFFcmd += ' -map 0:s -c:s srt ';
} else {
strFFcmd += ' -map 0:s -scodec copy ';
}
}
strFFcmd += ` -map_metadata:g -1 -metadata JBDONEVERSION=1 -metadata JBDONEDATE=${new Date().toISOString()} `;
if (bolDoChapters) {
strFFcmd += ' -map_chapters 0 ';
} else {
strFFcmd += ' -map_chapters -1 ';
}
strFFcmd += strTranscodeFileOptions;
/// ///////////////////////////////////////////////////////////////////////////////////////////////////
response.preset += strFFcmd;
response.processFile = true;
response.infoLog += 'File needs work. Transcoding. \n';
return response;
};
module.exports.details = details;
module.exports.plugin = plugin;