FFmpeg Scheduler

CMakeLists.txt

cmake_minimum_required(VERSION 3.9)
project(FFmpegScheduler)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

if(MSYS OR MINGW)
	SET(CMAKE_CXX_FLAGS "-static")
	SET(CMAKE_SHARED_LIBRARY_LINK_CXX_FLAGS "-static")
endif()

set(CMAKE_VERBOSE_MAKEFILE ON)

aux_source_directory(. srcs)
add_executable(fs ${srcs})

if(MSYS OR MINGW)
	target_link_libraries(fs)
else()
	target_link_libraries(fs PUBLIC tbb)
endif()

Arguments.h

#pragma once

#include <string>
#include <any>
#include <functional>
#include <optional>

#include "String.h"
#include "Log.h"

class IArgument
{
public:
	virtual ~IArgument() = default;
	virtual void Set(const std::string& value) = 0;
	virtual operator std::string() const = 0;
	virtual std::any Get() = 0;
	virtual std::string GetName() = 0;
	virtual std::string GetDesc() = 0;
};

template <typename T = std::string>
class Argument : public IArgument
{
public:
	using ValueType = T;
	using ValueTypeOpt = std::optional<ValueType>;
	using ConstraintFuncMsg = std::optional<std::string>;
	using ConstraintFunc = std::function<ConstraintFuncMsg(const ValueType&)>;
	using ConvertFunc = std::function<ValueType(const std::string&)>;

	Argument(
		std::string name,
		std::string desc = "",
		ValueTypeOpt defaultValue = std::nullopt,
		ConstraintFunc constraint = [](const auto&) { return std::nullopt; },
		ConvertFunc convert = [](const auto& val) { return val; }) :
		val(std::move(defaultValue)),
		constraint(std::move(constraint)),
		convert(std::move(convert)),
		name(std::move(name)),
		desc(std::move(desc)) {}

	void Set(const std::string& value) override
	{
		auto conv = convert(value);
		auto msg = constraint(conv);
		if (!msg)
		{
			val = ValueTypeOpt(conv);
		}
		else
		{
			ThrowEx(name, ": ", msg.value().c_str());
		}
	}

	[[nodiscard]] std::any Get() override { return val; }

	[[nodiscard]] std::string GetName() override { return name; }

	[[nodiscard]] std::string GetDesc() override { return desc; }

	[[nodiscard]] operator std::string() const override { return name; }

private:
	std::any val;
	ConstraintFunc constraint;
	ConvertFunc convert;
	std::string name;
	std::string desc;
};

class Arguments
{
public:
	void Parse(int argc, char** argv);

	template <typename T>
	void Add(Argument<T>& arg)
	{
		if (args.find(arg.GetName()) != args.end())
		{
			ThrowEx(PrefixString<'-'>(args.at(arg)->GetName()), ": exists");
		}
		args[arg.GetName()] = &arg;
	}

	std::string GetDesc();

	template <typename T = std::string>
	typename Argument<T>::ValueType Value(const std::string& arg)
	{
		try
		{
			return Get<T>(arg).value();
		}
		catch (const std::exception& e)
		{
			ThrowEx(PrefixString<'-'>(args.at(arg)->GetName()), ": ", e.what());
		}
	}

	template <typename T = std::string>
	std::optional<T> Get(const std::string& arg)
	{
		return std::any_cast<typename Argument<T>::ValueTypeOpt>(args.at(arg)->Get());
	}

	IArgument* operator[](const std::string& arg);

private:
	std::unordered_map<std::string, IArgument*> args;
};

Arguments.cpp

#include "Arguments.h"

std::string Arguments::GetDesc()
{
	std::ostringstream ss;
	for (auto& arg : args)
	{
		ss << SuffixString<'\n'>(Combine(SuffixString<'-'>(std::string(4, ' ')),
			SuffixString(arg.first),
			arg.second->GetDesc()));
	}
	return ss.str();
}

