HaveAGitGat / Tdarr

Tdarr - Distributed transcode automation using FFmpeg/HandBrake + Audio/Video library analytics + video health checking (Windows, macOS, Linux & Docker)
Other
2.94k stars 92 forks source link

Allow multiple presets to be supplied to Classic Transcoding plugin. #1047

Closed matthijsvrenswoude closed 2 months ago

matthijsvrenswoude commented 2 months ago

Okay, hear me out.

I have been using Tdarr on my media library for some time now. For that, I have written all the plugins myself. Currently, I am currently doing a full rework of my original plugins and trying to combine them all, including some mayor improvements including efficiency and a more streamlined design so it could be made into a community plugin.

For that I would like to request some changes though. The current version of the plugin, you could follow for updates here. It is still very much in WIP, but I am currently in the state where I can comfortably request some changes, since I have mostly figured out the complete software design.

So, a summary of what it does:

What additions I would like to see:

  1. To be able to build correctly encoded and playable for most devices DoVi Mp4s, I think 2 applications would be a good addition to add to Tdarrs default node apps:
    • Mp4Box: Mp4box is kinda like ffmpeg, but does everything mp4 related quite well. While FFMpeg can do most of the things on a basic level as well, you will quickly notice it doesn't support newer versions of MP4. It is specificly great for creating correctly encoded mp4s muxed with DoVi streams.
  1. In the classic transcode plugin, I would like otherArguments to have a parameter called pathVars. I was thinking pathvars should be an array with all the shell executable programs that are available to the Tdarr_node, kinda like how windows handles path variables I am currently using the following: let pathVars = [ ["node", "C:/Tdarr/Tdarr_Node/runtime/Tdarr_Node_Runtime.exe"], ["ffmpeg", otherArguments.ffmpegPath], ["mkvextract", otherArguments.mkvpropeditPath?.replace("mkvpropedit","mkvextract")], ["dovi_tool", "C:/Tdarr/DoviTool/dovi_tool.exe"], ["mp4box", "C:/Program Files/GPAC/mp4box.exe"], ];

This is how I would code it for Tdarr:

const fs = require("fs");
const path = require("path");

// You would probably want this array in the Tdarr node config file
const executableContainingFolders = [
    "C:/Tdarr/ffmpeg/",
    "C:/Tdarr/DoviTool/",
    "C:/Tdarr/Handbrake/",
    "C:/Tdarr/mkvtoolnix/",
    "C:/Tdarr/local/",
];

const fetchAllFilesFromGivenFolder = (fullPath) => {
    let files = [];
    fs.readdirSync(fullPath).forEach(file => {
        const absolutePath = path.join(fullPath, file);
        if (fs.statSync(absolutePath).isDirectory()){
            const filesFromNestedFolder = fetchAllFilesFromGivenFolder(absolutePath);
            filesFromNestedFolder.forEach(file => {
                files.push(file);
            })
        } else return files.push(absolutePath);
    }); return files
}

function generateNodePathVars(tdarrNodePath){
    const pathVars = [];
    const unixPlatforms = ["linux", "darwin", "freebsd", "openbsd", "aix", "sunos"];
    const platformExecutableExtension = process.platform === "win32" ? ".exe" : unixPlatforms.includes(process.platform) ? ".sh" : "";
    pathVars["node"] = tdarrNodePath;
    executableContainingFolders.forEach(folder => {
        try{
            const isFolder = fs.statSync(folder).isDirectory();
            isFolder && fetchAllFilesFromGivenFolder(folder).forEach(filePath => {
                if (filePath.endsWith(platformExecutableExtension)){
                    pathVars[path.parse(filePath).name] = filePath
                }
            })
        } catch (error){}
    });
    return pathVars;
}
const otherArguments = {};
otherArguments.pathVars = generateNodePathVars("C:\\Tdarr\\Tdarr_Node\\runtime\\Tdarr_Node_Runtime.exe"); //TdarrNodePath should be platform dynamic of course
  1. The earlier mentioned pathVars can then be used to execute multiple presets in order when set on the correct mode. For example, ill supply the following response to the classic transcoding plugin:
let response = {
    processFile: true,
    container: `.${file.container}`,
    multiPresetMode: true, //Allows an array to be supplied as preset and uses the executable name from pathVars to replace the first , in the corresponding preset to the program path.
    preset: [ // the following presets are an example of a file workflow
        ["mkvextract", ", extracting embedded subtitle"],
        ["node", ", preform subtitle filtering"],
        ["ffmpeg-fdk", ", create aac track with an ffmpeg builded with FDK_AAC support"], // With a local folder in the executableContainingFolders shown in suggestion 2, users can run their own shell programs with Tdarr monitoring. 
        ["ffmpeg", ", Package all the files together in one file."] // the last preset is always the one where tdarr ouput path/file container must be appended to
    ],
};

