├── README.md └── Tdarr_Plugin_purpan_H264_HEVC_to_NVENC_optional_HDR.js /README.md: -------------------------------------------------------------------------------- 1 | # purpan's H264/HEVC to NVENC with Optional HDR 2 | 3 | This utilizes code from multiple other plugins, but it's but mostly built off of [tws101's Ultimate_GPU_Transcoder_HDR](https://github.com/HaveAGitGat/Tdarr_Plugins/blob/4e0dd002c249247d338bf52c0595df917532eca7/Community/Tdarr_Plugin_tws101_Ultimate_GPU_Transcoder_HDR.js) plugin. I heavily tweaked it for higher quality encodes with even smaller sizes, added the tagging function, changed HDR to be optional, and made some other various edits, but most of the credit goes to them. 4 | 5 | ## Installation 6 | Click the file name above and hit the download button. Place the plugin in (main Tdarr directory)/server/Tdarr/plugins/local. Refresh your UI and it will appear under the 'local' plugins tab. 7 | 8 | **Make sure to add Migz-Remove-Image-formats-from-file _before_ this plugin. Otherwise it will attempt to remove the image formats itself, but it will go very slow.** 9 | 10 | ## Description 11 | 12 | This plugin will transcode H264 or reconvert HEVC files using NVENC with bframes, 10bit, and (optional) HDR. Requires a Turing NVIDIA GPU or newer. 13 | If reconvert HEVC is on and the entire file is over the bitrate filter, the HEVC stream will be re-encoded. Typically results in a 20-55% smaller size with very little quality loss. 14 | 15 | This plugin is designed for processing entire movie libraries, HDR content and all. **However, it's not recommended you actually use this to reconvert HDR files as it strips some HDR10/+/Dolby Vision metadata and leaves just PQ. The reconvert_hdr option is more meant to filter out these files rather than actually convert them.** 16 | 17 | Because of the heavily tweaked ffmpeg encoder settings, HEVC to HEVC reconverting somtimes results in a higher bitrate than the specified target bitrate, but much less than the original file's. This plugin implements the filter_by_stream_tag plugin to prevent infinite loops caused by that higher bitrate being above that target bitrate. 18 | 19 | By default, all settings are ideal for most use cases. 20 | 21 | ## Screenshots and previews 22 | 23 | You can click on the pictures and zoom in to get a feel for the (very minimal) difference in quality. 24 | 25 | **Disclaimer: the % reduction in these pictures is inaccurate.** Use the GB/mbps numbers for comparison instead- 26 | 27 | ### Full frames 28 | 29 | ![Screenshot 2023-12-31 152131](https://github.com/PronPan/Tdarr-H264-HEVC-to-NVENC-with-Optional-HDR/assets/5284391/619f1b39-b814-4b1f-b8c7-2ae07416d5a7) 30 | 31 | ![Screenshot 2023-12-31 152201](https://github.com/PronPan/Tdarr-H264-HEVC-to-NVENC-with-Optional-HDR/assets/5284391/ddabcd0a-ffc6-43a5-898a-cdbcb3dd665c) 32 | 33 | ![Screenshot 2023-12-31 152216](https://github.com/PronPan/Tdarr-H264-HEVC-to-NVENC-with-Optional-HDR/assets/5284391/5a201de8-b878-439b-b229-f5b5d257ea2c) 34 | 35 | ### Zoomed and cropped frames from dark scenes with motion 36 | 37 | ![theoutsiders](https://github.com/PronPan/Tdarr-H264-HEVC-to-NVENC-with-Optional-HDR/assets/5284391/2156b4e3-5cf3-40d6-a166-228ed1f190ec) 38 | 39 | ![thenun](https://github.com/PronPan/Tdarr-H264-HEVC-to-NVENC-with-Optional-HDR/assets/5284391/ed5511ef-56ec-4203-bdd0-47d6f7bdb9c3) 40 | 41 | 42 | -------------------------------------------------------------------------------- /Tdarr_Plugin_purpan_H264_HEVC_to_NVENC_optional_HDR.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // tdarrSkipTest 3 | const details = () => ({ 4 | id: 'Tdarr_Plugin_purpan_H264_HEVC_to_NVENC_optional_HDR', 5 | Stage: 'Pre-processing', 6 | Name: 'purpan- H264/HEVC to NVENC with Optional HDR', 7 | Type: 'Video', 8 | Operation: 'Transcode', 9 | Description: 10 | `This plugin will transcode H264 or reconvert HEVC files using NVENC with bframes, 10bit, and (optional) HDR. Requires a Turing NVIDIA GPU or newer. 11 | If reconvert HEVC is on and the entire file is over the bitrate filter, the HEVC stream will be re-encoded. Typically results in a 50-75% smaller size with little to no quality loss. 12 | When setting the re-encode bitrate filter be aware that it is a file total bitrate, so leave overhead for audio. 13 | This plugin implements the filter_by_stream_tag plugin to prevent infinite loops caused by reprocessing files above the filter or target bitrate. 14 | By default, all settings are ideal for most use cases. 15 | Version 1.3: Corrected FFmpeg command structure for input/output options. Fixed h264_cuvid initialization error.`, 16 | // Original plugin created by tws101 who was inspired by DOOM and MIGZ 17 | // This version edited by /u/purpan 18 | Version: '1.3', 19 | Tags: 'pre-processing,ffmpeg,nvenc h265, hdr', 20 | Inputs: [ 21 | { 22 | name: 'target_bitrate_480p576p', 23 | type: 'number', 24 | defaultValue: 1000, 25 | inputUI: { 26 | type: 'text', 27 | }, 28 | tooltip: 'Specify the target bitrate in kilobits for 480p and 576p files. Example 400 equals 400k', 29 | }, 30 | { 31 | name: 'target_bitrate_720p', 32 | type: 'number', 33 | defaultValue: 2000, 34 | inputUI: { 35 | type: 'text', 36 | }, 37 | tooltip: 'Specify the target bitrate in kilobits for 720p files. Example 400 equals 400k', 38 | }, 39 | { 40 | name: 'target_bitrate_1080p', 41 | type: 'number', 42 | defaultValue: 4000, 43 | inputUI: { 44 | type: 'text', 45 | }, 46 | tooltip: 'Specify the target bitrate in kilobits for 1080p files. Example 400 equals 400k', 47 | }, 48 | { 49 | name: 'target_bitrate_4KUHD', 50 | type: 'number', 51 | defaultValue: 8000, 52 | inputUI: { 53 | type: 'text', 54 | }, 55 | tooltip: 'Specify the target bitrate in kilobits for 4KUHD files. Example 400 equals 400k', 56 | }, 57 | { 58 | name: 'target_pct_reduction', 59 | type: 'number', 60 | defaultValue: 0.5, 61 | inputUI: { 62 | type: 'text', 63 | }, 64 | tooltip: 'Specify the target reduction for H264 bitrates if the current bitrate is less than resolution targets.', 65 | }, 66 | { 67 | name: 'bframes', 68 | type: 'boolean', 69 | defaultValue: false, 70 | inputUI: { 71 | type: 'dropdown', 72 | options: [ 73 | 'false', 74 | 'true', 75 | ], 76 | }, 77 | tooltip: 'Enables or disables bframes from being used. Sacrifices some detail for better compression. Requires NVIDIA Turing card or newer', 78 | }, 79 | { 80 | name: 'reconvert_hevc', 81 | type: 'boolean', 82 | defaultValue: true, 83 | inputUI: { 84 | type: 'dropdown', 85 | options: [ 86 | 'false', 87 | 'true', 88 | ], 89 | }, 90 | tooltip: 'Will reconvert hevc files that are above the hevc_resolution_filter_bitrate', 91 | }, 92 | { 93 | name: 'reconvert_hdr', 94 | type: 'boolean', 95 | defaultValue: false, 96 | inputUI: { 97 | type: 'dropdown', 98 | options: [ 99 | 'false', 100 | 'true', 101 | ], 102 | }, 103 | tooltip: 'Enable or disable reconverting HDR files. NOT recommended for HDR10/+/Dolby Vision files as it strips some HDR metadata and leaves just PQ', 104 | }, 105 | { 106 | name: 'hevc_480p_576p_filter_bitrate', 107 | type: 'number', 108 | defaultValue: 2000, 109 | inputUI: { 110 | type: 'text', 111 | }, 112 | tooltip: 'Filter bitrate in kilobits to reconvert_480p_576p_hevc. Example 1200 equals 1200k ', 113 | }, 114 | { 115 | name: 'hevc_720p_filter_bitrate', 116 | type: 'number', 117 | defaultValue: 3000, 118 | inputUI: { 119 | type: 'text', 120 | }, 121 | tooltip: 'Filter bitrate in kilobits to reconvert_720p_hevc. Example 1200 equals 1200k ', 122 | }, 123 | { 124 | name: 'hevc_1080p_filter_bitrate', 125 | type: 'number', 126 | defaultValue: 4000, 127 | inputUI: { 128 | type: 'text', 129 | }, 130 | tooltip: 'Filter bitrate in kilobits to reconvert_1080p_hevc. Example 1200 equals 1200k ', 131 | }, 132 | { 133 | name: 'hevc_filter_bitrate_4KUHD', 134 | type: 'number', 135 | defaultValue: 8000, 136 | inputUI: { 137 | type: 'text', 138 | }, 139 | tooltip: 'Filter bitrate in kilobits to reconvert_4KUHD_hevc. Example 1200 equals 1200k', 140 | }, 141 | { 142 | name: 'tagName', 143 | type: 'string', 144 | defaultValue: 'COPYRIGHT', 145 | inputUI: { 146 | type: 'text', 147 | }, 148 | tooltip: 149 | 'Enter the stream tag to check. By default, this metadata is added during the transcode process and no tagging options need to be changed', 150 | }, 151 | { 152 | name: 'tagValues', 153 | type: 'string', 154 | defaultValue: 'processed', 155 | inputUI: { 156 | type: 'text', 157 | }, 158 | tooltip: 159 | 'Enter a comma separated list of tag values to check for. By default, this metadata is added during the transcode process and no tagging options need to be changed', 160 | }, 161 | { 162 | name: 'exactMatch', 163 | type: 'boolean', 164 | defaultValue: true, 165 | inputUI: { 166 | type: 'dropdown', 167 | options: [ 168 | 'false', 169 | 'true', 170 | ], 171 | }, 172 | tooltip: 173 | 'Specify true if the property value must be an exact match,' 174 | + ' false if the property value must contain the value.', 175 | }, 176 | { 177 | name: 'continueIfTagFound', 178 | type: 'boolean', 179 | defaultValue: false, 180 | inputUI: { 181 | type: 'dropdown', 182 | options: [ 183 | 'false', 184 | 'true', 185 | ], 186 | }, 187 | tooltip: 188 | 'Specify whether to continue the plugin stack if the tag is found. This should almost never be True unless you want to transcode files twice', 189 | }, 190 | ], 191 | }); 192 | // #region Helper Classes/Modules 193 | /** 194 | * Handles logging in a standardised way. 195 | */ 196 | class Log { 197 | constructor() { 198 | this.entries = []; 199 | } 200 | /** 201 | * 202 | * @param {String} entry the log entry string 203 | */ 204 | Add(entry) { 205 | this.entries.push(entry); 206 | } 207 | /** 208 | * 209 | * @param {String} entry the log entry string 210 | */ 211 | AddSuccess(entry) { 212 | this.entries.push(`☑ ${entry}`); 213 | } 214 | /** 215 | * 216 | * @param {String} entry the log entry string 217 | */ 218 | AddError(entry) { 219 | this.entries.push(`☒ ${entry}`); 220 | } 221 | /** 222 | * Returns the log lines separated by new line delimiter. 223 | */ 224 | GetLogData() { 225 | return this.entries.join('\n'); 226 | } 227 | } 228 | /** 229 | * Handles the storage of FFmpeg configuration. 230 | */ 231 | class Configurator { 232 | constructor(defaultOutputSettings = null) { 233 | this.shouldProcess = false; 234 | this.outputSettings = defaultOutputSettings || []; 235 | this.inputSettings = []; 236 | } 237 | AddInputSetting(configuration) { 238 | if (configuration && configuration.trim() !== '') { 239 | this.inputSettings.push(configuration); 240 | } 241 | } 242 | AddOutputSetting(configuration) { 243 | this.shouldProcess = true; 244 | this.outputSettings.push(configuration); 245 | } 246 | ResetOutputSetting(configuration) { 247 | this.shouldProcess = false; 248 | this.outputSettings = configuration; 249 | } 250 | RemoveOutputSetting(configuration) { 251 | const index = this.outputSettings.indexOf(configuration); 252 | if (index === -1) return; 253 | this.outputSettings.splice(index, 1); 254 | } 255 | GetOutputSettings() { 256 | return this.outputSettings.join(' '); 257 | } 258 | GetInputSettings() { 259 | return this.inputSettings.join(' '); 260 | } 261 | } 262 | // #endregion 263 | // #region Plugin Methods 264 | /** 265 | * Abort Section 266 | */ 267 | function checkAbort(inputs, file, logger) { 268 | if (file.fileMedium !== 'video') { 269 | logger.AddError('File is not a video.'); 270 | return true; 271 | } 272 | return false; 273 | } 274 | /** 275 | * Calculate Bitrate 276 | */ 277 | function calculateBitrate(file) { 278 | let bitrateProbe = file.ffProbeData.streams[0].bit_rate; 279 | if (isNaN(bitrateProbe)) { 280 | bitrateProbe = file.bit_rate; 281 | } 282 | return bitrateProbe; 283 | } 284 | /** 285 | * Loops over the file streams and executes the given method on 286 | * each stream when the matching codec_type is found. 287 | * @param {Object} file the file. 288 | * @param {string} type the typeo of stream. 289 | * @param {function} method the method to call. 290 | */ 291 | function loopOverStreamsOfType(file, type, method) { 292 | let id = 0; 293 | for (let i = 0; i < file.ffProbeData.streams.length; i++) { 294 | if (file.ffProbeData.streams[i].codec_type.toLowerCase() === type) { 295 | method(file.ffProbeData.streams[i], id); 296 | id++; 297 | } 298 | } 299 | } 300 | function checkHDRMetadata(stream, id, inputs, logger, configuration) { 301 | const hdrColorSpaces = ['smpte2084', 'bt2020', 'bt2020nc']; 302 | logger.Add(`Checking HDR Metadata for video stream ${id}`); 303 | if (stream.color_space && hdrColorSpaces.includes(stream.color_space)) { 304 | logger.Add(`HDR Color Space detected in video stream ${id}: ${stream.color_space}`); 305 | if (!inputs.reconvert_hdr) { 306 | logger.AddError(`HDR Metadata detected in video stream ${id}. Skipping encoding due to reconvert_hdr set to false.`); 307 | return false; // Returning false to indicate HDR detected but reconvert_hdr is false 308 | } 309 | logger.AddSuccess(`HDR Metadata detected in video stream ${id}. Maintaining.`); 310 | // Add HDR configuration to the output settings 311 | if (stream.color_space === 'bt2020nc') { 312 | logger.Add(`Applying HDR configuration to stream ${id}: -color_primaries bt2020 -colorspace bt2020nc -color_trc smpte2084`); 313 | configuration.AddOutputSetting(' -strict unofficial -color_primaries bt2020 -colorspace bt2020nc -color_trc smpte2084 '); 314 | } 315 | return true; // HDR detected and reconvert_hdr is true, continue encoding 316 | } 317 | logger.Add(`No HDR Metadata detected in video stream ${id}. Continuing encoding.`); 318 | return true; // HDR not detected, continue encoding 319 | } 320 | /** 321 | * Video, Map EVERYTHING and encode video streams to 265 322 | */ 323 | function buildVideoConfiguration(inputs, file, logger) { 324 | const configuration = new Configurator(['-map 0']); // -map 0 is an output option 325 | const tiered = { 326 | '480p': { 327 | bitrate: inputs.target_bitrate_480p576p, 328 | max_increase: 100, 329 | cq: 22, 330 | }, 331 | '576p': { 332 | bitrate: inputs.target_bitrate_480p576p, 333 | max_increase: 100, 334 | cq: 22 335 | }, 336 | '720p': { 337 | bitrate: inputs.target_bitrate_720p, 338 | max_increase: 200, 339 | cq: 23 340 | }, 341 | '1080p': { 342 | bitrate: inputs.target_bitrate_1080p, 343 | max_increase: 400, 344 | cq: 24 345 | }, 346 | '4KUHD': { 347 | bitrate: inputs.target_bitrate_4KUHD, 348 | max_increase: 400, 349 | cq: 26 , 350 | }, 351 | Other: { 352 | bitrate: inputs.target_bitrate_1080p, 353 | max_increase: 400, 354 | cq: 24, 355 | }, 356 | }; 357 | // These are HWAccel decoders, thus input options 358 | const inputDecoderSettings = { 359 | h263: '-c:v h263_cuvid', 360 | h264: '', // Allow FFmpeg to auto-select decoder for H264 361 | mjpeg: '-c:v mjpeg_cuvid', 362 | mpeg1: '-c:v mpeg1_cuvid', 363 | mpeg2: '-c:v mpeg2_cuvid', 364 | vc1: '-c:v vc1_cuvid', 365 | vp8: '-c:v vp8_cuvid', 366 | vp9: '-c:v vp9_cuvid', 367 | }; 368 | 369 | function videoProcess(stream, id) { 370 | if (stream.codec_name === 'mjpeg') { 371 | configuration.AddOutputSetting(`-map -v:${id}`); // Output option 372 | return; 373 | } 374 | if (!checkHDRMetadata(stream, id, inputs, logger, configuration)) { 375 | return; 376 | } 377 | 378 | const filterBitrate480 = (inputs.hevc_480p_576p_filter_bitrate * 1000); 379 | const filterBitrate720 = (inputs.hevc_720p_filter_bitrate * 1000); 380 | const filterBitrate1080 = (inputs.hevc_1080p_filter_bitrate * 1000); 381 | const filterBitrate4k = (inputs.hevc_filter_bitrate_4KUHD * 1000); 382 | const fileResolution = file.video_resolution; 383 | const reconvert = inputs.reconvert_hevc; 384 | const res480p = '480p'; 385 | const res576p = '576p'; 386 | const res720p = '720p'; 387 | const res1080p = '1080p'; 388 | const res4k = '4KUHD'; 389 | 390 | if (reconvert === false) { 391 | if (stream.codec_name === 'hevc' || stream.codec_name === 'vp9') { 392 | logger.AddSuccess(`Video stream ${id} is hevc, and hevc reconvert is off`); 393 | return; 394 | } 395 | } 396 | 397 | function reconvertcheck(filterbitrate, res, res2) { 398 | if ((filterbitrate > 0) && ((fileResolution === res) || (fileResolution === res2))) { 399 | if ((stream.codec_name === 'hevc' || stream.codec_name === 'vp9') && (file.bit_rate < filterbitrate)) { 400 | logger.AddSuccess(`Video stream ${id} bitrate is below the HEVC/VP9 filter criteria: Bitrate Criteria (${filterbitrate} kbps) > File Bitrate (${file.bit_rate} kbps)`); 401 | return true; 402 | } else if (stream.codec_name === 'hevc' || stream.codec_name === 'vp9') { 403 | logger.Add(`Video stream ${id} is HEVC/VP9 and its bitrate (${file.bit_rate} kbps) is above filter (${filterbitrate} kbps)`); 404 | } 405 | } 406 | return false; 407 | } 408 | 409 | const bool480 = reconvertcheck(filterBitrate480, res480p, res576p); 410 | const bool720 = reconvertcheck(filterBitrate720, res720p); 411 | const bool1080 = reconvertcheck(filterBitrate1080, res1080p); 412 | const bool4k = reconvertcheck(filterBitrate4k, res4k); 413 | if (bool480 === true || bool720 === true || bool1080 === true || bool4k === true) { 414 | return; 415 | } 416 | 417 | if (stream.codec_name === 'png') { 418 | configuration.AddOutputSetting(`-map -0:v:${id}`); // Output option 419 | } else { 420 | const bitrateProbe = (calculateBitrate(file) / 1000); 421 | let bitrateTarget = 0; 422 | const tier = tiered[file.video_resolution]; 423 | if (tier == null) { 424 | logger.AddError('Plugin does not support the files video resolution'); 425 | return; 426 | } 427 | const bitrateCheck = parseInt(tier.bitrate); 428 | if (bitrateProbe !== null && bitrateProbe < bitrateCheck) { 429 | bitrateTarget = parseInt(bitrateProbe * inputs.target_pct_reduction); 430 | } else { 431 | bitrateTarget = parseInt(tier.bitrate); 432 | } 433 | const bitrateMax = bitrateTarget + tier.max_increase; 434 | const { cq } = tier; 435 | 436 | // Add output video encoding settings 437 | configuration.AddOutputSetting(`-c:v hevc_nvenc -profile:v main10 -pix_fmt:v p010le -qmin 0 -cq:v ${cq} -b:v ${bitrateTarget}k -maxrate:v ${bitrateMax}k -preset slow -rc-lookahead 32 -spatial_aq:v 1 -aq-strength:v 15 -metadata:s:v:0 COPYRIGHT=processed`); 438 | 439 | // Add input decoder settings if specified 440 | const decoderSetting = inputDecoderSettings[file.video_codec_name]; 441 | if (decoderSetting !== undefined && decoderSetting.trim() !== '') { 442 | configuration.AddInputSetting(decoderSetting); 443 | } 444 | // The specific h264_cuvid logic that caused issues was removed previously. 445 | // Now relying on inputDecoderSettings['h264'] = '' for auto-selection. 446 | 447 | logger.Add(`Transcoding stream ${id} to HEVC using NVidia NVENC`); 448 | } 449 | } 450 | 451 | loopOverStreamsOfType(file, 'video', videoProcess); 452 | 453 | if (!configuration.shouldProcess) { 454 | logger.AddSuccess('No video processing necessary'); 455 | } 456 | return configuration; 457 | } 458 | /** 459 | * Audio, set audio to copy 460 | */ 461 | function buildAudioConfiguration(inputs, file, logger) { 462 | const configuration = new Configurator(['-c:a copy']); // Output option 463 | return configuration; 464 | } 465 | /** 466 | * Subtitles, set subs to copy 467 | */ 468 | function buildSubtitleConfiguration(inputs, file, logger) { 469 | const configuration = new Configurator(['-c:s copy']); // Output option 470 | return configuration; 471 | } 472 | function checkTags(file, inputs) { 473 | const { strHasValue } = require('../methods/utils'); 474 | const lib = require('../methods/lib')(); 475 | // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-param-reassign 476 | inputs = lib.loadDefaultValues(inputs, details); 477 | const response = { 478 | processFile: false, 479 | infoLog: '', 480 | }; 481 | if (inputs.tagName.trim() === '') { 482 | response.infoLog += 'No input tagName entered in plugin, skipping \n'; 483 | return response; 484 | } 485 | const tagName = inputs.tagName.trim(); 486 | if (inputs.tagValues.trim() === '') { 487 | response.infoLog += 'No input tagValues entered in plugin, skipping \n'; 488 | return response; 489 | } 490 | const tagValues = inputs.tagValues.trim().split(','); 491 | // Loop through file streams to check for the specified tag 492 | let streamContainsTag = false; 493 | try { 494 | for (let i = 0; i < file.ffProbeData.streams.length; i += 1) { 495 | if (file.ffProbeData.streams[i]?.tags && strHasValue(tagValues, file.ffProbeData.streams[i].tags[tagName], inputs.exactMatch)) { 496 | streamContainsTag = true; 497 | break; 498 | } 499 | } 500 | const message = `A stream with tag name ${tagName} containing ${tagValues.join(',')} has`; 501 | // Handling encoding skipping based on tag presence 502 | if (inputs.continueIfTagFound === true) { 503 | if (streamContainsTag === true) { 504 | response.processFile = true; // Continue encoding if tag found 505 | response.infoLog += `${message} been found but continue_if_tag_found is True. Continuing to encoding. \n`; 506 | console.log(`${message} been found but continue_if_tag_found is True. Continuing to encoding.`); 507 | } else { 508 | response.processFile = true; // Continue encoding if tag not found 509 | response.infoLog += `${message} not been found and continue_if_tag_found is True. Continuing to encoding. \n`; 510 | console.log(`${message} not been found and continue_if_tag_found is True. Continuing to encoding.`); 511 | } 512 | } else if (inputs.continueIfTagFound === false) { 513 | if (streamContainsTag === true) { 514 | response.processFile = false; // Skip encoding if tag found 515 | response.infoLog += `${message} been found and continue_if_tag_found is False. Skipping encoding. \n`; 516 | console.log(`${message} been found and continue_if_tag_found is False. Skipping encoding.`); 517 | } else { 518 | response.processFile = true; // Resume encoding if tag not found 519 | response.infoLog += `${message} not been found and continue_if_tag_found is False. Continuing to encoding. \n`; 520 | console.log(`${message} not been found and continue_if_tag_found is False. Continuing to encoding.`); 521 | } 522 | } 523 | } catch (err) { 524 | console.log(err); 525 | response.infoLog += err; 526 | response.processFile = false; // Set default to skip encoding in case of error 527 | } 528 | return response; 529 | } 530 | // #endregion 531 | const plugin = (file, librarySettings, inputs, otherArguments) => { 532 | const { strHasValue } = require('../methods/utils'); 533 | const lib = require('../methods/lib')(); 534 | inputs = lib.loadDefaultValues(inputs, details); 535 | const response = { 536 | container: `.${file.container}`, 537 | FFmpegMode: true, 538 | handBrakeMode: false, 539 | infoLog: '', 540 | processFile: false, 541 | preset: '', 542 | reQueueAfter: true, 543 | }; 544 | const logger = new Log(); 545 | const tagCheck = checkTags(file, inputs); 546 | if (!tagCheck.processFile) { 547 | response.processFile = false; 548 | response.infoLog += tagCheck.infoLog; 549 | return response; 550 | } 551 | const abort = checkAbort(inputs, file, logger); 552 | if (abort) { 553 | response.processFile = false; 554 | response.infoLog += logger.GetLogData(); 555 | return response; 556 | } 557 | 558 | const videoSettings = buildVideoConfiguration(inputs, file, logger); 559 | const audioSettings = buildAudioConfiguration(inputs, file, logger); // Primarily output settings 560 | const subtitleSettings = buildSubtitleConfiguration(inputs, file, logger); // Primarily output settings 561 | 562 | // Start with input-specific options 563 | // -analyzeduration and -probesize are input options 564 | let inputOptions = '-analyzeduration 2147483647 -probesize 2147483647'; 565 | const videoInputSettings = videoSettings.GetInputSettings(); 566 | if (videoInputSettings && videoInputSettings.trim() !== '') { 567 | inputOptions += ` ${videoInputSettings}`; 568 | } 569 | 570 | // Consolidate all output options 571 | let outputOptions = videoSettings.GetOutputSettings(); 572 | outputOptions += ` ${audioSettings.GetOutputSettings()}`; 573 | outputOptions += ` ${subtitleSettings.GetOutputSettings()}`; 574 | outputOptions += ' -max_muxing_queue_size 9999'; // This is an output option 575 | 576 | // b frames argument (output option) 577 | if (inputs.bframes === true) { 578 | outputOptions += ' -bf 2 -b_ref_mode middle'; 579 | } 580 | 581 | // Construct the preset string: INPUT_OPTIONS,OUTPUT_OPTIONS 582 | // Ensure there's always a comma, even if inputOptions is just the probesize/analyzeduration 583 | response.preset = `${inputOptions.trim()},${outputOptions.trim()}`; 584 | 585 | response.processFile = videoSettings.shouldProcess; 586 | if (!response.processFile) { 587 | logger.AddSuccess('No need to process file'); 588 | } 589 | response.infoLog += logger.GetLogData(); 590 | return response; 591 | }; 592 | module.exports.details = details; 593 | module.exports.plugin = plugin; 594 | --------------------------------------------------------------------------------