void Arguments::Parse(const int argc, char** argv)
{
	if (argc < 3 || (argc & 1) == 0)
	{
		ThrowEx(SuffixString(argv[0]), " [options]\n", GetDesc());
	}
	for (auto i = 1; i < argc; i += 2)
	{
		if (argv[i][0] != '-' || args.find(argv[i] + 1) == args.end())
		{
			ThrowEx(argv[i], ": Option not found");
		}
		args.at(argv[i] + 1)->Set(argv[i + 1]);
	}
}

IArgument* Arguments::operator[](const std::string& arg)
{
	return args.at(arg);
}

Combine.h

#pragma once

#include <string>
#include <sstream>

template <typename ...Args>
std::string Combine(Args&&... args)
{
	std::ostringstream ss;
	(ss << ... << args);
	return ss.str();
}

Convert.h

#pragma once

#include <string>
#include <charconv>

template<typename In = std::string, typename Out = int>
[[nodiscard]] Out Convert(const In& value)
{
	Out res;
	std::from_chars(value.data(), value.data() + value.size(), res);
	return res;
}

File.h

#pragma once

#include <vector>
#include <functional>
#include <filesystem>
#include <functional>

template<typename Element = std::filesystem::directory_entry, typename Container = std::vector<Element>>
[[nodiscard]] Container GetFiles(
	const std::filesystem::path& path,
	std::function<void(Container&, Element)> insertFunc = [](auto& files, const auto& file) { files.push_back(file); })
{
	Container files{};
	for (const auto& file :
		std::filesystem::directory_iterator(path, std::filesystem::directory_options::skip_permission_denied))
	{
		if (file.is_regular_file())
		{
			insertFunc(files, file);
		}
	}
	return files;
}

Log.h

#pragma once

#include <exception>

#include "Combine.h"

#define ToStringFunc(x) #x
#define ToString(x) ToStringFunc(x)

#define Line ToString(__LINE__)

#define ThrowEx(...) throw std::runtime_error(Combine( __FILE__ ": " Line ":\n", __VA_ARGS__))

Preset.h

#pragma once

#include "String.h"
#include "Combine.h"
#include "Unified.h"

const SuffixString Vsync0 = "-vsync 0";

const SuffixString HwCuvid = "-hwaccel cuvid";

const SuffixString X264Cuvid = "-c:v h264_cuvid";
const SuffixString X264Mmal = "-c:v h264_mmal";

const SuffixString FrameRate29_97 = "-framerate 29.97";

const SuffixString Input = Combine("-i ", DoubleQuotes("$$input$$"));
const SuffixString InputPng_d = Combine("-i ", DoubleQuotes("$$input$$" PathSeparator "%d.png"));

const SuffixString NvInput = Combine(HwCuvid, X264Cuvid, Input);

const SuffixString copy = "-c copy";
const SuffixString X264 = "-c:v libx264";
const SuffixString X265 = "-c:v libx265";
const SuffixString X264Nvenc = "-c:v h264_nvenc";
const SuffixString X264Omx = "-c:v h264_omx";
const SuffixString Aac = "-c:a aac";

const SuffixString InputCopy = Combine(Input, copy);

const SuffixString LoudNorm = "-filter:a loudnorm";
const SuffixString ScaleUp60Fps = R"(-filter:v "minterpolate='fps=60:mi_mode=mci:mc_mode=aobmc:me_mode=bidir:me=epzs:vsbmc=1:scd=fdiff'")";
const SuffixString Image2 = "-f image2";

const SuffixString Size100X100 = "-s 100x100";
const SuffixString Size720p = "-s 1280x720";

const SuffixString Resize720p = "-resize 1280x720";

const SuffixString Ac2 = "-ac 2";

const SuffixString PresetLossLessHp = "-preset losslesshp";
const SuffixString PresetUltraFast = "-preset ultrafast";
const SuffixString PresetSlower = "-preset slower";
const SuffixString PresetVerySlow = "-preset veryslow";
const SuffixString PresetPlacebo = "-preset placebo";

const SuffixString TuneFilm = "-tune film";

const SuffixString AudioBitrate128k = "-b:a 128k";