So now finally, with all the presets loaded, I would like Tdarr to show in the backend each individual preset with the following UI, afbeelding With it own openable and readable logs, as if it were separate plugins, including where if one preset fails the whole pipeline does too.

This way we can have one plugin have mayor performance improvements (CPU execution time, read/write operations you name it) and resource sharing capabilities, compared to separate flow plugins and all perfectly monitorable.

I hope I have explained my request well, If you have any questions, let me know.

HaveAGitGat commented 2 months ago

Hi thanks for posting.

The whole idea behind the flow system is not have super large plugins that do a lot of things but instead to have small configurable blocks where people can pick and choose what they want to happen.

Tdarr comes with several binaries (FFmpeg, FFprobe, HandBrake, mkvpropedit) and a few others which it needs for normal operation. If your plugin requires a specific external binary that the user must install manually then the paths for those can be set as a plugin input, likewise any presets can be set in plugin inputs and logic handled in the plugin. This can be combined with the global variable functionality on the Tools tab so that you can set the path in one place and re-use it in flow plugin inputs. Node path translation also applies to plugin inputs so any paths in input are automatically translated. There does not need to be new logic to do this.

So overall I think this suggestion is not adding anything new, in fact it's making it confusing as it seems to be misunderstanding how the classic plugins work. The preset string is just arguments for either handbrake or ffmpeg (handBrakeMode or FFmpegMode need to be set to true). Passing e.g. "mkvextract", ", extracting embedded subtitle" isn't a valid preset.

Also would recommend creating a flow plugin instead of a classic plugin. Classic plugins aren't being further developed and flow plugins are much more powerful and customisable (you can run any CLI on a file which is what you're trying to do, classic plugins are only for running handbrake or ffmpeg on a file).

Please post any plugin related bugs/requests at here: https://github.com/HaveAGitGat/Tdarr_Plugins

matthijsvrenswoude commented 2 months ago

I respectfully disagree on the added value.

While I agree with your statement there shouldn't be a super plugin, I think this one is an exception. And that reason is standardization of media. Codecs and playback support on devices is a giant cluster f*ck. Especially when having media from different sources. This Plugin tends to fix all that issues by creating 1 media file with almost identical quality to the original with maximum device playback support in new codecs.

Some examples are issues with media could be:

These are only a few issues, and I could get on and on with more issues that I have encountered. The plugin I have created is customizable for everyday normal users for the things that matter, without the need to have engineer level knowledge of codecs and playback support. In separate flow plugins that concept would be huge, incredibility complicated and also a pain to code. For the advanced users, there are even extra settings at the beginning of the code If they really want to play with the output / decision-making process.

For the current profile the plugin is using to intelligently determine the fate of individual streams, I spend days going through codec documentations and video files to determine the best course of action.

And undoubtedly the best setup for maximum support currently is HEVC, AC3 surround, AAC Stereo. (And User optional Dolby Truehd, DTS-HD Master Audio and Dolby vision in MP4)

The thing is, most people don't want to go in depth of the inner working of video playback, and to be honest I don't blame them, since it's complicated and often limited documented. Wouldn't it be nice if they can add one plugin, Setup that plugin with their Hardware Acceleration platform of choice, with the preferred languages of choice and some other preferences and the plugin takes care of fixing all codec issues, compact the file size by upgrading all the codecs and only keep the streams that they really want? They don't have to even leave it like that, and they could still be manipulate the file further by including it in flows, but at least it gives a good solid base to build on top off with current day codec support. I think on a platform like Tdarr that provides a good interface to track all progress and distribute tasks, such a plugin provides huge value.

I'll advise you to look through the code that I have already written, and see what you think.

