feat(*): added support for copying audio/subtitle streams and pause/abort (#1179)

* feat: added Makefile target for debian
* fix: fixed Dockerfile installing the wrong package
* feat: added hwaccel for encoder and decoder
* feat: added benchmark mode
* feat: removed hard-coded keyframe info
* chore: cleaned up headers and organized code
* style: cleaned up headers and includes
* feat: added a progress bar for CLI
* feat: removed atomicity requirements on processed frames
* feat: added pause and abort for CLI
* chore: updated default preset and crf settings
* feat: added support for copying audio and subtitle streams
* fix: fixed syntax issues for MSVC
* fix: fixed audio/subtitle timestamp rescaling

Signed-off-by: k4yt3x <i@k4yt3x.com>
This commit is contained in:
K4YT3X
2024-10-10 00:23:13 -07:00
committed by GitHub
parent c7fa9c10e6
commit 37c2c4c647
21 changed files with 731 additions and 322 deletions

View File

@@ -1,7 +1,19 @@
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <threads.h>
#include <time.h>
#ifdef _WIN32
#include <conio.h>
#else
#include <fcntl.h>
#include <termios.h>
#include <unistd.h>
#endif
#include <libavutil/hwcontext.h>
#include <libavutil/pixdesc.h>
#include <libavutil/pixfmt.h>
@@ -11,14 +23,35 @@
const char *VIDEO2X_VERSION = "6.0.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 &= ~(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
// Define command line options
static struct option long_options[] = {
{"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'},
{"version", no_argument, NULL, 'v'},
{"help", no_argument, NULL, 0},
{"hwaccel", required_argument, NULL, 'a'},
{"nocopystreams", no_argument, NULL, 0},
{"benchmark", no_argument, NULL, 0},
// Encoder options
{"codec", required_argument, NULL, 'c'},
@@ -27,7 +60,7 @@ static struct option long_options[] = {
{"bitrate", required_argument, NULL, 'b'},
{"crf", required_argument, NULL, 'q'},
// Libplacebo options
// libplacebo options
{"shader", required_argument, NULL, 's'},
{"width", required_argument, NULL, 'w'},
{"height", required_argument, NULL, 'h'},
@@ -45,6 +78,9 @@ struct arguments {
const char *input_filename;
const char *output_filename;
const char *filter_type;
const char *hwaccel;
bool nocopystreams;
bool benchmark;
// Encoder options
const char *codec;
@@ -64,6 +100,14 @@ struct arguments {
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;
};
const char *valid_models[] = {
"realesrgan-plus",
"realesrgan-plus-anime",
@@ -84,19 +128,23 @@ int is_valid_realesrgan_model(const char *model) {
void print_help() {
printf("Usage: video2x [OPTIONS]\n");
printf("\nGeneral Options:\n");
printf("\nOptions:\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(" -v, --version Print program version\n");
printf(" --help Display this help page\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: veryslow)\n");
printf(" -x, --pixfmt Output pixel format (default: yuv420p)\n");
printf(" -b, --bitrate Bitrate in bits per second (default: 2000000)\n");
printf(" -q, --crf Constant Rate Factor (default: 17.0)\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 to custom GLSL shader file\n");
@@ -107,6 +155,10 @@ void print_help() {
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-mode-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) {
@@ -117,13 +169,16 @@ void parse_arguments(int argc, char **argv, struct arguments *arguments) {
arguments->input_filename = NULL;
arguments->output_filename = NULL;
arguments->filter_type = NULL;
arguments->hwaccel = "none";
arguments->nocopystreams = false;
arguments->benchmark = false;
// Encoder options
arguments->codec = "libx264";
arguments->preset = "veryslow";
arguments->pix_fmt = "yuv420p";
arguments->bitrate = 2 * 1000 * 1000;
arguments->crf = 17.0;
arguments->preset = "slow";
arguments->pix_fmt = NULL;
arguments->bitrate = 0;
arguments->crf = 20.0;
// libplacebo options
arguments->shader_path = NULL;
@@ -135,8 +190,9 @@ void parse_arguments(int argc, char **argv, struct arguments *arguments) {
arguments->model = NULL;
arguments->scaling_factor = 0;
while ((c = getopt_long(argc, argv, "i:o:f:c:x:p:b:q:s:w:h:r:m:v", long_options, &option_index)
) != -1) {
while ((c = getopt_long(
argc, argv, "i:o:f:a:c:x:p:b:q:s:w:h:r:m:v", long_options, &option_index
)) != -1) {
switch (c) {
case 'i':
arguments->input_filename = optarg;
@@ -147,6 +203,9 @@ void parse_arguments(int argc, char **argv, struct arguments *arguments) {
case 'f':
arguments->filter_type = optarg;
break;
case 'a':
arguments->hwaccel = optarg;
break;
case 'c':
arguments->codec = optarg;
break;
@@ -195,7 +254,8 @@ void parse_arguments(int argc, char **argv, struct arguments *arguments) {
if (!is_valid_realesrgan_model(arguments->model)) {
fprintf(
stderr,
"Error: Invalid model specified. Must be 'realesrgan-plus', 'realesrgan-plus-anime', or 'realesr-animevideov3'.\n"
"Error: Invalid model specified. Must be 'realesrgan-plus', "
"'realesrgan-plus-anime', or 'realesr-animevideov3'.\n"
);
exit(1);
}
@@ -209,12 +269,16 @@ void parse_arguments(int argc, char **argv, struct arguments *arguments) {
}
break;
case 'v':
printf("video2x %s\n", VIDEO2X_VERSION);
printf("Video2X v%s\n", VIDEO2X_VERSION);
exit(0);
case 0: // Long-only options without short equivalents (e.g., help)
case 0: // Long-only options without short equivalents
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:
@@ -224,8 +288,13 @@ void parse_arguments(int argc, char **argv, struct arguments *arguments) {
}
// Check for required arguments
if (!arguments->input_filename || !arguments->output_filename) {
fprintf(stderr, "Error: Input and output files are required.\n");
if (!arguments->input_filename) {
fprintf(stderr, "Error: Input file path is required.\n");
exit(1);
}
if (!arguments->output_filename && !arguments->benchmark) {
fprintf(stderr, "Error: Output file path is required.\n");
exit(1);
}
@@ -239,7 +308,8 @@ void parse_arguments(int argc, char **argv, struct arguments *arguments) {
arguments->output_height == 0) {
fprintf(
stderr,
"Error: For libplacebo, shader name/path (-s), width (-w), and height (-e) are required.\n"
"Error: For libplacebo, shader name/path (-s), width (-w), "
"and height (-e) are required.\n"
);
exit(1);
}
@@ -253,7 +323,40 @@ void parse_arguments(int argc, char **argv, struct arguments *arguments) {
}
}
// Wrapper function for video processing thread
int process_video_thread(void *arg) {
struct ProcessVideoThreadArguments *thread_args = (struct ProcessVideoThreadArguments *)arg;
// Extract individual arguments
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->input_filename,
arguments->output_filename,
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);
@@ -283,16 +386,20 @@ int main(int argc, char **argv) {
}
// Parse pixel format to AVPixelFormat
enum AVPixelFormat 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;
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 = {
.output_width = 0, // To be filled by libvideo2x
.output_height = 0, // To be filled by libvideo2x
.copy_streams = !arguments.nocopystreams,
.codec = codec->id,
.pix_fmt = pix_fmt,
.preset = arguments.preset,
@@ -300,26 +407,134 @@ int main(int argc, char **argv) {
.crf = arguments.crf,
};
// Setup struct to store processing status
struct ProcessingStatus status = {0};
// Process the video
if (process_video(
arguments.input_filename,
arguments.output_filename,
&filter_config,
&encoder_config,
&status
)) {
fprintf(stderr, "Video processing failed.\n");
return 1;
// 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
};
// Enable non-blocking input
#ifndef _WIN32
set_nonblocking_input(true);
#endif
// 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("[Video2X] Video processing started.\n");
printf("[Video2X] Press SPACE to pause/resume, 'q' to abort.\n");
// 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("\n[Video2X] Processing paused. Press SPACE to resume, 'q' to abort.");
} else {
printf("\n[Video2X] Resuming processing...");
}
fflush(stdout);
} else if (ch == 'q' || ch == 'Q') {
// Abort processing
printf("\n[Video2X] Aborting processing...");
fflush(stdout);
proc_ctx.abort = true;
break;
}
// Display progress
if (!proc_ctx.pause && proc_ctx.total_frames > 0) {
printf(
"\r[Video2X] Processing frame %ld/%ld (%.2f%%); time elapsed: %lds",
proc_ctx.processed_frames,
proc_ctx.total_frames,
proc_ctx.total_frames > 0
? proc_ctx.processed_frames * 100.0 / proc_ctx.total_frames
: 0.0,
time(NULL) - proc_ctx.start_time
);
fflush(stdout);
}
// Sleep for a short duration
thrd_sleep(&(struct timespec){.tv_sec = 0, .tv_nsec = 100000000}, NULL); // Sleep for 100ms
}
puts(""); // Print newline after progress bar is complete
// 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);
if (proc_ctx.abort) {
fprintf(stderr, "Video processing aborted\n");
return 2;
}
if (process_result != 0) {
fprintf(stderr, "Video processing failed\n");
return process_result;
}
// Calculate statistics
time_t time_elapsed = time(NULL) - proc_ctx.start_time;
float average_speed_fps =
(float)proc_ctx.processed_frames / (time_elapsed > 0 ? time_elapsed : 1);
// Print processing summary
printf("====== Video2X Processing summary ======\n");
printf("====== Video2X %s summary ======\n", arguments.benchmark ? "Benchmark" : "Processing");
printf("Video file processed: %s\n", arguments.input_filename);
printf("Total frames processed: %ld\n", status.processed_frames);
printf("Total time taken: %lds\n", time(NULL) - status.start_time);
printf("Output written to: %s\n", arguments.output_filename);
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.output_filename);
}
return 0;
}