const SuffixString AudioS16 = "-sample_fmt s16";

const SuffixString Qp0 = "-qp 0";

const SuffixString Crf14 = "-crf 14";
const SuffixString Crf15 = "-crf 15";
const SuffixString Crf17 = "-crf 17";
const SuffixString Crf19 = "-crf 17";

const SuffixString Yuv420p = "-pix_fmt yuv420p";
const SuffixString Yuv420p10le = "-pix_fmt yuv420p10le";
const SuffixString Yuv444p10le = "-pix_fmt yuv444p10le";

const SuffixString ColorSpaceBt709 = "-colorspace 1";
const SuffixString Bt709 = Combine(ColorSpaceBt709, "-color_primaries 1 -color_trc 1");
const SuffixString Smpte170m = "-colorspace 6 -color_trc 6 -color_primaries 6";

const SuffixString Anima60FpsAvc720pParams = R"(-x264-params "deblock='0:0':keyint=600:min-keyint=1:ref=9:qcomp=0.7:rc-lookahead=180:aq-strength=0.9:merange=16:me=tesa:psy-rd='0:0.20':no-fast-pskip=1")";
const SuffixString Anima60FpsAvcParams = R"(-x264-params "mbtree=1:aq-mode=3:psy-rd='0.6:0.15':aq-strength=0.8:rc-lookahead=180:qcomp=0.75:deblock='-1:-1':keyint=600:min-keyint=1:bframes=8:ref=13:me=tesa:no-fast-pskip=1")";
const SuffixString Anima60FpsHevcParams = R"(-x265-params "deblock='-1:-1':ctu=32:qg-size=8:pbratio=1.2:cbqpoffs=-2:crqpoffs=-2:no-sao=1:me=3:subme=5:merange=38:b-intra=1:limit-tu=4:no-amp=1:ref=4:weightb=1:keyint=600:min-keyint=1:bframes=6:aq-mode=1:aq-strength=0.8:rd=5:psy-rd=2.0:psy-rdoq=1.0:rdoq-level=2:no-open-gop=1:rc-lookahead=180:scenecut=40:qcomp=0.65:no-strong-intra-smoothing=1:")";

const SuffixString AvcAudioComp = Combine(Aac, AudioBitrate128k/*, AudioS16*/);

const SuffixString AvcLossLess = Combine(X264, PresetUltraFast, Qp0);
const SuffixString AvcLossLessP10 = Combine(AvcLossLess, Yuv420p10le);
const SuffixString AvcVisuallyLossLess = Combine(X264, Crf17);
const SuffixString AvcVisuallyLossLessP10 = Combine(AvcVisuallyLossLess, Yuv420p10le);
const SuffixString NvencAvcLossLess = Combine(X264Nvenc, PresetLossLessHp, Qp0);
const SuffixString NvencAvc720pComp = Combine(X264Nvenc, PresetLossLessHp, Qp0);
const SuffixString AnimaAvc720pComp = Combine(X264, PresetVerySlow, Crf19, Yuv420p, TuneFilm, Anima60FpsAvc720pParams);
const SuffixString AnimaAvcComp = Combine(X264, PresetVerySlow, Crf15, Yuv420p10le, Anima60FpsAvcParams);
const SuffixString AnimaAvcCompYuv444 = Combine(X264, PresetVerySlow, Crf15, Yuv444p10le, Anima60FpsAvcParams);
const SuffixString AnimaHevcComp = Combine(X265, PresetSlower, Crf14, Yuv420p10le, Anima60FpsHevcParams);

const auto Output = DoubleQuotes("$$output$$");
const auto OutputJpg = DoubleQuotes("$$output$$.jpg");
const auto OutputPng = DoubleQuotes("$$output$$.png");
const auto OutputPng_d = DoubleQuotes("$$input$$" PathSeparator "%d.png");
const auto OutputMp4 = DoubleQuotes("$$output$$.mp4");

Semaphore.h

#pragma once

#include <condition_variable>
#include <mutex>

