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 }