diff third_party/ffmpeg/ffmpeg_cli.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/third_party/ffmpeg/ffmpeg_cli.c	Sun Jan 25 10:44:04 2026 -0800
@@ -0,0 +1,260 @@
+#include "ffmpeg_cli.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdarg.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <errno.h>
+
+#define MAX_CMD_LEN 4096
+#define MAX_ERROR_LEN 512
+
+static char last_error[MAX_ERROR_LEN] = {0};
+
+static void set_error(const char* fmt, ...) {
+    va_list args;
+    va_start(args, fmt);
+    vsnprintf(last_error, MAX_ERROR_LEN, fmt, args);
+    va_end(args);
+}
+
+static bool file_exists(const char* path) {
+    struct stat st;
+    return stat(path, &st) == 0;
+}
+
+static bool ensure_dir(const char* path) {
+    struct stat st;
+    if (stat(path, &st) == 0) {
+        return S_ISDIR(st.st_mode);
+    }
+    return mkdir(path, 0755) == 0;
+}
+
+static int run_command(const char* cmd) {
+    int ret = system(cmd);
+    if (ret == -1) {
+        set_error("Failed to execute command: %s", strerror(errno));
+        return -1;
+    }
+    return WEXITSTATUS(ret);
+}
+
+ImageConvertOpts ffmpeg_image_opts_default(void) {
+    return (ImageConvertOpts){
+        .quality = 80,
+        .max_width = 0,
+        .max_height = 0,
+        .preserve_aspect = true,
+    };
+}
+
+HlsConvertOpts ffmpeg_hls_opts_default(void) {
+    return (HlsConvertOpts){
+        .segment_duration = 6,
+        .video_bitrate = 0,
+        .audio_bitrate = 128,
+        .max_width = 0,
+        .max_height = 0,
+        .generate_thumbnail = true,
+    };
+}
+
+bool ffmpeg_is_available(void) {
+    return system("ffmpeg -version > /dev/null 2>&1") == 0;
+}
+
+const char* ffmpeg_last_error(void) {
+    return last_error;
+}
+
+FfmpegResult ffmpeg_image_to_webp(const char* input_path, const char* output_path, ImageConvertOpts* opts) {
+    if (!input_path || !output_path) {
+        set_error("Invalid arguments: input_path and output_path required");
+        return FFMPEG_ERR_INVALID_ARGS;
+    }
+
+    if (!file_exists(input_path)) {
+        set_error("Input file not found: %s", input_path);
+        return FFMPEG_ERR_INPUT_NOT_FOUND;
+    }
+
+    ImageConvertOpts default_opts = ffmpeg_image_opts_default();
+    if (!opts) opts = &default_opts;
+
+    char cmd[MAX_CMD_LEN];
+    char scale_filter[256] = "";
+
+    // Build scale filter if resizing
+    if (opts->max_width > 0 || opts->max_height > 0) {
+        int w = opts->max_width > 0 ? opts->max_width : -1;
+        int h = opts->max_height > 0 ? opts->max_height : -1;
+
+        if (opts->preserve_aspect) {
+            if (w > 0 && h > 0) {
+                snprintf(scale_filter, sizeof(scale_filter),
+                    "-vf \"scale='min(%d,iw)':min'(%d,ih)':force_original_aspect_ratio=decrease\"", w, h);
+            } else if (w > 0) {
+                snprintf(scale_filter, sizeof(scale_filter), "-vf \"scale='min(%d,iw)':-1\"", w);
+            } else {
+                snprintf(scale_filter, sizeof(scale_filter), "-vf \"scale=-1:'min(%d,ih)'\"", h);
+            }
+        } else {
+            snprintf(scale_filter, sizeof(scale_filter), "-vf \"scale=%d:%d\"", w, h);
+        }
+    }
+
+    snprintf(cmd, sizeof(cmd),
+        "ffmpeg -y -i \"%s\" %s -quality %d \"%s\" 2>/dev/null",
+        input_path, scale_filter, opts->quality, output_path);
+
+    int ret = run_command(cmd);
+    if (ret != 0) {
+        set_error("FFmpeg conversion failed with code %d", ret);
+        return FFMPEG_ERR_PROCESS_FAILED;
+    }
+
+    if (!file_exists(output_path)) {
+        set_error("Output file was not created");
+        return FFMPEG_ERR_OUTPUT_FAILED;
+    }
+
+    return FFMPEG_OK;
+}
+
+FfmpegResult ffmpeg_video_to_hls(const char* input_path, const char* output_dir, const char* name, HlsConvertOpts* opts) {
+    if (!input_path || !output_dir || !name) {
+        set_error("Invalid arguments: input_path, output_dir, and name required");
+        return FFMPEG_ERR_INVALID_ARGS;
+    }
+
+    if (!file_exists(input_path)) {
+        set_error("Input file not found: %s", input_path);
+        return FFMPEG_ERR_INPUT_NOT_FOUND;
+    }
+
+    if (!ensure_dir(output_dir)) {
+        set_error("Failed to create output directory: %s", output_dir);
+        return FFMPEG_ERR_OUTPUT_FAILED;
+    }
+
+    HlsConvertOpts default_opts = ffmpeg_hls_opts_default();
+    if (!opts) opts = &default_opts;
+
+    char cmd[MAX_CMD_LEN];
+    char scale_filter[256] = "";
+    char video_bitrate[64] = "";
+    char audio_bitrate[64] = "";
+
+    // Build scale filter if resizing
+    if (opts->max_width > 0 || opts->max_height > 0) {
+        int w = opts->max_width > 0 ? opts->max_width : -2;
+        int h = opts->max_height > 0 ? opts->max_height : -2;
+        snprintf(scale_filter, sizeof(scale_filter), "-vf \"scale=%d:%d\"", w, h);
+    }
+
+    // Video bitrate
+    if (opts->video_bitrate > 0) {
+        snprintf(video_bitrate, sizeof(video_bitrate), "-b:v %dk", opts->video_bitrate);
+    }
+
+    // Audio bitrate
+    snprintf(audio_bitrate, sizeof(audio_bitrate), "-b:a %dk",
+        opts->audio_bitrate > 0 ? opts->audio_bitrate : 128);
+
+    char playlist_path[1024];
+    char segment_pattern[1024];
+    snprintf(playlist_path, sizeof(playlist_path), "%s/%s.m3u8", output_dir, name);
+    snprintf(segment_pattern, sizeof(segment_pattern), "%s/%s_%%03d.ts", output_dir, name);
+
+    // Main HLS conversion command
+    snprintf(cmd, sizeof(cmd),
+        "ffmpeg -y -i \"%s\" "
+        "-c:v libx264 -preset fast -crf 22 %s %s "
+        "-c:a aac %s "
+        "-f hls "
+        "-hls_time %d "
+        "-hls_list_size 0 "
+        "-hls_segment_filename \"%s\" "
+        "\"%s\" 2>/dev/null",
+        input_path,
+        scale_filter, video_bitrate,
+        audio_bitrate,
+        opts->segment_duration,
+        segment_pattern,
+        playlist_path);
+
+    int ret = run_command(cmd);
+    if (ret != 0) {
+        set_error("FFmpeg HLS conversion failed with code %d", ret);
+        return FFMPEG_ERR_PROCESS_FAILED;
+    }
+
+    // Generate thumbnail if requested
+    if (opts->generate_thumbnail) {
+        char thumb_path[1024];
+        snprintf(thumb_path, sizeof(thumb_path), "%s/%s_thumb.jpg", output_dir, name);
+        ffmpeg_video_thumbnail(input_path, thumb_path, 1);
+    }
+
+    return FFMPEG_OK;
+}
+
+FfmpegResult ffmpeg_video_thumbnail(const char* input_path, const char* output_path, int timestamp_seconds) {
+    if (!input_path || !output_path) {
+        set_error("Invalid arguments: input_path and output_path required");
+        return FFMPEG_ERR_INVALID_ARGS;
+    }
+
+    if (!file_exists(input_path)) {
+        set_error("Input file not found: %s", input_path);
+        return FFMPEG_ERR_INPUT_NOT_FOUND;
+    }
+
+    char cmd[MAX_CMD_LEN];
+    snprintf(cmd, sizeof(cmd),
+        "ffmpeg -y -i \"%s\" -ss %d -vframes 1 -q:v 2 \"%s\" 2>/dev/null",
+        input_path, timestamp_seconds, output_path);
+
+    int ret = run_command(cmd);
+    if (ret != 0) {
+        set_error("FFmpeg thumbnail generation failed with code %d", ret);
+        return FFMPEG_ERR_PROCESS_FAILED;
+    }
+
+    return FFMPEG_OK;
+}
+
+FfmpegResult ffmpeg_get_duration(const char* input_path, double* duration_out) {
+    if (!input_path || !duration_out) {
+        set_error("Invalid arguments");
+        return FFMPEG_ERR_INVALID_ARGS;
+    }
+
+    if (!file_exists(input_path)) {
+        set_error("Input file not found: %s", input_path);
+        return FFMPEG_ERR_INPUT_NOT_FOUND;
+    }
+
+    char cmd[MAX_CMD_LEN];
+    snprintf(cmd, sizeof(cmd),
+        "ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 \"%s\" 2>/dev/null",
+        input_path);
+
+    FILE* fp = popen(cmd, "r");
+    if (!fp) {
+        set_error("Failed to run ffprobe");
+        return FFMPEG_ERR_PROCESS_FAILED;
+    }
+
+    char output[64];
+    if (fgets(output, sizeof(output), fp) != NULL) {
+        *duration_out = atof(output);
+    } else {
+        *duration_out = 0;
+    }
+
+    pclose(fp);
+    return FFMPEG_OK;
+}