Mercurial
comparison media_processor/media_processor.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 "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 } |