|
217
|
1 #include "media_processor.h"
|
|
|
2 #include "third_party/ffmpeg/ffmpeg_cli.h"
|
|
|
3 #include "dowa/dowa.h"
|
|
|
4
|
|
|
5 #include <stdio.h>
|
|
|
6 #include <stdlib.h>
|
|
|
7 #include <string.h>
|
|
|
8 #include <stdarg.h>
|
|
|
9 #include <time.h>
|
|
|
10 #include <unistd.h>
|
|
|
11 #include <sys/stat.h>
|
|
|
12 #include <errno.h>
|
|
|
13
|
|
|
14 // File extension mappings
|
|
|
15 static const struct {
|
|
|
16 const char* ext;
|
|
|
17 MediaType type;
|
|
|
18 } EXTENSION_MAP[] = {
|
|
|
19 // Images
|
|
|
20 {".jpg", MEDIA_TYPE_IMAGE},
|
|
|
21 {".jpeg", MEDIA_TYPE_IMAGE},
|
|
|
22 {".png", MEDIA_TYPE_IMAGE},
|
|
|
23 {".gif", MEDIA_TYPE_IMAGE},
|
|
|
24 {".webp", MEDIA_TYPE_IMAGE},
|
|
|
25 {".bmp", MEDIA_TYPE_IMAGE},
|
|
|
26 {".tiff", MEDIA_TYPE_IMAGE},
|
|
|
27 {".tif", MEDIA_TYPE_IMAGE},
|
|
|
28 {".heic", MEDIA_TYPE_IMAGE},
|
|
|
29 {".heif", MEDIA_TYPE_IMAGE},
|
|
|
30 {".avif", MEDIA_TYPE_IMAGE},
|
|
|
31
|
|
|
32 // Videos
|
|
|
33 {".mp4", MEDIA_TYPE_VIDEO},
|
|
|
34 {".mov", MEDIA_TYPE_VIDEO},
|
|
|
35 {".avi", MEDIA_TYPE_VIDEO},
|
|
|
36 {".mkv", MEDIA_TYPE_VIDEO},
|
|
|
37 {".webm", MEDIA_TYPE_VIDEO},
|
|
|
38 {".flv", MEDIA_TYPE_VIDEO},
|
|
|
39 {".wmv", MEDIA_TYPE_VIDEO},
|
|
|
40 {".m4v", MEDIA_TYPE_VIDEO},
|
|
|
41 {".3gp", MEDIA_TYPE_VIDEO},
|
|
|
42
|
|
|
43 // Audio
|
|
|
44 {".mp3", MEDIA_TYPE_AUDIO},
|
|
|
45 {".wav", MEDIA_TYPE_AUDIO},
|
|
|
46 {".flac", MEDIA_TYPE_AUDIO},
|
|
|
47 {".aac", MEDIA_TYPE_AUDIO},
|
|
|
48 {".ogg", MEDIA_TYPE_AUDIO},
|
|
|
49 {".m4a", MEDIA_TYPE_AUDIO},
|
|
|
50 {".wma", MEDIA_TYPE_AUDIO},
|
|
|
51
|
|
|
52 {NULL, MEDIA_TYPE_UNKNOWN}
|
|
|
53 };
|
|
|
54
|
|
|
55 MediaProcessorOpts media_processor_opts_default(const char* output_dir) {
|
|
|
56 return (MediaProcessorOpts){
|
|
|
57 .output_dir = output_dir,
|
|
|
58 .image_quality = 80,
|
|
|
59 .image_max_width = 0,
|
|
|
60 .image_max_height = 0,
|
|
|
61 .video_segment_duration = 6,
|
|
|
62 .video_bitrate = 0,
|
|
|
63 .video_max_width = 0,
|
|
|
64 .video_max_height = 0,
|
|
|
65 .generate_thumbnail = true,
|
|
|
66 };
|
|
|
67 }
|
|
|
68
|
|
|
69 const char* media_get_extension(const char* filename) {
|
|
|
70 if (!filename) return NULL;
|
|
|
71 const char* dot = strrchr(filename, '.');
|
|
|
72 return dot ? dot : NULL;
|
|
|
73 }
|
|
|
74
|
|
|
75 MediaType media_detect_type(const char* filename) {
|
|
|
76 const char* ext = media_get_extension(filename);
|
|
|
77 if (!ext) return MEDIA_TYPE_UNKNOWN;
|
|
|
78
|
|
|
79 // Convert to lowercase for comparison
|
|
|
80 char ext_lower[16] = {0};
|
|
|
81 size_t len = strlen(ext);
|
|
|
82 if (len >= sizeof(ext_lower)) return MEDIA_TYPE_UNKNOWN;
|
|
|
83
|
|
|
84 for (size_t i = 0; i < len; i++) {
|
|
|
85 ext_lower[i] = (ext[i] >= 'A' && ext[i] <= 'Z') ? ext[i] + 32 : ext[i];
|
|
|
86 }
|
|
|
87
|
|
|
88 for (int i = 0; EXTENSION_MAP[i].ext != NULL; i++) {
|
|
|
89 if (strcmp(ext_lower, EXTENSION_MAP[i].ext) == 0) {
|
|
|
90 return EXTENSION_MAP[i].type;
|
|
|
91 }
|
|
|
92 }
|
|
|
93
|
|
|
94 return MEDIA_TYPE_UNKNOWN;
|
|
|
95 }
|
|
|
96
|
|
|
97 const char* media_type_to_mime(MediaType type) {
|
|
|
98 switch (type) {
|
|
|
99 case MEDIA_TYPE_IMAGE: return "image/*";
|
|
|
100 case MEDIA_TYPE_VIDEO: return "video/*";
|
|
|
101 case MEDIA_TYPE_AUDIO: return "audio/*";
|
|
|
102 default: return "application/octet-stream";
|
|
|
103 }
|
|
|
104 }
|
|
|
105
|
|
|
106 static MediaResult* create_result(void) {
|
|
|
107 MediaResult* result = calloc(1, sizeof(MediaResult));
|
|
|
108 result->status = MEDIA_STATUS_PENDING;
|
|
|
109 return result;
|
|
|
110 }
|
|
|
111
|
|
|
112 static void set_result_error(MediaResult* result, const char* fmt, ...) {
|
|
|
113 result->status = MEDIA_STATUS_FAILED;
|
|
|
114
|
|
|
115 va_list args;
|
|
|
116 va_start(args, fmt);
|
|
|
117
|
|
|
118 char buf[512];
|
|
|
119 vsnprintf(buf, sizeof(buf), fmt, args);
|
|
|
120 result->error_message = strdup(buf);
|
|
|
121
|
|
|
122 va_end(args);
|
|
|
123 }
|
|
|
124
|
|
|
125 void media_result_free(MediaResult* result) {
|
|
|
126 if (!result) return;
|
|
|
127 free(result->error_message);
|
|
|
128 free(result->original_path);
|
|
|
129 free(result->webp_path);
|
|
|
130 free(result->hls_playlist);
|
|
|
131 free(result->thumbnail_path);
|
|
|
132 free(result);
|
|
|
133 }
|
|
|
134
|
|
|
135 char* media_generate_id(void) {
|
|
|
136 static int counter = 0;
|
|
|
137 char buf[64];
|
|
|
138 snprintf(buf, sizeof(buf), "%ld_%d_%d",
|
|
|
139 (long)time(NULL), getpid(), counter++);
|
|
|
140 return strdup(buf);
|
|
|
141 }
|
|
|
142
|
|
|
143 bool media_path_is_safe(const char* path) {
|
|
|
144 if (!path) return false;
|
|
|
145 // Check for directory traversal
|
|
|
146 if (strstr(path, "..") != NULL) return false;
|
|
|
147 // Check for absolute paths (in uploaded filenames)
|
|
|
148 if (path[0] == '/') return false;
|
|
|
149 return true;
|
|
|
150 }
|
|
|
151
|
|
|
152 static bool ensure_directory(const char* path) {
|
|
|
153 struct stat st;
|
|
|
154 if (stat(path, &st) == 0) {
|
|
|
155 return S_ISDIR(st.st_mode);
|
|
|
156 }
|
|
|
157 return mkdir(path, 0755) == 0;
|
|
|
158 }
|
|
|
159
|
|
|
160 bool media_copy_file(const char* src, const char* dst) {
|
|
|
161 FILE* in = fopen(src, "rb");
|
|
|
162 if (!in) return false;
|
|
|
163
|
|
|
164 FILE* out = fopen(dst, "wb");
|
|
|
165 if (!out) {
|
|
|
166 fclose(in);
|
|
|
167 return false;
|
|
|
168 }
|
|
|
169
|
|
|
170 char buf[8192];
|
|
|
171 size_t n;
|
|
|
172 while ((n = fread(buf, 1, sizeof(buf), in)) > 0) {
|
|
|
173 if (fwrite(buf, 1, n, out) != n) {
|
|
|
174 fclose(in);
|
|
|
175 fclose(out);
|
|
|
176 return false;
|
|
|
177 }
|
|
|
178 }
|
|
|
179
|
|
|
180 fclose(in);
|
|
|
181 fclose(out);
|
|
|
182 return true;
|
|
|
183 }
|
|
|
184
|
|
|
185 MediaResult* media_process_image(const char* input_path, const char* id, MediaProcessorOpts* opts) {
|
|
|
186 MediaResult* result = create_result();
|
|
|
187 result->status = MEDIA_STATUS_PROCESSING;
|
|
|
188
|
|
|
189 if (!opts || !opts->output_dir) {
|
|
|
190 set_result_error(result, "Output directory not specified");
|
|
|
191 return result;
|
|
|
192 }
|
|
|
193
|
|
|
194 if (!ensure_directory(opts->output_dir)) {
|
|
|
195 set_result_error(result, "Failed to create output directory: %s", opts->output_dir);
|
|
|
196 return result;
|
|
|
197 }
|
|
|
198
|
|
|
199 // Create subdirectory for this media
|
|
|
200 char media_dir[1024];
|
|
|
201 snprintf(media_dir, sizeof(media_dir), "%s/%s", opts->output_dir, id);
|
|
|
202 if (!ensure_directory(media_dir)) {
|
|
|
203 set_result_error(result, "Failed to create media directory: %s", media_dir);
|
|
|
204 return result;
|
|
|
205 }
|
|
|
206
|
|
|
207 // Copy original
|
|
|
208 const char* ext = media_get_extension(input_path);
|
|
|
209 char original_path[1024];
|
|
|
210 snprintf(original_path, sizeof(original_path), "%s/original%s", media_dir, ext ? ext : "");
|
|
|
211
|
|
|
212 if (!media_copy_file(input_path, original_path)) {
|
|
|
213 set_result_error(result, "Failed to copy original file");
|
|
|
214 return result;
|
|
|
215 }
|
|
|
216 result->original_path = strdup(original_path);
|
|
|
217
|
|
|
218 // Convert to WebP
|
|
|
219 char webp_path[1024];
|
|
|
220 snprintf(webp_path, sizeof(webp_path), "%s/image.webp", media_dir);
|
|
|
221
|
|
|
222 ImageConvertOpts img_opts = ffmpeg_image_opts_default();
|
|
|
223 img_opts.quality = opts->image_quality;
|
|
|
224 img_opts.max_width = opts->image_max_width;
|
|
|
225 img_opts.max_height = opts->image_max_height;
|
|
|
226
|
|
|
227 FfmpegResult ffmpeg_result = ffmpeg_image_to_webp(input_path, webp_path, &img_opts);
|
|
|
228 if (ffmpeg_result != FFMPEG_OK) {
|
|
|
229 set_result_error(result, "FFmpeg conversion failed: %s", ffmpeg_last_error());
|
|
|
230 return result;
|
|
|
231 }
|
|
|
232
|
|
|
233 result->webp_path = strdup(webp_path);
|
|
|
234 result->status = MEDIA_STATUS_COMPLETED;
|
|
|
235 return result;
|
|
|
236 }
|
|
|
237
|
|
|
238 MediaResult* media_process_video(const char* input_path, const char* id, MediaProcessorOpts* opts) {
|
|
|
239 MediaResult* result = create_result();
|
|
|
240 result->status = MEDIA_STATUS_PROCESSING;
|
|
|
241
|
|
|
242 if (!opts || !opts->output_dir) {
|
|
|
243 set_result_error(result, "Output directory not specified");
|
|
|
244 return result;
|
|
|
245 }
|
|
|
246
|
|
|
247 if (!ensure_directory(opts->output_dir)) {
|
|
|
248 set_result_error(result, "Failed to create output directory: %s", opts->output_dir);
|
|
|
249 return result;
|
|
|
250 }
|
|
|
251
|
|
|
252 // Create subdirectory for this media
|
|
|
253 char media_dir[1024];
|
|
|
254 snprintf(media_dir, sizeof(media_dir), "%s/%s", opts->output_dir, id);
|
|
|
255 if (!ensure_directory(media_dir)) {
|
|
|
256 set_result_error(result, "Failed to create media directory: %s", media_dir);
|
|
|
257 return result;
|
|
|
258 }
|
|
|
259
|
|
|
260 // Copy original
|
|
|
261 const char* ext = media_get_extension(input_path);
|
|
|
262 char original_path[1024];
|
|
|
263 snprintf(original_path, sizeof(original_path), "%s/original%s", media_dir, ext ? ext : "");
|
|
|
264
|
|
|
265 if (!media_copy_file(input_path, original_path)) {
|
|
|
266 set_result_error(result, "Failed to copy original file");
|
|
|
267 return result;
|
|
|
268 }
|
|
|
269 result->original_path = strdup(original_path);
|
|
|
270
|
|
|
271 // Get duration
|
|
|
272 ffmpeg_get_duration(input_path, &result->duration);
|
|
|
273
|
|
|
274 // Convert to HLS
|
|
|
275 HlsConvertOpts hls_opts = ffmpeg_hls_opts_default();
|
|
|
276 hls_opts.segment_duration = opts->video_segment_duration;
|
|
|
277 hls_opts.video_bitrate = opts->video_bitrate;
|
|
|
278 hls_opts.max_width = opts->video_max_width;
|
|
|
279 hls_opts.max_height = opts->video_max_height;
|
|
|
280 hls_opts.generate_thumbnail = opts->generate_thumbnail;
|
|
|
281
|
|
|
282 FfmpegResult ffmpeg_result = ffmpeg_video_to_hls(input_path, media_dir, "video", &hls_opts);
|
|
|
283 if (ffmpeg_result != FFMPEG_OK) {
|
|
|
284 set_result_error(result, "FFmpeg HLS conversion failed: %s", ffmpeg_last_error());
|
|
|
285 return result;
|
|
|
286 }
|
|
|
287
|
|
|
288 char playlist_path[1024];
|
|
|
289 snprintf(playlist_path, sizeof(playlist_path), "%s/video.m3u8", media_dir);
|
|
|
290 result->hls_playlist = strdup(playlist_path);
|
|
|
291
|
|
|
292 if (opts->generate_thumbnail) {
|
|
|
293 char thumb_path[1024];
|
|
|
294 snprintf(thumb_path, sizeof(thumb_path), "%s/video_thumb.jpg", media_dir);
|
|
|
295 result->thumbnail_path = strdup(thumb_path);
|
|
|
296 }
|
|
|
297
|
|
|
298 result->status = MEDIA_STATUS_COMPLETED;
|
|
|
299 return result;
|
|
|
300 }
|
|
|
301
|
|
|
302 MediaResult* media_process_file(const char* input_path, const char* id, MediaProcessorOpts* opts) {
|
|
|
303 MediaType type = media_detect_type(input_path);
|
|
|
304
|
|
|
305 switch (type) {
|
|
|
306 case MEDIA_TYPE_IMAGE:
|
|
|
307 return media_process_image(input_path, id, opts);
|
|
|
308 case MEDIA_TYPE_VIDEO:
|
|
|
309 return media_process_video(input_path, id, opts);
|
|
|
310 default: {
|
|
|
311 MediaResult* result = create_result();
|
|
|
312 set_result_error(result, "Unsupported media type");
|
|
|
313 return result;
|
|
|
314 }
|
|
|
315 }
|
|
|
316 }
|