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;
+        }
+    }
+}