Mercurial
diff 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 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/media_processor/media_processor.c Sun Jan 25 10:44:04 2026 -0800 @@ -0,0 +1,316 @@ +#include "media_processor.h" +#include "third_party/ffmpeg/ffmpeg_cli.h" +#include "dowa/dowa.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <stdarg.h> +#include <time.h> +#include <unistd.h> +#include <sys/stat.h> +#include <errno.h> + +// File extension mappings +static const struct { + const char* ext; + MediaType type; +} EXTENSION_MAP[] = { + // Images + {".jpg", MEDIA_TYPE_IMAGE}, + {".jpeg", MEDIA_TYPE_IMAGE}, + {".png", MEDIA_TYPE_IMAGE}, + {".gif", MEDIA_TYPE_IMAGE}, + {".webp", MEDIA_TYPE_IMAGE}, + {".bmp", MEDIA_TYPE_IMAGE}, + {".tiff", MEDIA_TYPE_IMAGE}, + {".tif", MEDIA_TYPE_IMAGE}, + {".heic", MEDIA_TYPE_IMAGE}, + {".heif", MEDIA_TYPE_IMAGE}, + {".avif", MEDIA_TYPE_IMAGE}, + + // Videos + {".mp4", MEDIA_TYPE_VIDEO}, + {".mov", MEDIA_TYPE_VIDEO}, + {".avi", MEDIA_TYPE_VIDEO}, + {".mkv", MEDIA_TYPE_VIDEO}, + {".webm", MEDIA_TYPE_VIDEO}, + {".flv", MEDIA_TYPE_VIDEO}, + {".wmv", MEDIA_TYPE_VIDEO}, + {".m4v", MEDIA_TYPE_VIDEO}, + {".3gp", MEDIA_TYPE_VIDEO}, + + // Audio + {".mp3", MEDIA_TYPE_AUDIO}, + {".wav", MEDIA_TYPE_AUDIO}, + {".flac", MEDIA_TYPE_AUDIO}, + {".aac", MEDIA_TYPE_AUDIO}, + {".ogg", MEDIA_TYPE_AUDIO}, + {".m4a", MEDIA_TYPE_AUDIO}, + {".wma", MEDIA_TYPE_AUDIO}, + + {NULL, MEDIA_TYPE_UNKNOWN} +}; + +MediaProcessorOpts media_processor_opts_default(const char* output_dir) { + return (MediaProcessorOpts){ + .output_dir = output_dir, + .image_quality = 80, + .image_max_width = 0, + .image_max_height = 0, + .video_segment_duration = 6, + .video_bitrate = 0, + .video_max_width = 0, + .video_max_height = 0, + .generate_thumbnail = true, + }; +} + +const char* media_get_extension(const char* filename) { + if (!filename) return NULL; + const char* dot = strrchr(filename, '.'); + return dot ? dot : NULL; +} + +MediaType media_detect_type(const char* filename) { + const char* ext = media_get_extension(filename); + if (!ext) return MEDIA_TYPE_UNKNOWN; + + // Convert to lowercase for comparison + char ext_lower[16] = {0}; + size_t len = strlen(ext); + if (len >= sizeof(ext_lower)) return MEDIA_TYPE_UNKNOWN; + + for (size_t i = 0; i < len; i++) { + ext_lower[i] = (ext[i] >= 'A' && ext[i] <= 'Z') ? ext[i] + 32 : ext[i]; + } + + for (int i = 0; EXTENSION_MAP[i].ext != NULL; i++) { + if (strcmp(ext_lower, EXTENSION_MAP[i].ext) == 0) { + return EXTENSION_MAP[i].type; + } + } + + return MEDIA_TYPE_UNKNOWN; +} + +const char* media_type_to_mime(MediaType type) { + switch (type) { + case MEDIA_TYPE_IMAGE: return "image/*"; + case MEDIA_TYPE_VIDEO: return "video/*"; + case MEDIA_TYPE_AUDIO: return "audio/*"; + default: return "application/octet-stream"; + } +} + +static MediaResult* create_result(void) { + MediaResult* result = calloc(1, sizeof(MediaResult)); + result->status = MEDIA_STATUS_PENDING; + return result; +} + +static void set_result_error(MediaResult* result, const char* fmt, ...) { + result->status = MEDIA_STATUS_FAILED; + + va_list args; + va_start(args, fmt); + + char buf[512]; + vsnprintf(buf, sizeof(buf), fmt, args); + result->error_message = strdup(buf); + + va_end(args); +} + +void media_result_free(MediaResult* result) { + if (!result) return; + free(result->error_message); + free(result->original_path); + free(result->webp_path); + free(result->hls_playlist); + free(result->thumbnail_path); + free(result); +} + +char* media_generate_id(void) { + static int counter = 0; + char buf[64]; + snprintf(buf, sizeof(buf), "%ld_%d_%d", + (long)time(NULL), getpid(), counter++); + return strdup(buf); +} + +bool media_path_is_safe(const char* path) { + if (!path) return false; + // Check for directory traversal + if (strstr(path, "..") != NULL) return false; + // Check for absolute paths (in uploaded filenames) + if (path[0] == '/') return false; + return true; +} + +static bool ensure_directory(const char* path) { + struct stat st; + if (stat(path, &st) == 0) { + return S_ISDIR(st.st_mode); + } + return mkdir(path, 0755) == 0; +} + +bool media_copy_file(const char* src, const char* dst) { + FILE* in = fopen(src, "rb"); + if (!in) return false; + + FILE* out = fopen(dst, "wb"); + if (!out) { + fclose(in); + return false; + } + + char buf[8192]; + size_t n; + while ((n = fread(buf, 1, sizeof(buf), in)) > 0) { + if (fwrite(buf, 1, n, out) != n) { + fclose(in); + fclose(out); + return false; + } + } + + fclose(in); + fclose(out); + return true; +} + +MediaResult* media_process_image(const char* input_path, const char* id, MediaProcessorOpts* opts) { + MediaResult* result = create_result(); + result->status = MEDIA_STATUS_PROCESSING; + + if (!opts || !opts->output_dir) { + set_result_error(result, "Output directory not specified"); + return result; + } + + if (!ensure_directory(opts->output_dir)) { + set_result_error(result, "Failed to create output directory: %s", opts->output_dir); + return result; + } + + // Create subdirectory for this media + char media_dir[1024]; + snprintf(media_dir, sizeof(media_dir), "%s/%s", opts->output_dir, id); + if (!ensure_directory(media_dir)) { + set_result_error(result, "Failed to create media directory: %s", media_dir); + return result; + } + + // Copy original + const char* ext = media_get_extension(input_path); + char original_path[1024]; + snprintf(original_path, sizeof(original_path), "%s/original%s", media_dir, ext ? ext : ""); + + if (!media_copy_file(input_path, original_path)) { + set_result_error(result, "Failed to copy original file"); + return result; + } + result->original_path = strdup(original_path); + + // Convert to WebP + char webp_path[1024]; + snprintf(webp_path, sizeof(webp_path), "%s/image.webp", media_dir); + + ImageConvertOpts img_opts = ffmpeg_image_opts_default(); + img_opts.quality = opts->image_quality; + img_opts.max_width = opts->image_max_width; + img_opts.max_height = opts->image_max_height; + + FfmpegResult ffmpeg_result = ffmpeg_image_to_webp(input_path, webp_path, &img_opts); + if (ffmpeg_result != FFMPEG_OK) { + set_result_error(result, "FFmpeg conversion failed: %s", ffmpeg_last_error()); + return result; + } + + result->webp_path = strdup(webp_path); + result->status = MEDIA_STATUS_COMPLETED; + return result; +} + +MediaResult* media_process_video(const char* input_path, const char* id, MediaProcessorOpts* opts) { + MediaResult* result = create_result(); + result->status = MEDIA_STATUS_PROCESSING; + + if (!opts || !opts->output_dir) { + set_result_error(result, "Output directory not specified"); + return result; + } + + if (!ensure_directory(opts->output_dir)) { + set_result_error(result, "Failed to create output directory: %s", opts->output_dir); + return result; + } + + // Create subdirectory for this media + char media_dir[1024]; + snprintf(media_dir, sizeof(media_dir), "%s/%s", opts->output_dir, id); + if (!ensure_directory(media_dir)) { + set_result_error(result, "Failed to create media directory: %s", media_dir); + return result; + } + + // Copy original + const char* ext = media_get_extension(input_path); + char original_path[1024]; + snprintf(original_path, sizeof(original_path), "%s/original%s", media_dir, ext ? ext : ""); + + if (!media_copy_file(input_path, original_path)) { + set_result_error(result, "Failed to copy original file"); + return result; + } + result->original_path = strdup(original_path); + + // Get duration + ffmpeg_get_duration(input_path, &result->duration); + + // Convert to HLS + HlsConvertOpts hls_opts = ffmpeg_hls_opts_default(); + hls_opts.segment_duration = opts->video_segment_duration; + hls_opts.video_bitrate = opts->video_bitrate; + hls_opts.max_width = opts->video_max_width; + hls_opts.max_height = opts->video_max_height; + hls_opts.generate_thumbnail = opts->generate_thumbnail; + + FfmpegResult ffmpeg_result = ffmpeg_video_to_hls(input_path, media_dir, "video", &hls_opts); + if (ffmpeg_result != FFMPEG_OK) { + set_result_error(result, "FFmpeg HLS conversion failed: %s", ffmpeg_last_error()); + return result; + } + + char playlist_path[1024]; + snprintf(playlist_path, sizeof(playlist_path), "%s/video.m3u8", media_dir); + result->hls_playlist = strdup(playlist_path); + + if (opts->generate_thumbnail) { + char thumb_path[1024]; + snprintf(thumb_path, sizeof(thumb_path), "%s/video_thumb.jpg", media_dir); + result->thumbnail_path = strdup(thumb_path); + } + + result->status = MEDIA_STATUS_COMPLETED; + return result; +} + +MediaResult* media_process_file(const char* input_path, const char* id, MediaProcessorOpts* opts) { + MediaType type = media_detect_type(input_path); + + switch (type) { + case MEDIA_TYPE_IMAGE: + return media_process_image(input_path, id, opts); + case MEDIA_TYPE_VIDEO: + return media_process_video(input_path, id, opts); + default: { + MediaResult* result = create_result(); + set_result_error(result, "Unsupported media type"); + return result; + } + } +}