Mercurial
diff third_party/ffmpeg/ffmpeg_cli.c @ 217:7ef4c9d2a72d hg-web
[DO NOT PUSH] Random values.
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Sun, 25 Jan 2026 10:44:04 -0800 |
| parents | |
| children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/third_party/ffmpeg/ffmpeg_cli.c Sun Jan 25 10:44:04 2026 -0800 @@ -0,0 +1,260 @@ +#include "ffmpeg_cli.h" +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <stdarg.h> +#include <sys/stat.h> +#include <sys/wait.h> +#include <errno.h> + +#define MAX_CMD_LEN 4096 +#define MAX_ERROR_LEN 512 + +static char last_error[MAX_ERROR_LEN] = {0}; + +static void set_error(const char* fmt, ...) { + va_list args; + va_start(args, fmt); + vsnprintf(last_error, MAX_ERROR_LEN, fmt, args); + va_end(args); +} + +static bool file_exists(const char* path) { + struct stat st; + return stat(path, &st) == 0; +} + +static bool ensure_dir(const char* path) { + struct stat st; + if (stat(path, &st) == 0) { + return S_ISDIR(st.st_mode); + } + return mkdir(path, 0755) == 0; +} + +static int run_command(const char* cmd) { + int ret = system(cmd); + if (ret == -1) { + set_error("Failed to execute command: %s", strerror(errno)); + return -1; + } + return WEXITSTATUS(ret); +} + +ImageConvertOpts ffmpeg_image_opts_default(void) { + return (ImageConvertOpts){ + .quality = 80, + .max_width = 0, + .max_height = 0, + .preserve_aspect = true, + }; +} + +HlsConvertOpts ffmpeg_hls_opts_default(void) { + return (HlsConvertOpts){ + .segment_duration = 6, + .video_bitrate = 0, + .audio_bitrate = 128, + .max_width = 0, + .max_height = 0, + .generate_thumbnail = true, + }; +} + +bool ffmpeg_is_available(void) { + return system("ffmpeg -version > /dev/null 2>&1") == 0; +} + +const char* ffmpeg_last_error(void) { + return last_error; +} + +FfmpegResult ffmpeg_image_to_webp(const char* input_path, const char* output_path, ImageConvertOpts* opts) { + if (!input_path || !output_path) { + set_error("Invalid arguments: input_path and output_path required"); + return FFMPEG_ERR_INVALID_ARGS; + } + + if (!file_exists(input_path)) { + set_error("Input file not found: %s", input_path); + return FFMPEG_ERR_INPUT_NOT_FOUND; + } + + ImageConvertOpts default_opts = ffmpeg_image_opts_default(); + if (!opts) opts = &default_opts; + + char cmd[MAX_CMD_LEN]; + char scale_filter[256] = ""; + + // Build scale filter if resizing + if (opts->max_width > 0 || opts->max_height > 0) { + int w = opts->max_width > 0 ? opts->max_width : -1; + int h = opts->max_height > 0 ? opts->max_height : -1; + + if (opts->preserve_aspect) { + if (w > 0 && h > 0) { + snprintf(scale_filter, sizeof(scale_filter), + "-vf \"scale='min(%d,iw)':min'(%d,ih)':force_original_aspect_ratio=decrease\"", w, h); + } else if (w > 0) { + snprintf(scale_filter, sizeof(scale_filter), "-vf \"scale='min(%d,iw)':-1\"", w); + } else { + snprintf(scale_filter, sizeof(scale_filter), "-vf \"scale=-1:'min(%d,ih)'\"", h); + } + } else { + snprintf(scale_filter, sizeof(scale_filter), "-vf \"scale=%d:%d\"", w, h); + } + } + + snprintf(cmd, sizeof(cmd), + "ffmpeg -y -i \"%s\" %s -quality %d \"%s\" 2>/dev/null", + input_path, scale_filter, opts->quality, output_path); + + int ret = run_command(cmd); + if (ret != 0) { + set_error("FFmpeg conversion failed with code %d", ret); + return FFMPEG_ERR_PROCESS_FAILED; + } + + if (!file_exists(output_path)) { + set_error("Output file was not created"); + return FFMPEG_ERR_OUTPUT_FAILED; + } + + return FFMPEG_OK; +} + +FfmpegResult ffmpeg_video_to_hls(const char* input_path, const char* output_dir, const char* name, HlsConvertOpts* opts) { + if (!input_path || !output_dir || !name) { + set_error("Invalid arguments: input_path, output_dir, and name required"); + return FFMPEG_ERR_INVALID_ARGS; + } + + if (!file_exists(input_path)) { + set_error("Input file not found: %s", input_path); + return FFMPEG_ERR_INPUT_NOT_FOUND; + } + + if (!ensure_dir(output_dir)) { + set_error("Failed to create output directory: %s", output_dir); + return FFMPEG_ERR_OUTPUT_FAILED; + } + + HlsConvertOpts default_opts = ffmpeg_hls_opts_default(); + if (!opts) opts = &default_opts; + + char cmd[MAX_CMD_LEN]; + char scale_filter[256] = ""; + char video_bitrate[64] = ""; + char audio_bitrate[64] = ""; + + // Build scale filter if resizing + if (opts->max_width > 0 || opts->max_height > 0) { + int w = opts->max_width > 0 ? opts->max_width : -2; + int h = opts->max_height > 0 ? opts->max_height : -2; + snprintf(scale_filter, sizeof(scale_filter), "-vf \"scale=%d:%d\"", w, h); + } + + // Video bitrate + if (opts->video_bitrate > 0) { + snprintf(video_bitrate, sizeof(video_bitrate), "-b:v %dk", opts->video_bitrate); + } + + // Audio bitrate + snprintf(audio_bitrate, sizeof(audio_bitrate), "-b:a %dk", + opts->audio_bitrate > 0 ? opts->audio_bitrate : 128); + + char playlist_path[1024]; + char segment_pattern[1024]; + snprintf(playlist_path, sizeof(playlist_path), "%s/%s.m3u8", output_dir, name); + snprintf(segment_pattern, sizeof(segment_pattern), "%s/%s_%%03d.ts", output_dir, name); + + // Main HLS conversion command + snprintf(cmd, sizeof(cmd), + "ffmpeg -y -i \"%s\" " + "-c:v libx264 -preset fast -crf 22 %s %s " + "-c:a aac %s " + "-f hls " + "-hls_time %d " + "-hls_list_size 0 " + "-hls_segment_filename \"%s\" " + "\"%s\" 2>/dev/null", + input_path, + scale_filter, video_bitrate, + audio_bitrate, + opts->segment_duration, + segment_pattern, + playlist_path); + + int ret = run_command(cmd); + if (ret != 0) { + set_error("FFmpeg HLS conversion failed with code %d", ret); + return FFMPEG_ERR_PROCESS_FAILED; + } + + // Generate thumbnail if requested + if (opts->generate_thumbnail) { + char thumb_path[1024]; + snprintf(thumb_path, sizeof(thumb_path), "%s/%s_thumb.jpg", output_dir, name); + ffmpeg_video_thumbnail(input_path, thumb_path, 1); + } + + return FFMPEG_OK; +} + +FfmpegResult ffmpeg_video_thumbnail(const char* input_path, const char* output_path, int timestamp_seconds) { + if (!input_path || !output_path) { + set_error("Invalid arguments: input_path and output_path required"); + return FFMPEG_ERR_INVALID_ARGS; + } + + if (!file_exists(input_path)) { + set_error("Input file not found: %s", input_path); + return FFMPEG_ERR_INPUT_NOT_FOUND; + } + + char cmd[MAX_CMD_LEN]; + snprintf(cmd, sizeof(cmd), + "ffmpeg -y -i \"%s\" -ss %d -vframes 1 -q:v 2 \"%s\" 2>/dev/null", + input_path, timestamp_seconds, output_path); + + int ret = run_command(cmd); + if (ret != 0) { + set_error("FFmpeg thumbnail generation failed with code %d", ret); + return FFMPEG_ERR_PROCESS_FAILED; + } + + return FFMPEG_OK; +} + +FfmpegResult ffmpeg_get_duration(const char* input_path, double* duration_out) { + if (!input_path || !duration_out) { + set_error("Invalid arguments"); + return FFMPEG_ERR_INVALID_ARGS; + } + + if (!file_exists(input_path)) { + set_error("Input file not found: %s", input_path); + return FFMPEG_ERR_INPUT_NOT_FOUND; + } + + char cmd[MAX_CMD_LEN]; + snprintf(cmd, sizeof(cmd), + "ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 \"%s\" 2>/dev/null", + input_path); + + FILE* fp = popen(cmd, "r"); + if (!fp) { + set_error("Failed to run ffprobe"); + return FFMPEG_ERR_PROCESS_FAILED; + } + + char output[64]; + if (fgets(output, sizeof(output), fp) != NULL) { + *duration_out = atof(output); + } else { + *duration_out = 0; + } + + pclose(fp); + return FFMPEG_OK; +}