Now to the topic of "mkvextract", ", extracting embedded subtitle" I know that the above-mentioned preset is not a valid preset it was only as example of the action I would like to, if you want an actual example, here is one without even something advanced like Dolby Vision or MKV to MP4 Conversion (I'm still improving the profiles on these):

afbeelding

HaveAGitGat commented 2 months ago

But then why not just put it in a single flow plugin if you're really wanting to do that? Or create a flow template that has all those plugins in ready to go so the user just has to configure/disable certain ones? That's sort of the whole purpose of templates: image

Would just export the flow and then add the file here in the same formatting as the others there: https://github.com/HaveAGitGat/Tdarr_Plugins/tree/master/FlowPluginsTs/CommunityFlowTemplates/video

The flows system has sub-flows with the Go To Flow plugin. So they could do their own stuff in a flow, then go to your flow, then go back to their original flow to continue other stuff?

Have a look at these 2 flow plugins: https://github.com/HaveAGitGat/Tdarr_Plugins/blob/master/FlowPluginsTs/CommunityFlowPlugins/classic/runClassicTranscodePlugin/2.0.0/index.ts https://github.com/HaveAGitGat/Tdarr_Plugins/blob/master/FlowPluginsTs/CommunityFlowPlugins/handbrake/handbrakeCustomArguments/2.0.0/index.ts

Those contain info on everything needed. runClassicTranscodePlugin shows how to run a classic plugin in a flow and handbrakeCustomArguments shows how to run a CLI in a flow. A lot of extra stuff in those plugins but this is the most important part:

  const cli = new CLI({
    cli: args.handbrakePath,
    spawnArgs: cliArgs,
    spawnOpts: {},
    jobLog: args.jobLog,
    outputFilePath,
    inputFileObj: args.inputFileObj,
    logFullCliOutput: args.logFullCliOutput,
    updateWorker: args.updateWorker,
    args,
  });

  const res = await cli.runCli();

  if (res.cliExitCode !== 0) {
    args.jobLog('Running HandBrake failed');
    throw new Error('Running HandBrake failed');
  }

So in the flow plugin inputs could have a switch or dropdown etc to enable disable different presets and then can act on those in the code.

So that's 2 ways to do what you're trying to do, or am I missing something there?

matthijsvrenswoude commented 2 months ago

I already started migrating to a flow plugin based on your reasoning in a previous post of using global vars for additional program paths. Which is a good middle ground. I however was still was under the impression flow plugins also only could execute one CLI action, which apparently is not the case. Your last post clears it up fully, and indeed lines up with my needs.

HaveAGitGat commented 1 month ago

Yeah that's right you can pretty much do anything in a flow plugin as long as you return file and output number at the end for Tdarr to continue with.

So can so can use this code to generate a new temp output folder file each time if you've got multiple things going on:

  const outputFilePath1 = `${getPluginWorkDir(args)}/${getFileName(args.inputFileObj._id)}.${container}`;
  const outputFilePath2 = `${getPluginWorkDir(args)}/${getFileName(args.inputFileObj._id)}.${container}`;

Where getPluginWorkDir creates a new folder for the temp file to be in.

Then can have multiple bits of processing:

...

if(args.inputs.thing1){
  const cli = new CLI({
    cli: args.ffmpegPath,
    spawnArgs: cliArgs,
    spawnOpts: {},
    jobLog: args.jobLog,
    outputFilePath: outputFilePath1,
    inputFileObj: args.inputFileObj,
    logFullCliOutput: args.logFullCliOutput,
    updateWorker: args.updateWorker,
    args,
  });

  const res = await cli.runCli();

  if (res.cliExitCode !== 0) {
    args.jobLog('Running FFmpeg failed');
    throw new Error('Running FFmpeg failed');
  }
}

...

if(args.inputs.thing2){
  const cli = new CLI({
    cli: args.ffmpegPath,
    spawnArgs: cliArgs,
    spawnOpts: {},
    jobLog: args.jobLog,
    outputFilePath: outputFilePath2,
    inputFileObj: args.inputFileObj,
    logFullCliOutput: args.logFullCliOutput,
    updateWorker: args.updateWorker,
    args,
  });

  const res = await cli.runCli();

  if (res.cliExitCode !== 0) {
    args.jobLog('Running FFmpeg failed');
    throw new Error('Running FFmpeg failed');
  }
}

...

If you have VSCode (other IDEs should support this too), you can right click on the type/interface to see the definition so you can see what info is passed to the plugin.

image

image

Hope that helps

HaveAGitGat commented 1 month ago

And if you produce a new file that you want Tdarr to give you info for, you can do it 2 ways.

First way is to use this (can see example in here: https://github.com/HaveAGitGat/Tdarr_Plugins/blob/71f468fab7ca51613dfee13c42e09435cbef7b8a/FlowPluginsTs/FlowHelpers/1.0.0/classicPlugins.ts#L114) :

  const fileInfo = await  args.scanIndividualFile(inputFileScanArgs, scanTypes);

Second way is each time you produce a new file, you return that and use the same process as what the Run Classic Transcode Plugin does where output 1 is if processing was done and output 2 is if no processing was done. So can keep looping through output 1 and then when all stuff is finished go to ouput 2. Each time it's looped Tdarr will scan the new file info and that will be passed to args at the start of the plugin.

image