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