class Semaphore
{
public:
	Semaphore(int count);

	void Release();

	void WaitOne();

private:
	std::mutex mtx;
	std::condition_variable cv;
	int count;
};

Semaphore.cpp

#include "Semaphore.h"

Semaphore::Semaphore(const int count) : count(count) {}

void Semaphore::Release()
{
	std::unique_lock<std::mutex> lock(mtx);
	count++;
	cv.notify_one();
}

void Semaphore::WaitOne()
{
	std::unique_lock<std::mutex> lock(mtx);
	while (count == 0) cv.wait(lock);
	count--;
}

String.h

#pragma once

#include <string>
#include <sstream>

template <char Suffix = ' '>
class SuffixString : public std::string
{
public:
	template <typename ...T>
	SuffixString(T&&... arg) : std::string(std::forward<T>(arg)...) { }

	friend std::ostream& operator<<(std::ostream& ss, const SuffixString<Suffix>& cs)
	{
		return ss << static_cast<std::string>(cs) << Suffix;
	}
};

template <char Prefix = ' '>
class PrefixString : public std::string
{
public:
	template <typename ...T>
	PrefixString(T&&... arg) : std::string(std::forward<T>(arg)...) { }

	friend std::ostream& operator<<(std::ostream& ss, const PrefixString<Prefix>& cs)
	{
		return ss << Prefix << static_cast<std::string>(cs);
	}
};

template <char Prefix = ' ', char Suffix = ' '>
class PrefixSuffixString : public std::string
{
public:
	template <typename ...T>
	PrefixSuffixString(T&&... arg) : std::string(std::forward<T>(arg)...) { }

	friend std::ostream& operator<<(std::ostream& ss, const PrefixSuffixString<Prefix, Suffix>& cs)
	{
		return ss << Prefix << static_cast<std::string>(cs) << Suffix;
	}
};

using DoubleQuotes = PrefixSuffixString<'\"', '\"'>;

Unified.h

#pragma once

#if (_WIN32 || _WIN64)

#define PathSeparator "\\"

#else

#define PathSeparator "/"

#endif

main.cpp

#include <cstdio>
#include <cstdlib>
#include <string>
#include <unordered_map>
#include <filesystem>
#include <regex>
#include <functional>
#include <optional>
#include <thread>
#include <chrono>
#include <exception>
#include <execution>
#include <mutex>

#include "Arguments.h"
#include "Convert.h"
#include "File.h"
#include "Semaphore.h"
#include "Preset.h"

std::unordered_map<std::string, std::string> Preset
{
	{"anima,avc,720p,comp,colorspace=bt709", Combine(InputCopy, Size720p, AvcAudioComp, AnimaAvc720pComp, ColorSpaceBt709, Output)},

	{"anima,avc,comp", Combine(InputCopy, AnimaAvcComp, Output)},
	{"anima,avc,comp,colorspace=bt709", Combine(InputCopy, AnimaAvcComp, ColorSpaceBt709, Output)},
	{"anima,avc,comp,bt709", Combine(InputCopy, AnimaAvcComp, Bt709, Output)},

	{"anima,hevc,comp", Combine(InputCopy, AnimaHevcComp, Output)},
	{"anima,hevc,comp,colorspace=bt709", Combine(InputCopy, AnimaHevcComp, ColorSpaceBt709, Output)},
	{"anima,hevc,comp,bt709", Combine(InputCopy, AnimaHevcComp, Bt709, Output)},

	{"anima,upto60fps,avc,ll", Combine(InputCopy, ScaleUp60Fps, AvcLossLessP10, Output)},
	{"anima,upto60fps,avc,ll,colorspace=bt709", Combine(InputCopy, ScaleUp60Fps, AvcLossLessP10, ColorSpaceBt709, Output)},
	{"anima,upto60fps,avc,ll,bt709", Combine(InputCopy, ScaleUp60Fps, AvcLossLessP10, Bt709, Output)},

	{"anima,upto60fps,avc,comp", Combine(InputCopy, ScaleUp60Fps, AnimaAvcComp, Output)},
	{"anima,upto60fps,avc,comp,colorspace=bt709", Combine(InputCopy, ScaleUp60Fps, AnimaAvcComp, ColorSpaceBt709, Output)},
	{"anima,upto60fps,avc,comp,bt709", Combine(InputCopy, ScaleUp60Fps, AnimaAvcComp, Bt709, Output)},

	{"anima,upto60fps,avc,comp,444p10,colorspace=bt709", Combine(InputCopy, ScaleUp60Fps, AnimaAvcCompYuv444, ColorSpaceBt709, Output)},

	{"avc,ll", Combine(InputCopy, AvcLossLess, Output)},
	{"avc,vll", Combine(InputCopy, AvcVisuallyLossLess, Output)},
	{"avc,vll,p10", Combine(InputCopy, AvcVisuallyLossLessP10, Output)},
	{"avc,vll,p10,bt709", Combine(InputCopy, AvcVisuallyLossLessP10, Bt709, Output)},
	{"avc,vll,p10,smpte170m", Combine(InputCopy, AvcVisuallyLossLessP10, Smpte170m, Output)},

	{"avc", Combine(Input, X264, Output)},
	{"avc,bt709", Combine(Input, X264, Bt709, Output)},
	{"avc,placebo", Combine(Input, X264, PresetPlacebo, Output)},

	{"avc,720p,nvenc,colorspace=bt709", Combine(InputCopy, Size720p, AvcAudioComp, X264Nvenc, Yuv420p, ColorSpaceBt709, Output)},

	{"nv,avc", Combine(NvInput, X264Nvenc, Output)},
	{"nv,avc,ll", Combine(NvInput, NvencAvcLossLess, Output)},

	{"rp,avc", Combine(X264Mmal, Input, X264Omx, Output)},

	{"pic,jpg", Combine(Input, OutputJpg)},
	{"pic,png", Combine(Input, OutputPng)},

	{"pic,png,resize100x100", Combine(Input, Size100X100, OutputPng)},

	{"vid,mp4", Combine(Input, OutputMp4)},

	{"vid2png%d", Combine(Input, Image2, OutputPng_d)},

	{"png%d2mp4,29.97fps,p10le", Combine(FrameRate29_97, InputPng_d, Yuv420p10le, Output)},

	{"loudnorm", Combine(InputCopy, LoudNorm, Output)},

	{"i", Combine(Input)},

	{"easydecode", Combine(Vsync0, HwCuvid, X264Cuvid, Resize720p, Input, X264Nvenc, Ac2, OutputMp4)}
};

[[nodiscard]] std::string PresetDesc()
{
	std::ostringstream ss;
	for (auto& preset : Preset)
	{
		ss << SuffixString<'\n'>(Combine(std::string(8, ' '), preset.first, " => ", preset.second));
	}
	return ss.str();
}

