Mercurial
comparison 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 |
comparison
equal
deleted
inserted
replaced
| 192:b818a4561a3c | 217:7ef4c9d2a72d |
|---|---|
| 1 #include "ffmpeg_cli.h" | |
| 2 #include <stdio.h> | |
| 3 #include <stdlib.h> | |
| 4 #include <string.h> | |
| 5 #include <stdarg.h> | |
| 6 #include <sys/stat.h> | |
| 7 #include <sys/wait.h> | |
| 8 #include <errno.h> | |
| 9 | |
| 10 #define MAX_CMD_LEN 4096 | |
| 11 #define MAX_ERROR_LEN 512 | |
| 12 | |
| 13 static char last_error[MAX_ERROR_LEN] = {0}; | |
| 14 | |
| 15 static void set_error(const char* fmt, ...) { | |
| 16 va_list args; | |
| 17 va_start(args, fmt); | |
| 18 vsnprintf(last_error, MAX_ERROR_LEN, fmt, args); | |
| 19 va_end(args); | |
| 20 } | |
| 21 | |
| 22 static bool file_exists(const char* path) { | |
| 23 struct stat st; | |
| 24 return stat(path, &st) == 0; | |
| 25 } | |
| 26 | |
| 27 static bool ensure_dir(const char* path) { | |
| 28 struct stat st; | |
| 29 if (stat(path, &st) == 0) { | |
| 30 return S_ISDIR(st.st_mode); | |
| 31 } | |
| 32 return mkdir(path, 0755) == 0; | |
| 33 } | |
| 34 | |
| 35 static int run_command(const char* cmd) { | |
| 36 int ret = system(cmd); | |
| 37 if (ret == -1) { | |
| 38 set_error("Failed to execute command: %s", strerror(errno)); | |
| 39 return -1; | |
| 40 } | |
| 41 return WEXITSTATUS(ret); | |
| 42 } | |
| 43 | |
| 44 ImageConvertOpts ffmpeg_image_opts_default(void) { | |
| 45 return (ImageConvertOpts){ | |
| 46 .quality = 80, | |
| 47 .max_width = 0, | |
| 48 .max_height = 0, | |
| 49 .preserve_aspect = true, | |
| 50 }; | |
| 51 } | |
| 52 | |
| 53 HlsConvertOpts ffmpeg_hls_opts_default(void) { | |
| 54 return (HlsConvertOpts){ | |
| 55 .segment_duration = 6, | |
| 56 .video_bitrate = 0, | |
| 57 .audio_bitrate = 128, | |
| 58 .max_width = 0, | |
| 59 .max_height = 0, | |
| 60 .generate_thumbnail = true, | |
| 61 }; | |
| 62 } | |
| 63 | |
| 64 bool ffmpeg_is_available(void) { | |
| 65 return system("ffmpeg -version > /dev/null 2>&1") == 0; | |
| 66 } | |
| 67 | |
| 68 const char* ffmpeg_last_error(void) { | |
| 69 return last_error; | |
| 70 } | |
| 71 | |
| 72 FfmpegResult ffmpeg_image_to_webp(const char* input_path, const char* output_path, ImageConvertOpts* opts) { | |
| 73 if (!input_path || !output_path) { | |
| 74 set_error("Invalid arguments: input_path and output_path required"); | |
| 75 return FFMPEG_ERR_INVALID_ARGS; | |
| 76 } | |
| 77 | |
| 78 if (!file_exists(input_path)) { | |
| 79 set_error("Input file not found: %s", input_path); | |
| 80 return FFMPEG_ERR_INPUT_NOT_FOUND; | |
| 81 } | |
| 82 | |
| 83 ImageConvertOpts default_opts = ffmpeg_image_opts_default(); | |
| 84 if (!opts) opts = &default_opts; | |
| 85 | |
| 86 char cmd[MAX_CMD_LEN]; | |
| 87 char scale_filter[256] = ""; | |
| 88 | |
| 89 // Build scale filter if resizing | |
| 90 if (opts->max_width > 0 || opts->max_height > 0) { | |
| 91 int w = opts->max_width > 0 ? opts->max_width : -1; | |
| 92 int h = opts->max_height > 0 ? opts->max_height : -1; | |
| 93 | |
| 94 if (opts->preserve_aspect) { | |
| 95 if (w > 0 && h > 0) { | |
| 96 snprintf(scale_filter, sizeof(scale_filter), | |
| 97 "-vf \"scale='min(%d,iw)':min'(%d,ih)':force_original_aspect_ratio=decrease\"", w, h); | |
| 98 } else if (w > 0) { | |
| 99 snprintf(scale_filter, sizeof(scale_filter), "-vf \"scale='min(%d,iw)':-1\"", w); | |
| 100 } else { | |
| 101 snprintf(scale_filter, sizeof(scale_filter), "-vf \"scale=-1:'min(%d,ih)'\"", h); | |
| 102 } | |
| 103 } else { | |
| 104 snprintf(scale_filter, sizeof(scale_filter), "-vf \"scale=%d:%d\"", w, h); | |
| 105 } | |
| 106 } | |
| 107 | |
| 108 snprintf(cmd, sizeof(cmd), | |
| 109 "ffmpeg -y -i \"%s\" %s -quality %d \"%s\" 2>/dev/null", | |
| 110 input_path, scale_filter, opts->quality, output_path); | |
| 111 | |
| 112 int ret = run_command(cmd); | |
| 113 if (ret != 0) { | |
| 114 set_error("FFmpeg conversion failed with code %d", ret); | |
| 115 return FFMPEG_ERR_PROCESS_FAILED; | |
| 116 } | |
| 117 | |
| 118 if (!file_exists(output_path)) { | |
| 119 set_error("Output file was not created"); | |
| 120 return FFMPEG_ERR_OUTPUT_FAILED; | |
| 121 } | |
| 122 | |
| 123 return FFMPEG_OK; | |
| 124 } | |
| 125 | |
| 126 FfmpegResult ffmpeg_video_to_hls(const char* input_path, const char* output_dir, const char* name, HlsConvertOpts* opts) { | |
| 127 if (!input_path || !output_dir || !name) { | |
| 128 set_error("Invalid arguments: input_path, output_dir, and name required"); | |
| 129 return FFMPEG_ERR_INVALID_ARGS; | |
| 130 } | |
| 131 | |
| 132 if (!file_exists(input_path)) { | |
| 133 set_error("Input file not found: %s", input_path); | |
| 134 return FFMPEG_ERR_INPUT_NOT_FOUND; | |
| 135 } | |
| 136 | |
| 137 if (!ensure_dir(output_dir)) { | |
| 138 set_error("Failed to create output directory: %s", output_dir); | |
| 139 return FFMPEG_ERR_OUTPUT_FAILED; | |
| 140 } | |
| 141 | |
| 142 HlsConvertOpts default_opts = ffmpeg_hls_opts_default(); | |
| 143 if (!opts) opts = &default_opts; | |
| 144 | |
| 145 char cmd[MAX_CMD_LEN]; | |
| 146 char scale_filter[256] = ""; | |
| 147 char video_bitrate[64] = ""; | |
| 148 char audio_bitrate[64] = ""; | |
| 149 | |
| 150 // Build scale filter if resizing | |
| 151 if (opts->max_width > 0 || opts->max_height > 0) { | |
| 152 int w = opts->max_width > 0 ? opts->max_width : -2; | |
| 153 int h = opts->max_height > 0 ? opts->max_height : -2; | |
| 154 snprintf(scale_filter, sizeof(scale_filter), "-vf \"scale=%d:%d\"", w, h); | |
| 155 } | |
| 156 | |
| 157 // Video bitrate | |
| 158 if (opts->video_bitrate > 0) { | |
| 159 snprintf(video_bitrate, sizeof(video_bitrate), "-b:v %dk", opts->video_bitrate); | |
| 160 } | |
| 161 | |
| 162 // Audio bitrate | |
| 163 snprintf(audio_bitrate, sizeof(audio_bitrate), "-b:a %dk", | |
| 164 opts->audio_bitrate > 0 ? opts->audio_bitrate : 128); | |
| 165 | |
| 166 char playlist_path[1024]; | |
| 167 char segment_pattern[1024]; | |
| 168 snprintf(playlist_path, sizeof(playlist_path), "%s/%s.m3u8", output_dir, name); | |
| 169 snprintf(segment_pattern, sizeof(segment_pattern), "%s/%s_%%03d.ts", output_dir, name); | |
| 170 | |
| 171 // Main HLS conversion command | |
| 172 snprintf(cmd, sizeof(cmd), | |
| 173 "ffmpeg -y -i \"%s\" " | |
| 174 "-c:v libx264 -preset fast -crf 22 %s %s " | |
| 175 "-c:a aac %s " | |
| 176 "-f hls " | |
| 177 "-hls_time %d " | |
| 178 "-hls_list_size 0 " | |
| 179 "-hls_segment_filename \"%s\" " | |
| 180 "\"%s\" 2>/dev/null", | |
| 181 input_path, | |
| 182 scale_filter, video_bitrate, | |
| 183 audio_bitrate, | |
| 184 opts->segment_duration, | |
| 185 segment_pattern, | |
| 186 playlist_path); | |
| 187 | |
| 188 int ret = run_command(cmd); | |
| 189 if (ret != 0) { | |
| 190 set_error("FFmpeg HLS conversion failed with code %d", ret); | |
| 191 return FFMPEG_ERR_PROCESS_FAILED; | |
| 192 } | |
| 193 | |
| 194 // Generate thumbnail if requested | |
| 195 if (opts->generate_thumbnail) { | |
| 196 char thumb_path[1024]; | |
| 197 snprintf(thumb_path, sizeof(thumb_path), "%s/%s_thumb.jpg", output_dir, name); | |
| 198 ffmpeg_video_thumbnail(input_path, thumb_path, 1); | |
| 199 } | |
| 200 | |
| 201 return FFMPEG_OK; | |
| 202 } | |
| 203 | |
| 204 FfmpegResult ffmpeg_video_thumbnail(const char* input_path, const char* output_path, int timestamp_seconds) { | |
| 205 if (!input_path || !output_path) { | |
| 206 set_error("Invalid arguments: input_path and output_path required"); | |
| 207 return FFMPEG_ERR_INVALID_ARGS; | |
| 208 } | |
| 209 | |
| 210 if (!file_exists(input_path)) { | |
| 211 set_error("Input file not found: %s", input_path); | |
| 212 return FFMPEG_ERR_INPUT_NOT_FOUND; | |
| 213 } | |
| 214 | |
| 215 char cmd[MAX_CMD_LEN]; | |
| 216 snprintf(cmd, sizeof(cmd), | |
| 217 "ffmpeg -y -i \"%s\" -ss %d -vframes 1 -q:v 2 \"%s\" 2>/dev/null", | |
| 218 input_path, timestamp_seconds, output_path); | |
| 219 | |
| 220 int ret = run_command(cmd); | |
| 221 if (ret != 0) { | |
| 222 set_error("FFmpeg thumbnail generation failed with code %d", ret); | |
| 223 return FFMPEG_ERR_PROCESS_FAILED; | |
| 224 } | |
| 225 | |
| 226 return FFMPEG_OK; | |
| 227 } | |
| 228 | |
| 229 FfmpegResult ffmpeg_get_duration(const char* input_path, double* duration_out) { | |
| 230 if (!input_path || !duration_out) { | |
| 231 set_error("Invalid arguments"); | |
| 232 return FFMPEG_ERR_INVALID_ARGS; | |
| 233 } | |
| 234 | |
| 235 if (!file_exists(input_path)) { | |
| 236 set_error("Input file not found: %s", input_path); | |
| 237 return FFMPEG_ERR_INPUT_NOT_FOUND; | |
| 238 } | |
| 239 | |
| 240 char cmd[MAX_CMD_LEN]; | |
| 241 snprintf(cmd, sizeof(cmd), | |
| 242 "ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 \"%s\" 2>/dev/null", | |
| 243 input_path); | |
| 244 | |
| 245 FILE* fp = popen(cmd, "r"); | |
| 246 if (!fp) { | |
| 247 set_error("Failed to run ffprobe"); | |
| 248 return FFMPEG_ERR_PROCESS_FAILED; | |
| 249 } | |
| 250 | |
| 251 char output[64]; | |
| 252 if (fgets(output, sizeof(output), fp) != NULL) { | |
| 253 *duration_out = atof(output); | |
| 254 } else { | |
| 255 *duration_out = 0; | |
| 256 } | |
| 257 | |
| 258 pclose(fp); | |
| 259 return FFMPEG_OK; | |
| 260 } |