Advertisement
Linkyboy

Tdarr NVENC Plugin

Mar 6th, 2023 (edited)
80
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. // This is a modified script using the "Boosh-Transcode using NVENC GPU & FFMPEG"
  2. // and "Remove letterbox script" by control#0405
  3. // This script is WIP and improves the logic for cropping black borders
  4. // It is currently hardcoded to use only NVENC hardware decoding
  5. // Future expansion will include an option to select the hardware decoder for NVENC
  6. // This script is designed to set video files to HEVC mkv and remove black borders
  7.  
  8. const details = () => ({
  9.   id: 'Tdarr_Plugin_Himea_NVENC_Optimizer',
  10.   Stage: 'Pre-processing',
  11.   Name: 'Himea NVENC Optimizer',
  12.   Type: 'Video',
  13.   Operation: 'Transcode',
  14.   Description: `This is a NVENC specific plugin. 8th+ gen Nvidia NVENC enabled CPUs are required.
  15.     Files not in H265/HEVC will be transcoded into H265/HEVC using NVENC. Files already in H265/HEVC will be
  16.     via Nvidia GPU using FFmpeg. Settings are dependant on file bitrate working by the logic that H265 can support
  17.     the same amount of data at half the bitrate of H264. This plugin will skip files already in HEVC, AV1 & VP9
  18.     unless "reconvert_hevc" is marked as true. If it is then these will be reconverted again into HEVC if they
  19.     exceed the bitrate specified in "hevc_max_bitrate".
  20.     NOTE - Created for use with UNRAID Docker and while it should support Windows/Mac etc, it may require
  21.     a custom version of FFmpeg to work properly.`,
  22.   Version: '1.0',
  23.   Tags: 'pre-processing,ffmpeg,video only,NVENC,h265,hevc,configurable',
  24.   Inputs: [
  25.   {
  26.     name: 'container',
  27.     type: 'string',
  28.     defaultValue: 'mkv',
  29.     inputUI: {
  30.       type: 'dropdown',
  31.       options: [
  32.         'mkv',
  33.         'mp4',
  34.       ],
  35.     },
  36.     tooltip: `\\n
  37.     ==DESCRIPTION==
  38.     \\n Specifies the output container of the file.
  39.     \\n Ensure that all stream types you may have are supported by your chosen container.
  40.     \\n
  41.     ==INFO==
  42.     \\n Only MP4 & MKV are supported and MKV is recommended.
  43.     \\nExample:\\n
  44.     mkv
  45.     \\nExample:\\n
  46.     mp4`,
  47.   },
  48.   {
  49.     name: 'force_conform',
  50.     type: 'boolean',
  51.     defaultValue: false,
  52.     inputUI: {
  53.       type: 'dropdown',
  54.       options: [
  55.         'false',
  56.         'true',
  57.       ],
  58.     },
  59.     tooltip: `\\n
  60.     ==DESCRIPTION==
  61.     \\n Make the file conform to output containers requirements.
  62.     Use if you need to ensure the encode works from mp4>mkv or mkv>mp4. \\n
  63.     ==WARNING== \\n
  64.     This will remove data of certain types so ensure you are happy with that,
  65.     or use another plugin to convert these data types first!
  66.     \\n
  67.     ==INFO==
  68.     \\n Drop hdmv_pgs_subtitle/eia_608/subrip/timed_id3 for MP4.
  69.     \\n Drop data streams/mov_text/eia_608/timed_id3 for MKV.
  70.     \\n Default is false.
  71.     \\nExample:\\n
  72.     true
  73.     \\nExample:\\n
  74.     false`,
  75.   },
  76.   {
  77.     name: 'output_codec',
  78.     type: 'string',
  79.     defaultValue: 'h265',
  80.     inputUI: {
  81.       type: 'dropdown',
  82.       options: [
  83.         'h264',
  84.         'h265'
  85.       ],
  86.     },
  87.     tooltip: `\\n
  88.     ==DESCRIPTION==
  89.     \\n Specify which codec to output as. h265 is recommended for 4k, h264 for 1080p.`
  90.   },
  91.   {
  92.     name: 'encoder_speedpreset',
  93.     type: 'string',
  94.     defaultValue: 'slow',
  95.     inputUI: {
  96.       type: 'dropdown',
  97.       options: [
  98.         'veryfast',
  99.         'faster',
  100.         'fast',
  101.         'medium',
  102.         'slow',
  103.         'slower',
  104.         'veryslow',
  105.       ],
  106.     },
  107.     tooltip: `\\n
  108.     ==DESCRIPTION==
  109.     \\n Specify the encoder speed/preset to use.
  110.     Slower options mean a slower encode but better quality and faster options mean faster encodes but
  111.     worse quality.
  112.     \\n For more information see Nvidia white paper on FFmpeg results using NVENC: \\n`
  113.     // eslint-disable-next-line max-len
  114.     + `https://www.Nvidia.com/content/dam/www/public/us/en/documents/white-papers/cloud-computing-quicksync-video-ffmpeg-white-paper.pdf
  115.     \\n
  116.     ==INFO==
  117.     \\n Default is "slow".
  118.     \\nExample:\\n
  119.     medium
  120.     \\nExample:\\n
  121.     slower`,
  122.   },
  123.   {
  124.     name: 'extra_NVENC_options',
  125.     type: 'string',
  126.     defaultValue: '',
  127.     inputUI: {
  128.       type: 'text',
  129.     },
  130.     tooltip: `\\n
  131.     ==DESCRIPTION==
  132.     \\n Here you can add extra options to the FFmpeg NVENC ENCODE cmd.
  133.     This does not override the FFmpeg cmd, it just allows additions to it.
  134.     \\n
  135.     There are extra NVENC options that can be
  136.     forced on/off as desired. See here for some possible cmds -
  137.     https://ffmpeg.org/ffmpeg-codecs.html#toc-HEVC-Options-1
  138.     \\n
  139.     ==WARNING== \\n
  140.     Just because a cmd is mentioned doesn't mean your installed version of FFmpeg supports it...
  141.    Be certain to verify the cmds work before adding to your workflow. \\n
  142.    Check Tdarr Help Tab. Enter FFmpeg cmd - "-h encoder=hevc_NVENC". This will give a list of supported commands.
  143.    \\n
  144.    ==INFO==
  145.    \\n Default is empty but a suggested value is below. If unsure just leave empty.
  146.    \\n Ensure to only use cmds valid to encoding NVENC as the script handles other FFmpeg cmds relating to
  147.    bitrate etc. Anything else entered here might be supported but could cause undesired results.
  148.    \\nExample:\\n
  149.    -extbrc 1 -rdo 1 -mbbrc 1 -b_strategy 1 -adaptive_i 1 -adaptive_b 1`,
  150.  },
  151.  {
  152.    name: 'bitrate_cutoff',
  153.    type: 'number',
  154.    defaultValue: 1500,
  155.    inputUI: {
  156.      type: 'text',
  157.    },
  158.    tooltip: `\\n
  159.    ==DESCRIPTION==
  160.    \\n Specify bitrate cutoff, files with a total bitrate lower then this will not be processed. \n
  161.    Since getting the bitrate of the video from files is unreliable, bitrate here refers to the total
  162.    bitrate of the file and not just the video steam.
  163.    \\n
  164.    ==INFO==
  165.    \\n Rate is in kbps.
  166.    \\n Defaults to 0 which means this is disabled.
  167.    \\n Enter a valid number to enable.
  168.    \\nExample:\\n
  169.    2500
  170.    \\nExample:\\n
  171.    1500`,
  172.  },
  173.  {
  174.    name: 'max_average_bitrate',
  175.    type: 'number',
  176.    defaultValue: 0,
  177.    inputUI: {
  178.      type: 'text',
  179.    },
  180.    tooltip: `\\n
  181.    ==DESCRIPTION==
  182.    \\n Specify a maximum average video bitrate. When encoding we take the current total bitrate and halve it
  183.    to get an average target. This option sets a upper limit to that average
  184.    (i.e if you have a video bitrate of 10000, half is 5000, if your maximum desired average bitrate is 4000
  185.    then we use that as the target instead of 5000).
  186.    \\n
  187.    ==INFO==
  188.    \\n Bitrate here is referring to video bitrate as we want to set the video bitrate on encode.
  189.    \\n Rate is in kbps.
  190.    \\n Defaults to 0 which means this is disabled.
  191.    \\n Enter a valid number to enable.
  192.    \\nExample:\\n
  193.    4000
  194.    \\nExample:\\n
  195.    3000`,
  196.  },
  197.  {
  198.    name: 'min_average_bitrate',
  199.    type: 'number',
  200.    defaultValue: 0,
  201.    inputUI: {
  202.      type: 'text',
  203.    },
  204.    tooltip: `\\n
  205.    ==DESCRIPTION==
  206.    \\n Specify a minimum average video bitrate. When encoding we take the current total bitrate and halve
  207.    it to get an average target. This option sets a lower limit to that average (i.e if you have a video bitrate
  208.    of 3000, half is 1500, if your minimum desired average bitrate is 2000 then we use that as the target instead
  209.    of 1500).
  210.    \\n
  211.    ==INFO==
  212.    \\n Bitrate here is referring to video bitrate as we want to set the video bitrate on encode.
  213.    \\n Rate is in kbps.
  214.    \\n Defaults to 0 which means this is disabled.
  215.    \\n Enter a valid number to enable.
  216.    \\nExample:\\n
  217.    2000
  218.    \\nExample:\\n
  219.    1000`,
  220.  },
  221.  {
  222.    name: 'reconvert_hevc',
  223.    type: 'boolean',
  224.    defaultValue: false,
  225.    inputUI: {
  226.      type: 'dropdown',
  227.      options: [
  228.        'false',
  229.        'true',
  230.      ],
  231.    },
  232.    tooltip: `\\n
  233.    ==DESCRIPTION==
  234.    \\n Specify if we want to reprocess HEVC, VP9 or AV1 files
  235.    (i.e reduce bitrate of files already in those codecs).
  236.    \\n Since this uses the same logic as normal, halving the current bitrate, this is NOT recommended
  237.    unless you know what you are doing, so leave false if unsure.
  238.    NEEDS to be used in conjunction with "bitrate_cutoff" or "hevc_max_bitrate" otherwise is ignored.
  239.    This is useful in certain situations, perhaps you have a file which is HEVC but has an extremely high
  240.    bitrate and you'd like to reduce it.
  241.     \\n Bare in mind that you can convert a file to HEVC and still be above your cutoff meaning it would
  242.     be converted again if this is set to true (since it's now HEVC). So if you use this be sure to set
  243.    "hevc_max_bitrate" & "max_average_bitrate" to prevent the plugin looping. Also it is highly suggested
  244.    that you have your "hevc_max_bitrate" higher than "max_average_bitrate".
  245.    \\n Again if you are unsure, please leave this as false!
  246.    \\n
  247.    ==WARNING== \\n
  248.    IF YOU HAVE VP9 OR AV1 FILES YOU WANT TO KEEP IN THOSE FORMATS THEN DO NOT USE THIS OPTION.
  249.    \\n
  250.    \\nExample:\\n
  251.    true
  252.    \\nExample:\\n
  253.    false`,
  254.  },
  255.  {
  256.    name: 'hevc_max_bitrate',
  257.    type: 'number',
  258.    defaultValue: 0,
  259.    inputUI: {
  260.      type: 'text',
  261.    },
  262.    tooltip: `\\n
  263.    ==DESCRIPTION==
  264.    \\n Has no effect unless "reconvert_hevc" is set to true. This allows you to specify a maximum
  265.    allowed average bitrate for HEVC or similar files. Much like the "bitrate_cutoff" option, but
  266.    specifically for HEVC files. It should be set HIGHER then your standard cutoff for safety.
  267.    \\n Also, it's highly suggested you use the min & max average bitrate options in combination with this. You
  268.     will want those to control the bitrate otherwise you may end up repeatedly reprocessing HEVC files.
  269.     i.e your file might have a bitrate of 20000, if your hevc cutoff is 5000 then it's going to reconvert
  270.    multiple times before it'll fall below that cutoff. While HEVC reprocessing can be useful
  271.     this is why it is NOT recommended!
  272.     \\n As with the cutoff, getting the bitrate of the video from files is unreliable, so bitrate
  273.     here refers to the total bitrate of the file and not just the video steam.
  274.     \\n
  275.     ==INFO==
  276.     \\n Rate is in kbps.
  277.     \\n Defaults to 0 which means this is disabled.
  278.     \\n Enter a valid number to enable, otherwise we use "bitrate_cutoff" and multiply x2 for a safe limit.
  279.     \\nExample:\\n
  280.     4000
  281.     \\nExample:\\n
  282.     3000`,
  283.   },
  284. ],
  285. });
  286.  
  287. // eslint-disable-next-line no-unused-vars
  288. const plugin = (file, librarySettings, inputs, otherArguments) => {
  289.   const lib = require('../methods/lib')(); const os = require('os');
  290.   // eslint-disable-next-line no-unused-vars,no-param-reassign
  291.   inputs = lib.loadDefaultValues(inputs, details);
  292.   const response = {
  293.     processFile: false,
  294.     preset: '',
  295.     handBrakeMode: false,
  296.     FFmpegMode: true,
  297.     infoLog: '',
  298.     container: `.${inputs.container}`
  299.   };
  300.  
  301.   // Set up required variables.
  302.   let duration = 0;
  303.   let videoIdx = 0;
  304.   let extraArguments = '';
  305.   let bitrateSettings = '';
  306.   let inflatedCutoff = 0;
  307.   let main10 = false;
  308.   let cutoff = false;
  309.  
  310.   // Check if file is a video. If it isn't then exit plugin.
  311.   if (file.fileMedium !== 'video') {
  312.     response.infoLog += 'File is not a video. \n';
  313.     return response;
  314.   }
  315.  
  316.   // Check if duration info is filled, if so times it by 0.0166667 to get time in minutes.
  317.   // If not filled then get duration of stream 0 and do the same.
  318.   if (parseFloat(file.ffProbeData?.format?.duration) > 0) {
  319.     duration = parseFloat(file.ffProbeData?.format?.duration) * 0.0166667;
  320.   } else if (typeof file.meta.Duration !== 'undefined') {
  321.     duration = file.meta.Duration * 0.0166667;
  322.   } else {
  323.     duration = file.ffProbeData.streams[0].duration * 0.0166667;
  324.   }
  325.  
  326.   // Work out currentBitrate using "Bitrate = file size / (number of minutes * .0075)"
  327.   // Used from here https://blog.frame.io/2017/03/06/calculate-video-bitrates/
  328.   const currentBitrate = Math.round(file.file_size / (duration * 0.0075));
  329.   // Use the same calculation used for currentBitrate but divide it in half to get targetBitrate.
  330.   // Logic of h265 can be half the bitrate as h264 without losing quality.
  331.   let targetBitrate = Math.round((file.file_size / (duration * 0.0075)) / 2);
  332.   // Allow some leeway under and over the targetBitrate.
  333.   let minimumBitrate = Math.round(targetBitrate * 0.75);
  334.   let maximumBitrate = Math.round(targetBitrate * 1.25);
  335.  
  336.   response.infoLog += `☑ It looks like the current bitrate is ${currentBitrate}k. \n`;
  337.  
  338.   // Check if the file needs to be cropped or if file is in 10bit (Plex does not support 10bit HEVC)
  339.   crop_values = generate_crop_values(file, Math.round(duration/0.0166667), otherArguments);
  340.   response.infoLog += `Crop values detected: ${crop_values.w}:${crop_values.h}:${crop_values.x}:${crop_values.y} \n`;
  341.   if (crop_values.x > 10 || crop_values.y > 10) {
  342.     response.infoLog += `☒ File needs to be cropped. Skipping bitrate cutoffs. \n`;
  343.  
  344.     for (let i = 0; i < file.ffProbeData.streams.length; i += 1) {
  345.       // If files are already 10bit then disable hardware decode to avoid problems with encode
  346.       // 10 bit from source file should be retained without extra arguments.
  347.       if (file.ffProbeData.streams[i].profile === 'High 10'
  348.         || file.ffProbeData.streams[i].profile === 'Main 10'
  349.         || file.ffProbeData.streams[i].bits_per_raw_sample === '10') {
  350.         main10 = true;
  351.         response.infoLog += 'Input file is 10bit. \n\n';
  352.       };
  353.     };
  354.   } else {
  355.     // If targetBitrate or currentBitrate comes out as 0 then something
  356.     // has gone wrong and bitrates could not be calculated.
  357.     // Cancel plugin completely.
  358.     if (targetBitrate <= 0 || currentBitrate <= 0) {
  359.       throw new Error('Target bitrate could not be calculated. Skipping this plugin.');
  360.     }
  361.  
  362.     // If targetBitrate is equal or greater than currentBitrate then something
  363.     // has gone wrong as that is not what we want.
  364.     // Cancel plugin completely.
  365.     if (targetBitrate >= currentBitrate) {
  366.       throw new Error("☒ Target bitrate has been calculated as ${targetBitrate}k. This is equal or greater than the current bitrate... Something has gone wrong and this shouldn't happen! Skipping this plugin.");
  367.     }
  368.  
  369.     // Ensure that bitrate_cutoff is set if reconvert_hevc is true since we need some protection against a loop
  370.     // Cancel the plugin
  371.     if (inputs.reconvert_hevc === true && inputs.bitrate_cutoff <= 0 && inputs.hevc_max_bitrate <= 0 && crop_values == '0:0:0:0') {
  372.       throw new Error(`☒ Reconvert HEVC is ${inputs.reconvert_hevc}, however there is no bitrate cutoff or HEVC specific cutoff set so we have no way to know when to stop processing this file.
  373.         Either set reconvert_HEVC to false or set a bitrate cutoff and set a hevc_max_bitrate cutoff. Skipping this plugin.`);
  374.     }
  375.  
  376.     // Check if inputs.bitrate cutoff has something entered.
  377.     // (Entered means user actually wants something to happen, empty would disable this).
  378.     if (inputs.bitrate_cutoff > 0) {
  379.       // Checks if currentBitrate is below inputs.bitrate_cutoff.
  380.       // If so then cancel plugin without touching original files.
  381.       if (currentBitrate <= inputs.bitrate_cutoff) {
  382.         response.infoLog += `☑ Current bitrate is below set cutoff of ${inputs.bitrate_cutoff}k. Cancelling plugin. \n`;
  383.         return response;
  384.       }
  385.       // If above cutoff then carry on
  386.       if (currentBitrate > inputs.bitrate_cutoff && inputs.reconvert_hevc === false) {
  387.         response.infoLog += `☒ Current bitrate appears to be above the cutoff of ${inputs.bitrate_cutoff}k. Need to process \n`;
  388.         cutoff = true;
  389.       }
  390.     }
  391.  
  392.     if (inputs.max_average_bitrate > 0) {
  393.       // Checks if targetBitrate is above inputs.max_average_bitrate.
  394.       // If so then clamp target bitrate
  395.       if (targetBitrate > inputs.max_average_bitrate) {
  396.         response.infoLog += `Our target bitrate is above the max_average_bitrate so
  397.         target average bitrate clamped at max of ${inputs.max_average_bitrate}k. \n`;
  398.         targetBitrate = Math.round(inputs.max_average_bitrate);
  399.         minimumBitrate = Math.round(targetBitrate * 0.75);
  400.         maximumBitrate = Math.round(targetBitrate * 1.25);
  401.       }
  402.     }
  403.  
  404.     // Check if inputs.min_average_bitrate has something entered.
  405.     // (Entered means user actually wants something to happen, empty would disable this).
  406.     if (inputs.min_average_bitrate > 0) {
  407.       // Exit the plugin is the cutoff is less than the min average bitrate. Most likely user error
  408.       if (inputs.bitrate_cutoff < inputs.min_average_bitrate) {
  409.         response.infoLog += `☒ Bitrate cutoff ${inputs.bitrate_cutoff}k is less than the set minimum
  410.         average bitrate set of ${inputs.min_average_bitrate}k. We don't want this. Cancelling plugin. \n`;
  411.        return response;
  412.      }
  413.      // Checks if inputs.bitrate_cutoff is below inputs.min_average_bitrate.
  414.      // If so then set currentBitrate to the minimum allowed.)
  415.      if (targetBitrate < inputs.min_average_bitrate) {
  416.        response.infoLog += `Target average bitrate clamped at min of ${inputs.min_average_bitrate}k. \n`;
  417.        targetBitrate = Math.round(inputs.min_average_bitrate);
  418.        minimumBitrate = Math.round(targetBitrate * 0.75);
  419.        maximumBitrate = Math.round(targetBitrate * 1.25);
  420.      }
  421.    }
  422.    // Go through each stream in the file.
  423.    for (let i = 0; i < file.ffProbeData.streams.length; i += 1) {
  424.      // Check if stream is a video.
  425.      if (file.ffProbeData.streams[i].codec_type.toLowerCase() === 'video') {
  426.        // Check if codec of stream is mjpeg/png, if so then remove this "video" stream.
  427.        // mjpeg/png are usually embedded pictures that can cause havoc with plugins.
  428.        if (file.ffProbeData.streams[i].codec_name === 'mjpeg' || file.ffProbeData.streams[i].codec_name === 'png') {
  429.          extraArguments += `-map -v:${videoIdx} `;
  430.        }
  431.  
  432.        // Check if video is already cropped and valid format.
  433.        if (crop_values.x < 10 && crop_values.y < 10) {
  434.          // Check if codec of stream is HEVC AND check if file.container matches inputs.container.
  435.          // If so nothing for plugin to do.
  436.          if ((file.ffProbeData.streams[i].codec_name === 'hevc' || file.ffProbeData.streams[i].codec_name === 'h264') && file.container === inputs.container && cutoff === false) {
  437.            response.infoLog += `☑ File is already HEVC or H264 & in ${inputs.container} and meets all cutoffs. \n`;
  438.            return response;
  439.          }
  440.  
  441.          // Check if codec is in valid decode format
  442.          if (file.ffProbeData.streams[i].codec_name === 'vp9' || file.ffProbeData.streams[i].codec_name === 'av1') {
  443.            throw new Error('VP9 or AV1 detected. This plugin does not support VP9 or AV1 decoding. Please use a different plugin.');
  444.          };
  445.  
  446.          // Check if codec of stream is HEVC, Vp9 or AV1
  447.          // AND check if file.container does NOT match inputs.container.
  448.          // If so remux file.
  449.          if ((file.ffProbeData.streams[i].codec_name === 'hevc' || file.ffProbeData.streams[i].codec_name === 'h264') && file.container !== inputs.container) {
  450.            response.infoLog += `☒ File is HEVC but is not in ${inputs.container} container. Remuxing. \n`;
  451.            response.preset = `<io> -map 0 -c copy ${extraArguments}`;
  452.            response.processFile = true;
  453.            return response;
  454.          }
  455.  
  456.          // New logic for reprocessing HEVC. Mainly done for my own use. Since we're reprocessing we're checking
  457.          // bitrate again and since this can be inaccurate (It calculates overall bitrate not video specific)
  458.          // we have to inflate the current bitrate so we don't keep looping this logic.
  459.         } else if (inputs.reconvert_hevc === true && (file.ffProbeData.streams[i].codec_name === 'hevc' || file.ffProbeData.streams[i].codec_name === 'h264') && crop_values.x < 10 && crop_values.y < 10) {
  460.           // If we're using the hevc max bitrate then update the cutoff to use it
  461.           if (inputs.hevc_max_bitrate > 0) {
  462.             if (currentBitrate > inputs.hevc_max_bitrate) {
  463.               // If bitrate is higher then hevc_max_bitrate then need to re-encode
  464.               response.infoLog += `☒ Reconvert_hevc is ${inputs.reconvert_hevc} & the file is already HEVC or H254.
  465.               Using HEVC specific cutoff of ${inputs.hevc_max_bitrate}k. \n
  466.               ☒ The file is still above this new cutoff! Reconverting. \n\n`;
  467.             } else {
  468.               // Otherwise we're now below the hevc cutoff and we can exit
  469.               response.infoLog += `☑ Reconvert_hevc is ${inputs.reconvert_hevc} & the file is already HEVC or H264.
  470.               Using HEVC specific cutoff of ${inputs.hevc_max_bitrate}k. \n
  471.               ☑ The file is NOT above this new cutoff. Exiting plugin. \n\n`;
  472.               return response;
  473.             }
  474.  
  475.             // If we're not using the hevc max bitrate then we need a safety net to try and ensure we don't keep
  476.             // looping this plugin. For maximum safety we simply multiply the cutoff by 2.
  477.           } else if (currentBitrate > (inputs.bitrate_cutoff * 2)) {
  478.             inflatedCutoff = Math.round(inputs.bitrate_cutoff * 2);
  479.             response.infoLog += `☒ Reconvert_hevc is ${inputs.reconvert_hevc} & the file is already HEVC or H264.
  480.             HEVC specific cutoff not set so bitrate_cutoff is multiplied by 2 for safety!
  481.             Cutoff now temporarily ${inflatedCutoff}k. \n The file is still above this new cutoff! Reconverting. \n\n`;
  482.           } else {
  483.             // File is below cutoff so we can exit
  484.             inflatedCutoff = Math.round(inputs.bitrate_cutoff * 2);
  485.             response.infoLog += `☑ Reconvert_hevc is ${inputs.reconvert_hevc} & the file is already HEVC, H264
  486.             so bitrate_cutoff is multiplied by 2! Cutoff now temporarily ${inflatedCutoff}k. \n
  487.             The file is NOT above this new cutoff. Exiting plugin. \n\n`;
  488.             return response;
  489.           };
  490.         };
  491.  
  492.         // If files are already 10bit then disable hardware decode to avoid problems with encode
  493.         // 10 bit from source file should be retained without extra arguments.
  494.         if (file.ffProbeData.streams[i].profile === 'High 10'
  495.           || file.ffProbeData.streams[i].profile === 'Main 10'
  496.           || file.ffProbeData.streams[i].bits_per_raw_sample === '10') {
  497.           main10 = true;
  498.           response.infoLog += 'Input file is 10bit. \n\n';
  499.         };
  500.  
  501.         // Increment video index. Needed to keep track of video id in case there is more than one video track.
  502.         // (i.e png or mjpeg which we would remove at the start of the loop)
  503.         videoIdx += 1;
  504.       };
  505.     };
  506.   };
  507.  
  508.   // It's possible to remux or flat out convert from mp4 to mkv so we need to conform to standards
  509.   // So check streams and add any extra parameters required to make file conform with output format.
  510.   // i.e drop mov_text for mkv files and drop pgs_subtitles for mp4
  511.   if (inputs.force_conform === true) {
  512.     if (inputs.container.toLowerCase() === 'mkv') {
  513.       extraArguments += '-map -0:d ';
  514.       for (let i = 0; i < file.ffProbeData.streams.length; i += 1) {
  515.         try {
  516.           if (
  517.             file.ffProbeData.streams[i].codec_name
  518.               .toLowerCase() === 'mov_text'
  519.             || file.ffProbeData.streams[i].codec_name
  520.               .toLowerCase() === 'eia_608'
  521.             || file.ffProbeData.streams[i].codec_name
  522.               .toLowerCase() === 'timed_id3'
  523.           ) {
  524.             extraArguments += `-map -0:${i} `;
  525.           }
  526.         } catch (err) {
  527.           // Error
  528.         }
  529.       }
  530.     }
  531.     if (inputs.container.toLowerCase() === 'mp4') {
  532.       for (let i = 0; i < file.ffProbeData.streams.length; i += 1) {
  533.         try {
  534.           if (
  535.             file.ffProbeData.streams[i].codec_name
  536.               .toLowerCase() === 'hdmv_pgs_subtitle'
  537.             || file.ffProbeData.streams[i].codec_name
  538.               .toLowerCase() === 'eia_608'
  539.             || file.ffProbeData.streams[i].codec_name
  540.               .toLowerCase() === 'subrip'
  541.             || file.ffProbeData.streams[i].codec_name
  542.               .toLowerCase() === 'timed_id3'
  543.           ) {
  544.             extraArguments += `-map -0:${i} `;
  545.           }
  546.         } catch (err) {
  547.           // Error
  548.         }
  549.       }
  550.     }
  551.   };
  552.  
  553.   // Set bitrateSettings variable using bitrate information calculated earlier.
  554.   bitrateSettings = `-maxrate ${inputs.bitrate_cutoff}k -bufsize ${currentBitrate}k`;
  555.   // Print to infoLog information around file & bitrate settings.
  556.   response.infoLog += `\nContainer for output selected as ${inputs.container}. \n`;
  557.   response.infoLog += 'Encode variable bitrate settings: \n';
  558.   response.infoLog += `Target = ${targetBitrate}k \n`;
  559.   // response.infoLog += `Minimum = ${minimumBitrate}k \n`;
  560.   response.infoLog += `Maximum = ${inputs.bitrate_cutoff}k \n`;
  561.  
  562.   // START PRESET
  563.   // DECODE FLAGS
  564.   // -fflags +genpts should regenerate timestamps if they end up missing...
  565.   response.preset = '-fflags +genpts ';
  566.  
  567.   //Set hardware acceleration
  568.   if (file.ffProbeData.streams[0].codec_name === 'h264') {
  569.     if (main10 === false) {
  570.       response.preset += '-hwaccel cuvid -hwaccel_output_format cuda -c:v h264_cuvid '
  571.       if (crop_values.x > 10 || crop_values.y > 10) {
  572.         response.preset += `-crop ${crop_values.y}x${crop_values.y}x${crop_values.x}x${crop_values.x} <io> -map 0 `;
  573.       } else {
  574.         response.preset += `<io> -map 0 `;
  575.       };
  576.     } else {
  577.       // If file is 10bit then disable hardware decode as it's unsupported
  578.       if (crop_values.x > 10 || crop_values.y > 10) {
  579.         response.preset += `<io> -map 0 -vf crop=${crop_values.w}:${crop_values.h}:${crop_values.x}:${crop_values.y},hwupload_cuda `
  580.       } else {
  581.         response.preset += `<io> -map 0 `
  582.       }
  583.     };
  584.   } else if (file.ffProbeData.streams[0].codec_name === 'hevc') {
  585.     if (main10 === false) {
  586.       response.preset += '-hwaccel cuvid -hwaccel_output_format cuda -c:v hevc_cuvid '
  587.       if (crop_values.x > 10 || crop_values.y > 10) {
  588.         response.preset += `-crop ${crop_values.y}x${crop_values.y}x${crop_values.x}x${crop_values.x} <io> -map 0 `;
  589.       } else {s
  590.         response.preset += `<io> -map 0 `;
  591.       };
  592.     } else {
  593.       // If file is 10bit then disable hardware decode as it's unsupported
  594.       if (crop_values.x > 10 || crop_values.y > 10) {
  595.         response.preset += `<io> -map 0 -vf crop=${crop_values.w}:${crop_values.h}:${crop_values.x}:${crop_values.y},hwupload_cuda `
  596.       } else {
  597.         response.preset += `<io> -map 0 `
  598.       }
  599.     };
  600.   } else {
  601.     // Unexpected file format, running without hardware decode
  602.     if (crop_values.x > 10 || crop_values.y > 10) {
  603.       response.preset += `<io> -map 0 -vf crop=${crop_values.w}:${crop_values.h}:${crop_values.x}:${crop_values.y},hwupload_cuda `;
  604.     } else {
  605.       response.preset += `<io> -map 0 `;
  606.     };
  607.   };
  608.   if (inputs.output_codec === 'h264') {
  609.     // Add the rest of the FFmpeg command
  610.     response.preset += ` ${bitrateSettings} `
  611.       + `-c:v h264_nvenc -preset ${inputs.encoder_speedpreset} ${inputs.extra_NVENC_options}
  612.       -c:a copy -c:s copy -max_muxing_queue_size 9999 ${extraArguments} -crf 20 -qp 20`;
  613.   } else if (inputs.output_codec === 'h265') {
  614.     // Add the rest of the FFmpeg command
  615.     response.preset += ` ${bitrateSettings} `
  616.       + `-c:v hevc_nvenc -preset ${inputs.encoder_speedpreset} ${inputs.extra_NVENC_options}
  617.       -c:a copy -c:s copy -max_muxing_queue_size 9999 ${extraArguments} -crf 20 -qp 20`;
  618.   };
  619.  
  620.  
  621.   // Add crop values to preset if they exist.
  622.   // if (crop_values.x > 10 || crop_values.y > 10) {
  623.   //   response.preset += `-vf hwdownload,crop=${crop_values.w}:${crop_values.h}:${crop_values.x}:${crop_values.y},hwupload_cuda`;
  624.   // }
  625.  
  626.   response.processFile = true;
  627.   response.infoLog += 'File Transcoding... \n';
  628.  
  629.   return response;
  630. };
  631.  
  632. module.exports.details = details;
  633. module.exports.plugin = plugin;
  634.  
  635. // Custom function to find the mode of a list
  636. function findMode(lst) {
  637. const counts = {};
  638. let maxCount = 0;
  639. let modes = [];
  640.  
  641. for (const num of lst) {
  642.   if (!counts[num]) {
  643.     counts[num] = 0;
  644.   }
  645.   counts[num]++;
  646.   if (counts[num] > maxCount) {
  647.     maxCount = counts[num];
  648.   }
  649. }
  650.  
  651. for (const num in counts) {
  652.   if (counts[num] === maxCount) {
  653.     modes.push(parseInt(num));
  654.   }
  655. }
  656.  
  657. return modes;
  658. }
  659.  
  660. // Use ffmpeg cropdetect to detect black bars.
  661. // If black bars are detected then set the crop variable to the detected values.
  662. function generate_crop_values(file, duration, otherArguments) {
  663. const fs = require('fs');
  664. const path = require('path');
  665. const { execSync } = require('child_process');
  666.  
  667. const sourceFile = file.meta.SourceFile;
  668. // const txtFile = path.join(path.dirname(sourceFile), path.basename(sourceFile, path.extname(sourceFile))) + ".txt";
  669.  
  670. // // Check if txtFile exists, and return the crop values from the file if it does
  671. // if (fs.existsSync(txtFile)) {
  672. //   const cropValues = fs.readFileSync(txtFile, 'utf8');
  673. //   const crop = cropValues.split(',');
  674. //   return {w: parseInt(crop[0]), h: parseInt(crop[1]), x: parseInt(crop[2]), y: parseInt(crop[3])};
  675. // }
  676.  
  677. // FFmpeg command to extract crop information and frames from the video
  678. const ffmpegCropCommand = `${otherArguments.ffmpegPath} -ss 120 -i \"${sourceFile}\" -t 9:00 -vf fps=1/2,cropdetect -f null - 2>&1`;
  679. const output = execSync(ffmpegCropCommand);
  680.  
  681. // Extract crop values from output using regex
  682. const crops = output
  683.  .toString()
  684.  .match(/crop=\S+/g)
  685.  .map((crop) => crop.substring(5));
  686.  
  687. //Get the most commonly returned number and set that as the crop value
  688. //ffmpeg returns 4 values for cropdetect: width:height:x:y
  689. var crop_w_mode = [];
  690. var crop_h_mode = [];
  691. var crop_x_mode = [];
  692. var crop_y_mode = [];
  693. for (var c = 0; c < crops.length; c++) {
  694.  crop = crops[c].split(':');
  695.  crop_w_mode.push(parseInt(crop[0]));
  696.  crop_h_mode.push(parseInt(crop[1]));
  697.  crop_x_mode.push(parseInt(crop[2]));
  698.  crop_y_mode.push(parseInt(crop[3]));
  699. }
  700. wMode = findMode(crop_w_mode);
  701. hMode = findMode(crop_h_mode);
  702. xMode = findMode(crop_x_mode);
  703. yMode = findMode(crop_y_mode);
  704.  
  705. // // Write the crop values to txtFile
  706. // const cropValuesStr = `${wMode}:${hMode}:${xMode}:${yMode}`;
  707. // fs.writeFileSync(txtFile, cropValuesStr);
  708.  
  709. // Return a dict of the crop values
  710. return {w:wMode, h:hMode, x:xMode, y:yMode};
  711. };
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement