|
217
|
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 }
|