From b309bec1cbe5fea9f39a0ace27d1c5e315165635 Mon Sep 17 00:00:00 2001 From: k4yt3x Date: Fri, 1 Nov 2024 22:20:32 -0400 Subject: [PATCH] feat(video2x): rewritten the CLI with C++ --- .gitmodules | 3 + CMakeLists.txt | 94 ++++--- include/getopt.h | 32 --- src/getopt.c | 249 ----------------- src/video2x.c | 599 --------------------------------------- src/video2x.cpp | 708 +++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 771 insertions(+), 914 deletions(-) delete mode 100644 include/getopt.h delete mode 100644 src/getopt.c delete mode 100644 src/video2x.c create mode 100644 src/video2x.cpp diff --git a/.gitmodules b/.gitmodules index 963f9c9..857e1f1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,6 @@ [submodule "third_party/opencv"] path = third_party/opencv url = https://github.com/opencv/opencv.git +[submodule "third_party/boost"] + path = third_party/boost + url = https://github.com/boostorg/boost.git diff --git a/CMakeLists.txt b/CMakeLists.txt index f03ec58..acfdabf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,9 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(video2x VERSION 6.0.0 LANGUAGES C CXX) - -# Set the C standard -set(CMAKE_C_STANDARD 11) -set(CMAKE_C_STANDARD_REQUIRED ON) +project(video2x VERSION 6.0.0 LANGUAGES CXX) # Set the C++ standard set(CMAKE_CXX_STANDARD 17) @@ -18,10 +14,8 @@ endif() # Set the default optimization flags for Release builds if(CMAKE_BUILD_TYPE STREQUAL "Release") if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") - set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} /Ox /GL /LTCG /MD /DNDEBUG") set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /Ox /GL /LTCG /MD /DNDEBUG") elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") - set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -O3 -march=native -flto") set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3 -march=native -flto") set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} -s") set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} -s") @@ -38,9 +32,10 @@ endif() # Build options option(BUILD_SHARED_LIBS "Build libvideo2x as a shared library" ON) option(BUILD_VIDEO2X_CLI "Build the video2x executable" ON) -option(USE_SYSTEM_SPDLOG "Use system spdlog library" ON) option(USE_SYSTEM_OPENCV "Use system OpenCV library" ON) option(USE_SYSTEM_NCNN "Use system ncnn library" ON) +option(USE_SYSTEM_SPDLOG "Use system spdlog library" ON) +option(USE_SYSTEM_BOOST "Use system Boost library" ON) # Generate the version header file configure_file( @@ -53,16 +48,6 @@ configure_file( set(ALL_INCLUDE_DIRS) set(ALL_LIBRARIES) -# spdlog -if (USE_SYSTEM_SPDLOG) - find_package(spdlog REQUIRED) - list(APPEND ALL_INCLUDE_DIRS ${spdlog_INCLUDE_DIRS}) - list(APPEND ALL_LIBRARIES spdlog::spdlog) -else() - add_subdirectory(third_party/spdlog) - list(APPEND ALL_LIBRARIES spdlog::spdlog_header_only) -endif() - # Platform-specific dependencies if(WIN32) # Define base paths for FFmpeg and ncnn @@ -71,7 +56,7 @@ if(WIN32) set(OPENCV_BASE_PATH ${PROJECT_SOURCE_DIR}/third_party/opencv-shared) # FFmpeg - list(APPEND ALL_LIBRARIES + set(FFMPEG_LIB ${FFMPEG_BASE_PATH}/lib/avcodec.lib ${FFMPEG_BASE_PATH}/lib/avdevice.lib ${FFMPEG_BASE_PATH}/lib/avfilter.lib @@ -79,6 +64,7 @@ if(WIN32) ${FFMPEG_BASE_PATH}/lib/avutil.lib ${FFMPEG_BASE_PATH}/lib/swscale.lib ) + list(APPEND ALL_LIBRARIES ${FFMPEG_LIB}) list(APPEND ALL_INCLUDE_DIRS ${FFMPEG_BASE_PATH}/include) # ncnn @@ -99,7 +85,7 @@ if(WIN32) list(APPEND ALL_INCLUDE_DIRS ${NCNN_BASE_PATH}/include/ncnn) # OpenCV - list(APPEND ALL_LIBRARIES ${OPENCV_BASE_PATH}/build/x64/vc16/lib/opencv_world4100.lib) + # list(APPEND ALL_LIBRARIES ${OPENCV_BASE_PATH}/build/x64/vc16/lib/opencv_world4100.lib) list(APPEND ALL_INCLUDE_DIRS ${OPENCV_BASE_PATH}/build/include) else() # FFmpeg @@ -114,11 +100,13 @@ else() ) # Loop through each package to find and collect include dirs and libraries + set(FFMPEG_LIB) foreach(PKG ${FFMPEG_REQUIRED_PKGS}) pkg_check_modules(${PKG} REQUIRED ${PKG}) list(APPEND ALL_INCLUDE_DIRS ${${PKG}_INCLUDE_DIRS}) - list(APPEND ALL_LIBRARIES ${${PKG}_LIBRARIES}) + list(APPEND FFMPEG_LIB ${${PKG}_LIBRARIES}) endforeach() + list(APPEND ALL_LIBRARIES ${FFMPEG_LIB}) # OpenCV if (USE_SYSTEM_OPENCV) @@ -165,7 +153,7 @@ else() ${PROJECT_SOURCE_DIR}/third_party/opencv/modules/core/include ${PROJECT_SOURCE_DIR}/third_party/opencv/modules/videoio/include ) - list(APPEND ALL_LIBRARIES opencv_core opencv_videoio) + # list(APPEND ALL_LIBRARIES opencv_core opencv_videoio) endif() # USE_SYSTEM_OPENCV endif() # WIN32 @@ -268,6 +256,31 @@ else() add_subdirectory(third_party/ncnn) endif() +# spdlog +if (USE_SYSTEM_SPDLOG) + find_package(spdlog REQUIRED) + list(APPEND ALL_INCLUDE_DIRS ${spdlog_INCLUDE_DIRS}) + set(SPDLOG_LIB spdlog::spdlog) +else() + add_subdirectory(third_party/spdlog) + set(SPDLOG_LIB spdlog::spdlog_header_only) +endif() +list(APPEND ALL_LIBRARIES ${SPDLOG_LIB}) + +# Boost +if (USE_SYSTEM_BOOST) + find_package(Boost REQUIRED COMPONENTS program_options) + list(APPEND ALL_INCLUDE_DIRS ${Boost_INCLUDE_DIRS}) +else() + option(Boost_USE_STATIC_LIBS "" ON) + option(Boost_USE_STATIC_RUNTIME "" ON) + option(Boost_COMPONENTS "program_options") + + add_subdirectory(third_party/boost) + include_directories(${PROJECT_SOURCE_DIR}/third_party/boost/libs/program_options/include) +endif() +set(BOOST_LIB Boost::program_options) + # Include ExternalProject module include(ExternalProject) @@ -288,11 +301,17 @@ ExternalProject_Add( list(REMOVE_DUPLICATES ALL_INCLUDE_DIRS) list(REMOVE_DUPLICATES ALL_LIBRARIES) -# Add all source files for libvideo2x -file(GLOB LIBVIDEO2X_SOURCES src/*.cpp) - # Create the shared library 'libvideo2x' -add_library(libvideo2x ${LIBVIDEO2X_SOURCES}) +add_library(libvideo2x + src/conversions.cpp + src/decoder.cpp + src/encoder.cpp + src/fsutils.cpp + src/libplacebo_filter.cpp + src/libplacebo.cpp + src/libvideo2x.cpp + src/realesrgan_filter.cpp +) target_compile_definitions(libvideo2x PRIVATE LIBVIDEO2X_EXPORTS) if(WIN32) set_target_properties(libvideo2x PROPERTIES OUTPUT_NAME libvideo2x) @@ -321,13 +340,13 @@ target_compile_options(libvideo2x PRIVATE # Define the path to the built libresrgan-ncnn-vulkan library if(WIN32) - set(REALESRGAN_LIB ${CMAKE_BINARY_DIR}/realesrgan_install/lib/librealesrgan-ncnn-vulkan.lib) + list(APPEND ALL_LIBRARIES ${CMAKE_BINARY_DIR}/realesrgan_install/lib/librealesrgan-ncnn-vulkan.lib) else() - set(REALESRGAN_LIB ${CMAKE_BINARY_DIR}/realesrgan_install/lib/librealesrgan-ncnn-vulkan.so) + list(APPEND ALL_LIBRARIES ${CMAKE_BINARY_DIR}/realesrgan_install/lib/librealesrgan-ncnn-vulkan.so) endif() # Link the shared library with the dependencies -target_link_libraries(libvideo2x PRIVATE ${ALL_LIBRARIES} ${REALESRGAN_LIB}) +target_link_libraries(libvideo2x PRIVATE ${ALL_LIBRARIES}) if(NOT WIN32) if (USE_SYSTEM_NCNN) @@ -339,7 +358,7 @@ endif() # Create the executable 'video2x' if (BUILD_VIDEO2X_CLI) - add_executable(video2x src/video2x.c src/getopt.c) + add_executable(video2x src/video2x.cpp) set_target_properties(video2x PROPERTIES OUTPUT_NAME video2x) # Include directories for the executable @@ -353,7 +372,7 @@ if (BUILD_VIDEO2X_CLI) target_compile_options(video2x PRIVATE $<$:-g -DDEBUG>) # Link the executable with the shared library - target_link_libraries(video2x PRIVATE ${ALL_LIBRARIES} libvideo2x) + target_link_libraries(video2x PRIVATE libvideo2x ${FFMPEG_LIB} ${SPDLOG_LIB} ${BOOST_LIB}) endif() # Define the default installation directories @@ -427,14 +446,21 @@ if(WIN32) GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE ) - install(FILES ${OPENCV_BASE_PATH}/build/x64/vc16/bin/opencv_world4100.dll - ${OPENCV_BASE_PATH}/build/x64/vc16/bin/opencv_videoio_msmf4100_64.dll + # install(FILES ${OPENCV_BASE_PATH}/build/x64/vc16/bin/opencv_world4100.dll + # ${OPENCV_BASE_PATH}/build/x64/vc16/bin/opencv_videoio_msmf4100_64.dll + # DESTINATION ${INSTALL_BIN_DESTINATION} + # PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE + # GROUP_READ GROUP_EXECUTE + # WORLD_READ WORLD_EXECUTE + # ) + install(FILES ${NCNN_BASE_PATH}/bin/ncnn.dll DESTINATION ${INSTALL_BIN_DESTINATION} PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE ) - install(FILES ${NCNN_BASE_PATH}/bin/ncnn.dll + set(BOOST_PROGRAM_OPTIONS_PATH ${CMAKE_BINARY_DIR}/third_party/boost/libs/program_options/${CMAKE_BUILD_TYPE}) + install(FILES ${BOOST_PROGRAM_OPTIONS_PATH}/boost_program_options-vc143-mt-x64-1_86.dll DESTINATION ${INSTALL_BIN_DESTINATION} PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE diff --git a/include/getopt.h b/include/getopt.h deleted file mode 100644 index be8b5ac..0000000 --- a/include/getopt.h +++ /dev/null @@ -1,32 +0,0 @@ -#ifndef __GETOPT_H__ -#define __GETOPT_H__ - -#ifdef __cplusplus -extern "C" { -#endif - -extern int opterr; /* if error message should be printed */ -extern int optind; /* index into parent argv vector */ -extern int optopt; /* character checked for validity */ -extern int optreset; /* reset getopt */ -extern char *optarg; /* argument associated with option */ - -struct option { - const char *name; - int has_arg; - int *flag; - int val; -}; - -#define no_argument 0 -#define required_argument 1 -#define optional_argument 2 - -// int getopt(int, char **, const char *); -int getopt_long(int, char **, const char *, const struct option *, int *); - -#ifdef __cplusplus -} -#endif - -#endif /* __GETOPT_H__ */ diff --git a/src/getopt.c b/src/getopt.c deleted file mode 100644 index 8924c93..0000000 --- a/src/getopt.c +++ /dev/null @@ -1,249 +0,0 @@ -/* - * Copyright (c) 1987, 1993, 1994, 1996 - * The Regents of the University of California. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 3. Neither the names of the copyright holders nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS - * IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, - * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ -#include -#include -#include -#include - -#include "getopt.h" - -/* -extern int opterr; -extern int optind; -extern int optopt; -extern int optreset; -extern char *optarg; -*/ - -int opterr = 1; /* if error message should be printed */ -int optind = 1; /* index into parent argv vector */ -int optopt = 0; /* character checked for validity */ -int optreset = 0; /* reset getopt */ -char *optarg = NULL; /* argument associated with option */ - -#ifndef __P -#define __P(x) x -#endif -#define _DIAGASSERT(x) assert(x) - -static char *__progname __P((char *)); -int getopt_internal __P((int, char *const *, const char *)); - -static char *__progname(char *nargv0) { - char *tmp; - - _DIAGASSERT(nargv0 != NULL); - - tmp = strrchr(nargv0, '/'); - if (tmp) { - tmp++; - } else { - tmp = nargv0; - } - return (tmp); -} - -#define BADCH (int)'?' -#define BADARG (int)':' -#define EMSG "" - -/* - * getopt -- - * Parse argc/argv argument vector. - */ -int getopt_internal(int nargc, char *const *nargv, const char *ostr) { - static char *place = EMSG; /* option letter processing */ - char *oli; /* option letter list index */ - - _DIAGASSERT(nargv != NULL); - _DIAGASSERT(ostr != NULL); - - if (optreset || !*place) { /* update scanning pointer */ - optreset = 0; - if (optind >= nargc || *(place = nargv[optind]) != '-') { - place = EMSG; - return (-1); - } - if (place[1] && *++place == '-') { /* found "--" */ - /* ++optind; */ - place = EMSG; - return (-2); - } - } /* option letter okay? */ - if ((optopt = (int)*place++) == (int)':' || !(oli = strchr(ostr, optopt))) { - /* - * if the user didn't specify '-' as an option, - * assume it means -1. - */ - if (optopt == (int)'-') { - return (-1); - } - if (!*place) { - ++optind; - } - if (opterr && *ostr != ':') { - (void)fprintf(stderr, "%s: illegal option -- %c\n", __progname(nargv[0]), optopt); - } - return (BADCH); - } - if (*++oli != ':') { /* don't need argument */ - optarg = NULL; - if (!*place) { - ++optind; - } - } else { /* need an argument */ - if (*place) { /* no white space */ - optarg = place; - } else if (nargc <= ++optind) { /* no arg */ - place = EMSG; - if ((opterr) && (*ostr != ':')) { - (void)fprintf( - stderr, "%s: option requires an argument -- %c\n", __progname(nargv[0]), optopt - ); - } - return (BADARG); - } else { /* white space */ - optarg = nargv[optind]; - } - place = EMSG; - ++optind; - } - return (optopt); /* dump back option letter */ -} - -#if 0 -/* - * getopt -- - * Parse argc/argv argument vector. - */ -int -getopt2(nargc, nargv, ostr) - int nargc; - char * const *nargv; - const char *ostr; -{ - int retval; - - if ((retval = getopt_internal(nargc, nargv, ostr)) == -2) { - retval = -1; - ++optind; - } - return(retval); -} -#endif - -/* - * getopt_long -- - * Parse argc/argv argument vector. - */ -int getopt_long( - int nargc, - char **nargv, - const char *options, - const struct option *long_options, - int *index -) { - int retval; - - _DIAGASSERT(nargv != NULL); - _DIAGASSERT(options != NULL); - _DIAGASSERT(long_options != NULL); - /* index may be NULL */ - - if ((retval = getopt_internal(nargc, nargv, options)) == -2) { - char *current_argv = nargv[optind++] + 2, *has_equal; - int i, match = -1; - size_t current_argv_len; - - if (*current_argv == '\0') { - return (-1); - } - if ((has_equal = strchr(current_argv, '=')) != NULL) { - current_argv_len = (size_t)(has_equal - current_argv); - has_equal++; - } else { - current_argv_len = strlen(current_argv); - } - - for (i = 0; long_options[i].name; i++) { - if (strncmp(current_argv, long_options[i].name, current_argv_len)) { - continue; - } - - if (strlen(long_options[i].name) == current_argv_len) { - match = i; - break; - } - if (match == -1) { - match = i; - } - } - if (match != -1) { - if (long_options[match].has_arg == required_argument || - long_options[match].has_arg == optional_argument) { - if (has_equal) { - optarg = has_equal; - } else { - optarg = nargv[optind++]; - } - } - if ((long_options[match].has_arg == required_argument) && (optarg == NULL)) { - /* - * Missing argument, leading : - * indicates no error should be generated - */ - if ((opterr) && (*options != ':')) { - (void)fprintf( - stderr, - "%s: option requires an argument -- %s\n", - __progname(nargv[0]), - current_argv - ); - } - return (BADARG); - } - } else { /* No matching argument */ - if ((opterr) && (*options != ':')) { - (void - )fprintf(stderr, "%s: illegal option -- %s\n", __progname(nargv[0]), current_argv); - } - return (BADCH); - } - if (long_options[match].flag) { - *long_options[match].flag = long_options[match].val; - retval = 0; - } else { - retval = long_options[match].val; - } - if (index) { - *index = match; - } - } - return (retval); -} diff --git a/src/video2x.c b/src/video2x.c deleted file mode 100644 index 5f08da6..0000000 --- a/src/video2x.c +++ /dev/null @@ -1,599 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#ifdef _WIN32 -#include -#else -#include -#include -#include -#endif - -#include -#include -#include - -#include -#include - -#include "getopt.h" - -// Define command line options -static struct option long_options[] = { - {"loglevel", required_argument, NULL, 0}, - {"noprogress", no_argument, NULL, 0}, - {"version", no_argument, NULL, 'v'}, - {"help", no_argument, NULL, 0}, - - // General options - {"input", required_argument, NULL, 'i'}, - {"output", required_argument, NULL, 'o'}, - {"filter", required_argument, NULL, 'f'}, - {"hwaccel", required_argument, NULL, 'a'}, - {"nocopystreams", no_argument, NULL, 0}, - {"benchmark", no_argument, NULL, 0}, - - // Encoder options - {"codec", required_argument, NULL, 'c'}, - {"preset", required_argument, NULL, 'p'}, - {"pixfmt", required_argument, NULL, 'x'}, - {"bitrate", required_argument, NULL, 'b'}, - {"crf", required_argument, NULL, 'q'}, - - // libplacebo options - {"shader", required_argument, NULL, 's'}, - {"width", required_argument, NULL, 'w'}, - {"height", required_argument, NULL, 'h'}, - - // RealESRGAN options - {"gpuid", required_argument, NULL, 'g'}, - {"model", required_argument, NULL, 'm'}, - {"scale", required_argument, NULL, 'r'}, - {0, 0, 0, 0} -}; - -// List of valid RealESRGAN models -const char *valid_realesrgan_models[] = { - "realesrgan-plus", - "realesrgan-plus-anime", - "realesr-animevideov3", -}; - -// Indicate if a newline needs to be printed before the next output -bool newline_required = false; - -// Structure to hold parsed arguments -struct arguments { - // General options - const char *loglevel; - bool noprogress; - const char *in_fname; - const char *out_fname; - const char *filter_type; - const char *hwaccel; - bool nocopystreams; - bool benchmark; - - // Encoder options - const char *codec; - const char *pix_fmt; - const char *preset; - int64_t bitrate; - float crf; - - // libplacebo options - const char *shader_path; - int out_width; - int out_height; - - // RealESRGAN options - int gpuid; - const char *model; - int scaling_factor; -}; - -struct ProcessVideoThreadArguments { - struct arguments *arguments; - enum AVHWDeviceType hw_device_type; - struct FilterConfig *filter_config; - struct EncoderConfig *encoder_config; - struct VideoProcessingContext *proc_ctx; -}; - -// Set UNIX terminal input to non-blocking mode -#ifndef _WIN32 -void set_nonblocking_input(bool enable) { - static struct termios oldt, newt; - if (enable) { - tcgetattr(STDIN_FILENO, &oldt); - newt = oldt; - newt.c_lflag &= ~(tcflag_t)(ICANON | ECHO); - tcsetattr(STDIN_FILENO, TCSANOW, &newt); - fcntl(STDIN_FILENO, F_SETFL, O_NONBLOCK); - } else { - tcsetattr(STDIN_FILENO, TCSANOW, &oldt); - fcntl(STDIN_FILENO, F_SETFL, 0); - } -} -#endif - -// Newline-safe log callback for FFmpeg -void newline_safe_ffmpeg_log_callback(void *ptr, int level, const char *fmt, va_list vl) { - if (level <= av_log_get_level() && newline_required) { - putchar('\n'); - newline_required = false; - } - av_log_default_callback(ptr, level, fmt, vl); -} - -int is_valid_realesrgan_model(const char *model) { - if (!model) { - return 0; - } - for (unsigned long i = 0; - i < sizeof(valid_realesrgan_models) / sizeof(valid_realesrgan_models[0]); - i++) { - if (strcmp(model, valid_realesrgan_models[i]) == 0) { - return 1; - } - } - return 0; -} - -void print_help(void) { - printf("Usage: video2x [OPTIONS]\n"); - printf("\nOptions:\n"); - printf(" --loglevel Set log level \n"); - printf(" (trace, debug, info, warn, error, critical, none)\n"); - printf(" --noprogress Do not display the progress bar\n"); - printf(" -v, --version Print program version\n"); - printf(" -?, --help Display this help page\n"); - printf("\nGeneral Processing Options:\n"); - printf(" -i, --input Input video file path\n"); - printf(" -o, --output Output video file path\n"); - printf(" -f, --filter Filter to use: 'libplacebo' or 'realesrgan'\n"); - printf(" -a, --hwaccel Hardware acceleration method (default: none)\n"); - printf(" --nocopystreams Do not copy audio and subtitle streams\n"); - printf(" --benchmark Discard processed frames and calculate average FPS\n"); - - printf("\nEncoder Options (Optional):\n"); - printf(" -c, --codec Output codec (default: libx264)\n"); - printf(" -p, --preset Encoder preset (default: slow)\n"); - printf(" -x, --pixfmt Output pixel format (default: auto)\n"); - printf(" -b, --bitrate Bitrate in bits per second (default: 0 (VBR))\n"); - printf(" -q, --crf Constant Rate Factor (default: 20.0)\n"); - - printf("\nlibplacebo Options:\n"); - printf(" -s, --shader Name or path of the GLSL shader file to use \n"); - printf(" (built-in: 'anime4k-a', 'anime4k-b', 'anime4k-c',\n"); - printf(" 'anime4k-a+a', 'anime4k-b+b', 'anime4k-c+a')\n"); - printf(" -w, --width Output width\n"); - printf(" -h, --height Output height\n"); - - printf("\nRealESRGAN Options:\n"); - printf(" -g, --gpuid Vulkan GPU ID (default: 0)\n"); - printf(" -m, --model Name of the model to use\n"); - printf(" -r, --scale Scaling factor (2, 3, or 4)\n"); - - printf("\nExamples Usage:\n"); - printf(" video2x -i in.mp4 -o out.mp4 -f libplacebo -s anime4k-v4-a+a -w 3840 -h 2160\n"); - printf(" video2x -i in.mp4 -o out.mp4 -f realesrgan -m realesr-animevideov3 -r 4\n"); -} - -void parse_arguments(int argc, char **argv, struct arguments *arguments) { - int option_index = 0; - int c; - - // Default argument values - arguments->loglevel = "info"; - arguments->noprogress = false; - arguments->in_fname = NULL; - arguments->out_fname = NULL; - arguments->filter_type = NULL; - arguments->hwaccel = "none"; - arguments->nocopystreams = false; - arguments->benchmark = false; - - // Encoder options - arguments->codec = "libx264"; - arguments->preset = "slow"; - arguments->pix_fmt = NULL; - arguments->bitrate = 0; - arguments->crf = 20.0; - - // libplacebo options - arguments->shader_path = NULL; - arguments->out_width = 0; - arguments->out_height = 0; - - // RealESRGAN options - arguments->gpuid = 0; - arguments->model = NULL; - arguments->scaling_factor = 0; - - while ((c = getopt_long( - argc, argv, "i:o:f:a:c:x:p:b:q:s:w:h:g:m:r:v", long_options, &option_index - )) != -1) { - switch (c) { - case 'i': - arguments->in_fname = optarg; - break; - case 'o': - arguments->out_fname = optarg; - break; - case 'f': - arguments->filter_type = optarg; - break; - case 'a': - arguments->hwaccel = optarg; - break; - case 'c': - arguments->codec = optarg; - break; - case 'x': - arguments->pix_fmt = optarg; - break; - case 'p': - arguments->preset = optarg; - break; - case 'b': - arguments->bitrate = strtoll(optarg, NULL, 10); - if (arguments->bitrate <= 0) { - fprintf(stderr, "Error: Invalid bitrate specified.\n"); - exit(1); - } - break; - case 'q': - arguments->crf = (float)atof(optarg); - if (arguments->crf < 0.0 || arguments->crf > 51.0) { - fprintf(stderr, "Error: CRF must be between 0 and 51.\n"); - exit(1); - } - break; - case 's': - arguments->shader_path = optarg; - break; - case 'w': - arguments->out_width = atoi(optarg); - if (arguments->out_width <= 0) { - fprintf(stderr, "Error: Output width must be greater than 0.\n"); - exit(1); - } - break; - case 'h': - arguments->out_height = atoi(optarg); - if (arguments->out_height <= 0) { - fprintf(stderr, "Error: Output height must be greater than 0.\n"); - exit(1); - } - break; - case 'g': - arguments->gpuid = atoi(optarg); - break; - case 'm': - arguments->model = optarg; - if (!is_valid_realesrgan_model(arguments->model)) { - fprintf( - stderr, - "Error: Invalid model specified. Must be 'realesrgan-plus', " - "'realesrgan-plus-anime', or 'realesr-animevideov3'.\n" - ); - exit(1); - } - break; - case 'r': - arguments->scaling_factor = atoi(optarg); - if (arguments->scaling_factor != 2 && arguments->scaling_factor != 3 && - arguments->scaling_factor != 4) { - fprintf(stderr, "Error: Scaling factor must be 2, 3, or 4.\n"); - exit(1); - } - break; - case 'v': - printf("Video2X version %s\n", LIBVIDEO2X_VERSION_STRING); - exit(0); - case 0: // Long-only options without short equivalents - if (strcmp(long_options[option_index].name, "loglevel") == 0) { - arguments->loglevel = optarg; - } else if (strcmp(long_options[option_index].name, "noprogress") == 0) { - arguments->noprogress = true; - } else if (strcmp(long_options[option_index].name, "help") == 0) { - print_help(); - exit(0); - } else if (strcmp(long_options[option_index].name, "nocopystreams") == 0) { - arguments->nocopystreams = true; - } else if (strcmp(long_options[option_index].name, "benchmark") == 0) { - arguments->benchmark = true; - } - break; - default: - fprintf(stderr, "Invalid options provided.\n"); - exit(1); - } - } - - // Check for required arguments - if (!arguments->in_fname) { - fprintf(stderr, "Error: Input file path is required.\n"); - exit(1); - } - - if (!arguments->out_fname && !arguments->benchmark) { - fprintf(stderr, "Error: Output file path is required.\n"); - exit(1); - } - - if (!arguments->filter_type) { - fprintf(stderr, "Error: Filter type is required (libplacebo or realesrgan).\n"); - exit(1); - } - - if (strcmp(arguments->filter_type, "libplacebo") == 0) { - if (!arguments->shader_path || arguments->out_width == 0 || arguments->out_height == 0) { - fprintf( - stderr, - "Error: For libplacebo, shader name/path (-s), width (-w), " - "and height (-h) are required.\n" - ); - exit(1); - } - } else if (strcmp(arguments->filter_type, "realesrgan") == 0) { - if (arguments->scaling_factor == 0 || !arguments->model) { - fprintf( - stderr, "Error: For realesrgan, scaling factor (-r) and model (-m) are required.\n" - ); - exit(1); - } - } -} - -enum Libvideo2xLogLevel parse_log_level(const char *level_name) { - if (strcmp(level_name, "trace") == 0) { - return LIBVIDEO2X_LOG_LEVEL_TRACE; - } else if (strcmp(level_name, "debug") == 0) { - return LIBVIDEO2X_LOG_LEVEL_DEBUG; - } else if (strcmp(level_name, "info") == 0) { - return LIBVIDEO2X_LOG_LEVEL_INFO; - } else if (strcmp(level_name, "warning") == 0) { - return LIBVIDEO2X_LOG_LEVEL_WARNING; - } else if (strcmp(level_name, "error") == 0) { - return LIBVIDEO2X_LOG_LEVEL_ERROR; - } else if (strcmp(level_name, "critical") == 0) { - return LIBVIDEO2X_LOG_LEVEL_CRITICAL; - } else if (strcmp(level_name, "off") == 0) { - return LIBVIDEO2X_LOG_LEVEL_OFF; - } else { - fprintf(stderr, "Warning: Invalid log level specified. Defaulting to 'info'.\n"); - return LIBVIDEO2X_LOG_LEVEL_INFO; - } -} - -// Wrapper function for video processing thread -int process_video_thread(void *arg) { - struct ProcessVideoThreadArguments *thread_args = (struct ProcessVideoThreadArguments *)arg; - - // Extract individual arguments - enum Libvideo2xLogLevel log_level = parse_log_level(thread_args->arguments->loglevel); - struct arguments *arguments = thread_args->arguments; - enum AVHWDeviceType hw_device_type = thread_args->hw_device_type; - struct FilterConfig *filter_config = thread_args->filter_config; - struct EncoderConfig *encoder_config = thread_args->encoder_config; - struct VideoProcessingContext *proc_ctx = thread_args->proc_ctx; - - // Call the process_video function - int result = process_video( - arguments->in_fname, - arguments->out_fname, - log_level, - arguments->benchmark, - hw_device_type, - filter_config, - encoder_config, - proc_ctx - ); - - proc_ctx->completed = true; - return result; -} - -int main(int argc, char **argv) { - // Print help if no arguments are provided - if (argc < 2) { - print_help(); - return 1; - } - - // Parse command line arguments - struct arguments arguments; - parse_arguments(argc, argv, &arguments); - - // Setup filter configurations based on the parsed arguments - struct FilterConfig filter_config; - if (strcmp(arguments.filter_type, "libplacebo") == 0) { - filter_config.filter_type = FILTER_LIBPLACEBO; - filter_config.config.libplacebo.out_width = arguments.out_width; - filter_config.config.libplacebo.out_height = arguments.out_height; - filter_config.config.libplacebo.shader_path = arguments.shader_path; - } else if (strcmp(arguments.filter_type, "realesrgan") == 0) { - filter_config.filter_type = FILTER_REALESRGAN; - filter_config.config.realesrgan.gpuid = arguments.gpuid; - filter_config.config.realesrgan.tta_mode = 0; - filter_config.config.realesrgan.scaling_factor = arguments.scaling_factor; - filter_config.config.realesrgan.model = arguments.model; - } else { - fprintf(stderr, "Error: Invalid filter type specified.\n"); - return 1; - } - - // Parse codec to AVCodec - const AVCodec *codec = avcodec_find_encoder_by_name(arguments.codec); - if (!codec) { - fprintf(stderr, "Error: Codec '%s' not found.\n", arguments.codec); - return 1; - } - - // Parse pixel format to AVPixelFormat - enum AVPixelFormat pix_fmt = AV_PIX_FMT_NONE; - if (arguments.pix_fmt) { - pix_fmt = av_get_pix_fmt(arguments.pix_fmt); - if (pix_fmt == AV_PIX_FMT_NONE) { - fprintf(stderr, "Error: Invalid pixel format '%s'.\n", arguments.pix_fmt); - return 1; - } - } - - // Setup encoder configuration - struct EncoderConfig encoder_config = { - .out_width = 0, // To be filled by libvideo2x - .out_height = 0, // To be filled by libvideo2x - .copy_streams = !arguments.nocopystreams, - .codec = codec->id, - .pix_fmt = pix_fmt, - .preset = arguments.preset, - .bit_rate = arguments.bitrate, - .crf = arguments.crf, - }; - - // Parse hardware acceleration method - enum AVHWDeviceType hw_device_type = AV_HWDEVICE_TYPE_NONE; - if (strcmp(arguments.hwaccel, "none") != 0) { - hw_device_type = av_hwdevice_find_type_by_name(arguments.hwaccel); - if (hw_device_type == AV_HWDEVICE_TYPE_NONE) { - fprintf(stderr, "Error: Invalid hardware device type '%s'.\n", arguments.hwaccel); - return 1; - } - } - - // Setup struct to store processing context - struct VideoProcessingContext proc_ctx = { - .processed_frames = 0, - .total_frames = 0, - .start_time = time(NULL), - .pause = false, - .abort = false, - .completed = false - }; - - // Create a ThreadArguments struct to hold all the arguments for the thread - struct ProcessVideoThreadArguments thread_args = { - .arguments = &arguments, - .hw_device_type = hw_device_type, - .filter_config = &filter_config, - .encoder_config = &encoder_config, - .proc_ctx = &proc_ctx - }; - - // Register a newline-safe log callback for FFmpeg - // This will ensure that log messages are printed on a new line after the progress bar - av_log_set_callback(newline_safe_ffmpeg_log_callback); - - // Create a thread for video processing - thrd_t processing_thread; - if (thrd_create(&processing_thread, process_video_thread, &thread_args) != thrd_success) { - fprintf(stderr, "Failed to create processing thread\n"); - return 1; - } - printf("Video processing started; press SPACE to pause/resume, 'q' to abort.\n"); - -// Enable non-blocking input -#ifndef _WIN32 - set_nonblocking_input(true); -#endif - - // Main thread loop to display progress and handle input - while (!proc_ctx.completed) { - // Check for key presses - int ch = -1; - - // Check for key press -#ifdef _WIN32 - if (_kbhit()) { - ch = _getch(); - } -#else - ch = getchar(); -#endif - - if (ch == ' ' || ch == '\n') { - // Toggle pause state - proc_ctx.pause = !proc_ctx.pause; - if (proc_ctx.pause) { - printf("\nProcessing paused. Press SPACE to resume, 'q' to abort.\n"); - } else { - printf("Resuming processing...\n"); - } - } else if (ch == 'q' || ch == 'Q') { - // Abort processing - printf("\nAborting processing...\n"); - proc_ctx.abort = true; - newline_required = false; - break; - } - - // Display progress - if (!arguments.noprogress && !proc_ctx.pause && proc_ctx.total_frames > 0) { - printf( - "\rProcessing frame %ld/%ld (%.2f%%); time elapsed: %lds", - proc_ctx.processed_frames, - proc_ctx.total_frames, - proc_ctx.total_frames > 0 - ? (double)proc_ctx.processed_frames * 100.0 / (double)proc_ctx.total_frames - : 0.0, - time(NULL) - proc_ctx.start_time - ); - fflush(stdout); - newline_required = true; - } - - // Sleep for 50ms - thrd_sleep(&(struct timespec){.tv_sec = 0, .tv_nsec = 100000000}, NULL); - } - -// Restore terminal to blocking mode -#ifndef _WIN32 - set_nonblocking_input(false); -#endif - - // Join the processing thread to ensure it completes before exiting - int process_result; - thrd_join(processing_thread, &process_result); - - // Print a newline if progress bar was displayed - if (newline_required) { - putchar('\n'); - } - - // Print final message based on processing result - if (proc_ctx.abort) { - fprintf(stderr, "Video processing aborted\n"); - return 2; - } else if (process_result != 0) { - fprintf(stderr, "Video processing failed\n"); - return process_result; - } else { - printf("Video processing completed successfully\n"); - } - - // Calculate statistics - time_t time_elapsed = time(NULL) - proc_ctx.start_time; - float average_speed_fps = - (float)proc_ctx.processed_frames / (time_elapsed > 0 ? (float)time_elapsed : 1); - - // Print processing summary - printf("====== Video2X %s summary ======\n", arguments.benchmark ? "Benchmark" : "Processing"); - printf("Video file processed: %s\n", arguments.in_fname); - printf("Total frames processed: %ld\n", proc_ctx.processed_frames); - printf("Total time taken: %lds\n", time_elapsed); - printf("Average processing speed: %.2f FPS\n", average_speed_fps); - - // Print additional information if not in benchmark mode - if (!arguments.benchmark) { - printf("Output written to: %s\n", arguments.out_fname); - } - - return 0; -} diff --git a/src/video2x.cpp b/src/video2x.cpp new file mode 100644 index 0000000..fc246de --- /dev/null +++ b/src/video2x.cpp @@ -0,0 +1,708 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#else +#include +#include +#include +#endif + +extern "C" { +#include +#include +#include +#include +#include + +#include +#include +} + +#include + +#ifdef _WIN32 +#define BOOST_PROGRAM_OPTIONS_WCHAR_T +#endif +#include +namespace po = boost::program_options; + +// List of valid RealESRGAN models +const std::vector valid_realesrgan_models = { + "realesrgan-plus", + "realesrgan-plus-anime", + "realesr-animevideov3" +}; + +// Indicate if a newline needs to be printed before the next output +std::atomic newline_required = false; + +// Structure to hold parsed arguments +struct Arguments { + // General options +#ifdef _WIN32 + std::wstring loglevel = L"info"; + std::wstring filter_type; + std::wstring hwaccel = L"none"; +#else + std::string loglevel = "info"; + std::string filter_type; + std::string hwaccel = "none"; +#endif + bool noprogress = false; + std::filesystem::path in_fname; + std::filesystem::path out_fname; + bool nocopystreams = false; + bool benchmark = false; + + // Encoder options +#ifdef _WIN32 + std::wstring codec = L"libx264"; + std::wstring pix_fmt; + std::wstring preset = L"slow"; +#else + std::string codec = "libx264"; + std::string pix_fmt; + std::string preset = "slow"; +#endif + int64_t bitrate = 0; + float crf = 20.0f; + + // libplacebo options + std::filesystem::path shader_path; + int out_width = 0; + int out_height = 0; + + // RealESRGAN options + int gpuid = 0; + std::filesystem::path model_path; + int scaling_factor = 0; +}; + +// Set UNIX terminal input to non-blocking mode +#ifndef _WIN32 +void set_nonblocking_input(bool enable) { + static struct termios oldt, newt; + if (enable) { + tcgetattr(STDIN_FILENO, &oldt); + newt = oldt; + newt.c_lflag &= static_cast(~(ICANON | ECHO)); + tcsetattr(STDIN_FILENO, TCSANOW, &newt); + fcntl(STDIN_FILENO, F_SETFL, O_NONBLOCK); + } else { + tcsetattr(STDIN_FILENO, TCSANOW, &oldt); + fcntl(STDIN_FILENO, F_SETFL, 0); + } +} +#endif + +// Convert a wide string to UTF-8 string +#ifdef _WIN32 +std::string wstring_to_utf8(const std::wstring &wstr) { + if (wstr.empty()) { + return std::string(); + } + int size_needed = WideCharToMultiByte( + CP_UTF8, 0, wstr.data(), static_cast(wstr.size()), nullptr, 0, nullptr, nullptr + ); + std::string strTo(size_needed, 0); + WideCharToMultiByte( + CP_UTF8, + 0, + wstr.data(), + static_cast(wstr.size()), + &strTo[0], + size_needed, + nullptr, + nullptr + ); + return strTo; +} +#endif + +// Newline-safe log callback for FFmpeg +void newline_safe_ffmpeg_log_callback(void *ptr, int level, const char *fmt, va_list vl) { + if (level <= av_log_get_level() && newline_required) { + putchar('\n'); + newline_required = false; + } + av_log_default_callback(ptr, level, fmt, vl); +} + +bool is_valid_realesrgan_model(const std::string &model) { + return std::find(valid_realesrgan_models.begin(), valid_realesrgan_models.end(), model) != + valid_realesrgan_models.end(); +} + +enum Libvideo2xLogLevel parse_log_level(const std::string &level_name) { + if (level_name == "trace") { + return LIBVIDEO2X_LOG_LEVEL_TRACE; + } else if (level_name == "debug") { + return LIBVIDEO2X_LOG_LEVEL_DEBUG; + } else if (level_name == "info") { + return LIBVIDEO2X_LOG_LEVEL_INFO; + } else if (level_name == "warning" || level_name == "warn") { + return LIBVIDEO2X_LOG_LEVEL_WARNING; + } else if (level_name == "error") { + return LIBVIDEO2X_LOG_LEVEL_ERROR; + } else if (level_name == "critical") { + return LIBVIDEO2X_LOG_LEVEL_CRITICAL; + } else if (level_name == "off" || level_name == "none") { + return LIBVIDEO2X_LOG_LEVEL_OFF; + } else { + spdlog::warn("Invalid log level specified. Defaulting to 'info'."); + return LIBVIDEO2X_LOG_LEVEL_INFO; + } +} + +// Mutex for synchronizing access to VideoProcessingContext +std::mutex proc_ctx_mutex; + +// Wrapper function for video processing thread +void process_video_thread( + Arguments *arguments, + AVHWDeviceType hw_device_type, + FilterConfig *filter_config, + EncoderConfig *encoder_config, + VideoProcessingContext *proc_ctx +) { + enum Libvideo2xLogLevel log_level = parse_log_level(wstring_to_utf8(arguments->loglevel)); + +#ifdef _WIN32 + const wchar_t *in_fname = arguments->in_fname.c_str(); + const wchar_t *out_fname = arguments->out_fname.c_str(); +#else + const char *in_fname = arguments->in_fname.c_str(); + const char *out_fname = arguments->out_fname.c_str(); +#endif + + int result = process_video( + in_fname, + out_fname, + log_level, + arguments->benchmark, + hw_device_type, + filter_config, + encoder_config, + proc_ctx + ); + + { + std::lock_guard lock(proc_ctx_mutex); + proc_ctx->completed = true; + } + + if (result != 0) { + spdlog::error("Video processing failed with error code: {}", result); + } +} + +#ifdef _WIN32 +int wmain(int argc, wchar_t *argv[]) { + SetConsoleOutputCP(CP_UTF8); +#else +int main(int argc, char **argv) { +#endif + // Initialize arguments structure + Arguments arguments; + + // Parse command line arguments using Boost.Program_options + try { + po::options_description desc("Allowed options"); + +#ifdef _WIN32 + desc.add_options() + ("help", "Display this help page") + ("version,v", "Print program version") + ("loglevel", po::wvalue(&arguments.loglevel), "Set log level (trace, debug, info, warn, error, critical, none)") + ("noprogress", po::bool_switch(&arguments.noprogress), "Do not display the progress bar") + + // General Processing Options + ("input,i", po::wvalue(), "Input video file path") + ("output,o", po::wvalue(), "Output video file path") + ("filter,f", po::wvalue(&arguments.filter_type), "Filter to use: 'libplacebo' or 'realesrgan'") + ("hwaccel,a", po::wvalue(&arguments.hwaccel), "Hardware acceleration method (default: none)") + ("nocopystreams", po::bool_switch(&arguments.nocopystreams), "Do not copy audio and subtitle streams") + ("benchmark", po::bool_switch(&arguments.benchmark), "Discard processed frames and calculate average FPS") + + // Encoder options + ("codec,c", po::wvalue(&arguments.codec), "Output codec (default: libx264)") + ("preset,p", po::wvalue(&arguments.preset), "Encoder preset (default: slow)") + ("pixfmt,x", po::wvalue(&arguments.pix_fmt), "Output pixel format (default: auto)") + ("bitrate,b", po::value(&arguments.bitrate)->default_value(0), "Bitrate in bits per second (default: 0 (VBR))") + ("crf,q", po::value(&arguments.crf)->default_value(20.0f), "Constant Rate Factor (default: 20.0)") + + // libplacebo options + ("shader,s", po::wvalue(), "Name or path of the GLSL shader file to use (built-in: 'anime4k-a', 'anime4k-b', 'anime4k-c', 'anime4k-a+a', 'anime4k-b+b', 'anime4k-c+a')") + ("width,w", po::value(&arguments.out_width), "Output width") + ("height,h", po::value(&arguments.out_height), "Output height") + + // RealESRGAN options + ("gpuid,g", po::value(&arguments.gpuid)->default_value(0), "Vulkan GPU ID (default: 0)") + ("model,m", po::wvalue(), "Name of the model to use") + ("scale,r", po::value(&arguments.scaling_factor), "Scaling factor (2, 3, or 4)") + ; +#else + desc.add_options() + ("help", "Display this help page") + ("version,v", "Print program version") + ("loglevel", po::value(&arguments.loglevel)->default_value("info"), "Set log level (trace, debug, info, warn, error, critical, none)") + ("noprogress", po::bool_switch(&arguments.noprogress), "Do not display the progress bar") + + // General Processing Options + ("input,i", po::value(), "Input video file path") + ("output,o", po::value(), "Output video file path") + ("filter,f", po::value(&arguments.filter_type), "Filter to use: 'libplacebo' or 'realesrgan'") + ("hwaccel,a", po::value(&arguments.hwaccel)->default_value("none"), "Hardware acceleration method (default: none)") + ("nocopystreams", po::bool_switch(&arguments.nocopystreams), "Do not copy audio and subtitle streams") + ("benchmark", po::bool_switch(&arguments.benchmark), "Discard processed frames and calculate average FPS") + + // Encoder options + ("codec,c", po::value(&arguments.codec)->default_value("libx264"), "Output codec (default: libx264)") + ("preset,p", po::value(&arguments.preset)->default_value("slow"), "Encoder preset (default: slow)") + ("pixfmt,x", po::value(&arguments.pix_fmt), "Output pixel format (default: auto)") + ("bitrate,b", po::value(&arguments.bitrate)->default_value(0), "Bitrate in bits per second (default: 0 (VBR))") + ("crf,q", po::value(&arguments.crf)->default_value(20.0f), "Constant Rate Factor (default: 20.0)") + + // libplacebo options + ("shader,s", po::value(), "Name or path of the GLSL shader file to use (built-in: 'anime4k-a', 'anime4k-b', 'anime4k-c', 'anime4k-a+a', 'anime4k-b+b', 'anime4k-c+a')") + ("width,w", po::value(&arguments.out_width), "Output width") + ("height,h", po::value(&arguments.out_height), "Output height") + + // RealESRGAN options + ("gpuid,g", po::value(&arguments.gpuid)->default_value(0), "Vulkan GPU ID (default: 0)") + ("model,m", po::value(), "Name of the model to use") + ("scale,r", po::value(&arguments.scaling_factor), "Scaling factor (2, 3, or 4)") + ; +#endif + + // Positional arguments + po::positional_options_description p; + p.add("input", 1).add("output", 1).add("filter", 1); + +#ifdef _WIN32 + po::variables_map vm; + po::store(po::wcommand_line_parser(argc, argv).options(desc).positional(p).run(), vm); +#else + po::variables_map vm; + po::store(po::command_line_parser(argc, argv).options(desc).positional(p).run(), vm); +#endif + po::notify(vm); + + // Set default values for optional arguments +#ifdef _WIN32 + if (!vm.count("loglevel")) { + arguments.loglevel = L"info"; + } + if (!vm.count("hwaccel")) { + arguments.hwaccel = L"none"; + } + if (!vm.count("codec")) { + arguments.codec = L"libx264"; + } + if (!vm.count("preset")) { + arguments.preset = L"slow"; + } +#endif + + if (vm.count("help")) { + std::cout << desc << std::endl; + return 0; + } + + if (vm.count("version")) { + std::cout << "Video2X version " << LIBVIDEO2X_VERSION_STRING << std::endl; + return 0; + } + + // Assign positional arguments + if (vm.count("input")) { +#ifdef _WIN32 + arguments.in_fname = std::filesystem::path(vm["input"].as()); +#else + arguments.in_fname = std::filesystem::path(vm["input"].as()); +#endif + } else { + spdlog::error("Error: Input file path is required."); + return 1; + } + + if (vm.count("output")) { +#ifdef _WIN32 + arguments.out_fname = std::filesystem::path(vm["output"].as()); +#else + arguments.out_fname = std::filesystem::path(vm["output"].as()); +#endif + } else if (!arguments.benchmark) { + spdlog::error("Error: Output file path is required."); + return 1; + } + + if (!vm.count("filter")) { + spdlog::error("Error: Filter type is required (libplacebo or realesrgan)."); + return 1; + } + + if (vm.count("shader")) { +#ifdef _WIN32 + arguments.shader_path = std::filesystem::path(vm["shader"].as()); +#else + arguments.shader_path = std::filesystem::path(vm["shader"].as()); +#endif + } + + if (vm.count("model")) { +#ifdef _WIN32 + arguments.model_path = std::filesystem::path(vm["model"].as()); +#else + arguments.model = vm["model"].as(); +#endif + if (!is_valid_realesrgan_model(vm["model"].as())) { + spdlog::error( + "Error: Invalid model specified. Must be 'realesrgan-plus', " + "'realesrgan-plus-anime', or 'realesr-animevideov3'." + ); + return 1; + } + } + } catch (const po::error &e) { + spdlog::error("Error parsing options: {}", e.what()); + return 1; + } catch (const std::exception &e) { + spdlog::error("Exception caught: {}", e.what()); + return 1; + } + +// Additional validations +#ifdef _WIN32 + if (arguments.filter_type == L"libplacebo") { +#else + if (arguments.filter_type == "libplacebo") { +#endif + if (arguments.shader_path.empty() || arguments.out_width == 0 || + arguments.out_height == 0) { + spdlog::error( + "Error: For libplacebo, shader name/path (-s), width (-w), " + "and height (-h) are required." + ); + return 1; + } +#ifdef _WIN32 + } else if (arguments.filter_type == L"realesrgan") { +#else + } else if (arguments.filter_type == "realesrgan") { +#endif + if (arguments.scaling_factor == 0 || arguments.model_path.empty()) { + spdlog::error("Error: For realesrgan, scaling factor (-r) and model (-m) are required." + ); + return 1; + } + if (arguments.scaling_factor != 2 && arguments.scaling_factor != 3 && + arguments.scaling_factor != 4) { + spdlog::error("Error: Scaling factor must be 2, 3, or 4."); + return 1; + } + } else { + spdlog::error("Error: Invalid filter type specified. Must be 'libplacebo' or 'realesrgan'." + ); + return 1; + } + + // Validate bitrate + if (arguments.bitrate < 0) { + spdlog::error("Error: Invalid bitrate specified."); + return 1; + } + + // Validate CRF + if (arguments.crf < 0.0f || arguments.crf > 51.0f) { + spdlog::error("Error: CRF must be between 0 and 51."); + return 1; + } + + // Parse codec to AVCodec + const AVCodec *codec = avcodec_find_encoder_by_name(wstring_to_utf8(arguments.codec).c_str()); + if (!codec) { + spdlog::error("Error: Codec '{}' not found.", wstring_to_utf8(arguments.codec)); + return 1; + } + + // Parse pixel format to AVPixelFormat + enum AVPixelFormat pix_fmt = AV_PIX_FMT_NONE; + if (!arguments.pix_fmt.empty()) { + pix_fmt = av_get_pix_fmt(wstring_to_utf8(arguments.pix_fmt).c_str()); + if (pix_fmt == AV_PIX_FMT_NONE) { + spdlog::error("Error: Invalid pixel format '{}'.", wstring_to_utf8(arguments.pix_fmt)); + return 1; + } + } + + // Set spdlog log level + auto log_level = parse_log_level(wstring_to_utf8(arguments.loglevel)); + switch (log_level) { + case LIBVIDEO2X_LOG_LEVEL_TRACE: + spdlog::set_level(spdlog::level::trace); + break; + case LIBVIDEO2X_LOG_LEVEL_DEBUG: + spdlog::set_level(spdlog::level::debug); + break; + case LIBVIDEO2X_LOG_LEVEL_INFO: + spdlog::set_level(spdlog::level::info); + break; + case LIBVIDEO2X_LOG_LEVEL_WARNING: + spdlog::set_level(spdlog::level::warn); + break; + case LIBVIDEO2X_LOG_LEVEL_ERROR: + spdlog::set_level(spdlog::level::err); + break; + case LIBVIDEO2X_LOG_LEVEL_CRITICAL: + spdlog::set_level(spdlog::level::critical); + break; + case LIBVIDEO2X_LOG_LEVEL_OFF: + spdlog::set_level(spdlog::level::off); + break; + default: + spdlog::set_level(spdlog::level::info); + break; + } + + // Setup filter configurations based on the parsed arguments + FilterConfig filter_config; +#ifdef _WIN32 + if (arguments.filter_type == L"libplacebo") { +#else + if (arguments.filter_type == "libplacebo") { +#endif + filter_config.filter_type = FILTER_LIBPLACEBO; + filter_config.config.libplacebo.out_width = arguments.out_width; + filter_config.config.libplacebo.out_height = arguments.out_height; +#ifdef _WIN32 + filter_config.config.libplacebo.shader_path = arguments.shader_path.c_str(); +#else + filter_config.config.libplacebo.shader_path = arguments.shader_path.c_str(); +#endif +#ifdef _WIN32 + } else if (arguments.filter_type == L"realesrgan") { +#else + } else if (arguments.filter_type == "realesrgan") { +#endif + filter_config.filter_type = FILTER_REALESRGAN; + filter_config.config.realesrgan.gpuid = arguments.gpuid; + filter_config.config.realesrgan.tta_mode = false; + filter_config.config.realesrgan.scaling_factor = arguments.scaling_factor; +#ifdef _WIN32 + filter_config.config.realesrgan.model_path = arguments.model_path.c_str(); +#else + filter_config.config.realesrgan.model_path = arguments.model_path.c_str(); +#endif + } + + // Convert arguments to UTF-8 encoded strings +#ifdef _WIN32 + std::string preset_str = wstring_to_utf8(arguments.preset); +#else + std::string preset_str = arguments.preset; +#endif + + // Setup encoder configuration + EncoderConfig encoder_config; + encoder_config.out_width = 0; + encoder_config.out_height = 0; + encoder_config.copy_streams = !arguments.nocopystreams; + encoder_config.codec = codec->id; + encoder_config.pix_fmt = pix_fmt; + encoder_config.preset = preset_str.c_str(); + encoder_config.bit_rate = arguments.bitrate; + encoder_config.crf = arguments.crf; + + // Parse hardware acceleration method + enum AVHWDeviceType hw_device_type = AV_HWDEVICE_TYPE_NONE; +#ifdef _WIN32 + if (arguments.hwaccel != L"none") { +#else + if (arguments.hwaccel != "none") { +#endif + hw_device_type = av_hwdevice_find_type_by_name(wstring_to_utf8(arguments.hwaccel).c_str()); + if (hw_device_type == AV_HWDEVICE_TYPE_NONE) { + spdlog::error( + "Error: Invalid hardware device type '{}'.", wstring_to_utf8(arguments.hwaccel) + ); + return 1; + } + } + + // Setup struct to store processing context + VideoProcessingContext proc_ctx; + proc_ctx.processed_frames = 0; + proc_ctx.total_frames = 0; + proc_ctx.pause = false; + proc_ctx.abort = false; + proc_ctx.completed = false; + + // Register a newline-safe log callback for FFmpeg + av_log_set_callback(newline_safe_ffmpeg_log_callback); + + // Create a thread for video processing + std::thread processing_thread( + process_video_thread, &arguments, hw_device_type, &filter_config, &encoder_config, &proc_ctx + ); + spdlog::info("Video processing started; press SPACE to pause/resume, 'q' to abort."); + auto start_time = std::chrono::steady_clock::now(); + auto paused_start = std::chrono::steady_clock::time_point(); + std::chrono::seconds total_paused_duration(0); + long long time_elapsed = 0; + + // Enable non-blocking input +#ifndef _WIN32 + set_nonblocking_input(true); +#endif + + // Main thread loop to display progress and handle input + while (true) { + bool completed; + { + std::lock_guard lock(proc_ctx_mutex); + completed = proc_ctx.completed; + } + if (completed) { + break; + } + + // Check for key presses + int ch = -1; + + // Check for key press +#ifdef _WIN32 + if (_kbhit()) { + ch = _getch(); + } +#else + ch = getchar(); +#endif + + if (ch == ' ' || ch == '\n') { + // Toggle pause state + { + std::lock_guard lock(proc_ctx_mutex); + proc_ctx.pause = !proc_ctx.pause; + if (proc_ctx.pause) { + putchar('\n'); + spdlog::info("Processing paused. Press SPACE to resume, 'q' to abort."); + paused_start = std::chrono::steady_clock::now(); + } else { + spdlog::info("Resuming processing..."); + total_paused_duration += std::chrono::duration_cast( + std::chrono::steady_clock::now() - paused_start + ); + } + } + } else if (ch == 'q' || ch == 'Q') { + // Abort processing + putchar('\n'); + spdlog::info("Aborting processing..."); + { + std::lock_guard lock(proc_ctx_mutex); + proc_ctx.abort = true; + newline_required = false; + } + break; + } + + // Display progress + if (!arguments.noprogress) { + int64_t processed_frames, total_frames; + bool pause; + { + std::lock_guard lock(proc_ctx_mutex); + processed_frames = proc_ctx.processed_frames; + total_frames = proc_ctx.total_frames; + pause = proc_ctx.pause; + } + if (!pause && total_frames > 0) { + double percentage = total_frames > 0 ? static_cast(processed_frames) * + 100.0 / static_cast(total_frames) + : 0.0; + auto now = std::chrono::steady_clock::now(); + time_elapsed = std::chrono::duration_cast( + now - start_time - total_paused_duration + ) + .count(); + + std::cout << "\rProcessing frame " << processed_frames << "/" << total_frames + << " (" << percentage << "%); time elapsed: " << time_elapsed << "s"; + std::cout.flush(); + newline_required = true; + } + } + + // Sleep for 100ms + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + // Restore terminal to blocking mode +#ifndef _WIN32 + set_nonblocking_input(false); +#endif + + // Join the processing thread to ensure it completes before exiting + processing_thread.join(); + + // Print a newline if progress bar was displayed + if (newline_required) { + std::cout << '\n'; + } + + // Print final message based on processing result + bool aborted, completed; + { + std::lock_guard lock(proc_ctx_mutex); + aborted = proc_ctx.abort; + completed = proc_ctx.completed; + } + if (aborted) { + spdlog::error("Video processing aborted"); + return 2; + } else if (!completed) { + spdlog::error("Video processing failed"); + return 1; + } else { + spdlog::info("Video processing completed successfully"); + } + + // Calculate statistics + int64_t processed_frames; + { + std::lock_guard lock(proc_ctx_mutex); + processed_frames = proc_ctx.processed_frames; + } + float average_speed_fps = static_cast(processed_frames) / + (time_elapsed > 0 ? static_cast(time_elapsed) : 1); + + // Print processing summary + printf("====== Video2X %s summary ======\n", arguments.benchmark ? "Benchmark" : "Processing"); + printf("Video file processed: %s\n", arguments.in_fname.u8string().c_str()); + printf("Total frames processed: %ld\n", proc_ctx.processed_frames); + printf("Total time taken: %lds\n", time_elapsed); + printf("Average processing speed: %.2f FPS\n", average_speed_fps); + + // Print additional information if not in benchmark mode + if (!arguments.benchmark) { + printf("Output written to: %s\n", arguments.out_fname.u8string().c_str()); + } + + return 0; +}