int main(int argc, char* argv[])
{
#define InvalidArgument(v) Argument<>::ConstraintFuncMsg{ Combine(v, ": Invalid argument") }
#define InvalidArgumentFunc(func) [](const auto& v) { return (func) ? std::nullopt : InvalidArgument(v); }

	try
	{
		Arguments args{};

		Argument input("i", "input");
		Argument output("o", "output");
		Argument log("l", "log path");
		Argument<int> thread(
			"t",
			"thread",
			1,
			{ InvalidArgumentFunc(v > 0) },
			Convert<std::string, int>);
		Argument custom("custom", "custom");
		Argument mode(
			"mode",
			"[(f)|d] file/directory",
			{ "f" },
			{ InvalidArgumentFunc(v == "f" || v == "d") });
		Argument<bool> move(
			"move",
			"[(y)|n] move when done",
			{ true },
			{ InvalidArgumentFunc(true) },
			{ [](const auto& v) { return !(v == "n"); } });
		Argument call(
			"call",
			"(ffmpeg) call ffmpeg",
			{ "ffmpeg" });
		Argument inputExtension(
			"ie",
			"input extension",
			{ "" });
		Argument outputExtension(
			"oe",
			"output extension",
			{ "" });
		Argument preset(
			"p",
			Combine("preset\n", PresetDesc()),
			{},
			{ InvalidArgumentFunc(Preset.find(v) != Preset.end()) });

		args.Add(input);
		args.Add(output);
		args.Add(log);
		args.Add(thread);
		args.Add(custom);
		args.Add(mode);
		args.Add(move);
		args.Add(inputExtension);
		args.Add(outputExtension);
		args.Add(call);
		args.Add(preset);

		args.Parse(argc, argv);

		const std::regex inputRe(R"(\${3}input.?\${3})");
		const std::regex outputRe(R"(\${3}output\${3})");
		const std::regex inputExtensionRe(R"("\${3}output\${3}")");
		const std::regex outputExtensionRe(R"("\${3}output\${3}.*?")");

		const auto extendPresetCmd = std::regex_replace(
			std::regex_replace(
				Combine(SuffixString(args.Value(call)), args.Get(custom) ? args.Value(custom) : Preset[args.Value(preset)]),
				outputExtensionRe,
				Combine(SuffixString(args.Value(outputExtension)), "

amp;")),
inputExtensionRe,
Combine(SuffixString(args.Value(inputExtension)), "


amp;"));

const auto inputPath = std::filesystem::path(args.Value(input));
const auto outputPath = args.Get(output) ? std::filesystem::path(args.Value(output)) : inputPath / "done";

create_directory(outputPath);

if (args.Value(mode) == "f")
{
const auto moveWhenDone = args.Value<decltype(move)::ValueType>(move);
const auto threadNum = args.Value<decltype(thread)::ValueType>(thread);

const auto rawPath = inputPath / "raw";
const auto logPath = args.Get(log) ? std::filesystem::path(args.Value(log)) : inputPath / "log";

if (moveWhenDone) create_directory(rawPath);
if (threadNum != 1) create_directory(logPath);

auto files = GetFiles(inputPath);

std::stable_sort(std::execution::par_unseq, files.begin(), files.end());

std::mutex idMtx{};
Semaphore cs(threadNum);

auto ffmpeg = [&, count = 0](const auto& file) mutable
{
cs.WaitOne();
idMtx.lock();
auto id = count++;
idMtx.unlock();

const auto currFile = inputPath / file.path().filename();
const auto cmd =
Combine(
std::regex_replace(
std::regex_replace(
extendPresetCmd,
inputRe,
currFile.string()),
outputRe,
(outputPath / file.path().filename()).string()),
threadNum == 1 ? "" : Combine(" >", DoubleQuotes((logPath / Combine("log.", id)).string()), " 2>&1"));
printf("\n>>> %s\n\n", cmd.c_str());
system(cmd.c_str());

if (moveWhenDone)
{
try
{
rename(currFile, rawPath / file.path().filename());
}
catch (...)
{
using namespace std::chrono_literals;
std::this_thread::sleep_for(+1s);
rename(currFile, rawPath / file.path().filename());
}
}
cs.Release();
};

if (threadNum == 1)
{
if (args.Value<decltype(move)::ValueType>(move))
{
for (; !files.empty();
files = GetFiles(inputPath),
std::stable_sort(std::execution::par_unseq, files.begin(), files.end()))
{
ffmpeg(files[0]);
}
}
else
{
std::for_each(files.begin(), files.end(), ffmpeg);
}
}
else
{
std::for_each(std::execution::par, files.begin(), files.end(), ffmpeg);
}
}
else
{
const auto cmd =
std::regex_replace(
std::regex_replace(
extendPresetCmd,
inputRe,
inputPath.string()),
outputRe,
outputPath.string());
printf("\n>>> %s\n\n", cmd、 Ccmd.c_str());
system(cmd.c_str());
}
}
catch (const std::exception& e)
{
fputs(e.what(), stderr), exit(EXIT_FAILURE);
}
}
// g++<10 may need tbb
// g++ fs.cpp -o fs -std=c++17 -ltbb -O2
 

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注