diff third_party/ffmpeg/ffmpeg_cli.c @ 242:543df0fe7168

[tools] Add full HLS player support
author MrJuneJune <me@mrjunejune.com>
date Mon, 03 Aug 2026 13:14:41 -0700
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/third_party/ffmpeg/ffmpeg_cli.c	Mon Aug 03 13:14:41 2026 -0700
@@ -0,0 +1,257 @@
+#include "third_party/ffmpeg/ffmpeg_cli.h"
+
+#include <errno.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#define MAX_ERROR_LENGTH 512
+#define MAX_PATH_LENGTH 1024
+
+static char g_last_error[MAX_ERROR_LENGTH];
+
+static void set_error(const char *format, ...)
+{
+  va_list arguments;
+  va_start(arguments, format);
+  vsnprintf(g_last_error, sizeof(g_last_error), format, arguments);
+  va_end(arguments);
+}
+
+static bool file_exists(const char *path)
+{
+  struct stat status;
+  return path && stat(path, &status) == 0 && S_ISREG(status.st_mode);
+}
+
+static bool ensure_directory(const char *path)
+{
+  struct stat status;
+  if (stat(path, &status) == 0)
+    return S_ISDIR(status.st_mode);
+  return mkdir(path, 0755) == 0;
+}
+
+static int run_process(char *const arguments[])
+{
+  pid_t child = fork();
+  if (child < 0)
+  {
+    set_error("fork failed: %s", strerror(errno));
+    return -1;
+  }
+  if (child == 0)
+  {
+    execvp(arguments[0], arguments);
+    _exit(127);
+  }
+
+  int status = 0;
+  while (waitpid(child, &status, 0) < 0)
+  {
+    if (errno == EINTR)
+      continue;
+    set_error("waitpid failed: %s", strerror(errno));
+    return -1;
+  }
+  if (!WIFEXITED(status))
+    return -1;
+  return WEXITSTATUS(status);
+}
+
+Fmp4HlsOpts ffmpeg_fmp4_hls_opts_default(void)
+{
+  return (Fmp4HlsOpts){
+    .segment_duration = 2,
+    .video_bitrate = 900,
+    .audio_bitrate = 96,
+    .max_width = 960,
+    .max_height = 720,
+    .include_audio = true,
+    .use_mpeg_ts = false,
+    .codec = FFMPEG_HLS_VP9_OPUS,
+  };
+}
+
+bool ffmpeg_is_available(void)
+{
+  char *const arguments[] = {
+    "ffmpeg", "-version", NULL,
+  };
+  return run_process(arguments) == 0;
+}
+
+const char *ffmpeg_last_error(void)
+{
+  return g_last_error;
+}
+
+FfmpegResult ffmpeg_video_to_fmp4_hls(
+    const char *input_path,
+    const char *output_dir,
+    const char *name,
+    const Fmp4HlsOpts *options)
+{
+  if (!input_path || !output_dir || !name || name[0] == '\0')
+  {
+    set_error("input path, output directory, and name are 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_directory(output_dir))
+  {
+    set_error("unable to create output directory: %s", output_dir);
+    return FFMPEG_ERR_OUTPUT_FAILED;
+  }
+
+  Fmp4HlsOpts defaults = ffmpeg_fmp4_hls_opts_default();
+  const Fmp4HlsOpts *opts = options ? options : &defaults;
+  if (opts->segment_duration <= 0 ||
+      opts->video_bitrate <= 0 ||
+      opts->max_width <= 0 ||
+      opts->max_height <= 0)
+  {
+    set_error("invalid fMP4 HLS options");
+    return FFMPEG_ERR_INVALID_ARGS;
+  }
+
+  char playlist_path[MAX_PATH_LENGTH];
+  char init_name[256];
+  char segment_pattern[MAX_PATH_LENGTH];
+  char segment_duration[16];
+  char video_bitrate[32];
+  char audio_bitrate[32];
+  char keyframe_expression[32];
+  char scale_filter[256];
+
+  snprintf(
+      playlist_path,
+      sizeof(playlist_path),
+      "%s/%s-stream.m3u8",
+      output_dir,
+      name);
+  snprintf(init_name, sizeof(init_name), "%s-init.mp4", name);
+  snprintf(
+      segment_pattern,
+      sizeof(segment_pattern),
+      "%s/%s-segment%%03d.%s",
+      output_dir,
+      name,
+      opts->use_mpeg_ts ? "ts" : "m4s");
+  snprintf(segment_duration, sizeof(segment_duration), "%d", opts->segment_duration);
+  snprintf(video_bitrate, sizeof(video_bitrate), "%dk", opts->video_bitrate);
+  snprintf(audio_bitrate, sizeof(audio_bitrate), "%dk", opts->audio_bitrate);
+  snprintf(
+      keyframe_expression,
+      sizeof(keyframe_expression),
+      "expr:gte(t,n_forced*%d)",
+      opts->segment_duration);
+  snprintf(
+      scale_filter,
+      sizeof(scale_filter),
+      "scale=w='min(iw,%d)':h='min(ih,%d)':force_original_aspect_ratio=decrease,"
+      "scale=trunc(iw/2)*2:trunc(ih/2)*2",
+      opts->max_width,
+      opts->max_height);
+
+  char *arguments[48];
+  size_t count = 0;
+#define ADD_ARGUMENT(value) arguments[count++] = (char *)(value)
+  ADD_ARGUMENT("ffmpeg");
+  ADD_ARGUMENT("-hide_banner");
+  ADD_ARGUMENT("-loglevel");
+  ADD_ARGUMENT("error");
+  ADD_ARGUMENT("-nostdin");
+  ADD_ARGUMENT("-y");
+  ADD_ARGUMENT("-i");
+  ADD_ARGUMENT(input_path);
+  ADD_ARGUMENT("-map");
+  ADD_ARGUMENT("0:v:0");
+  if (opts->include_audio)
+  {
+    ADD_ARGUMENT("-map");
+    ADD_ARGUMENT("0:a:0?");
+  }
+  ADD_ARGUMENT("-c:v");
+  if (opts->codec == FFMPEG_HLS_H264_AAC)
+  {
+    ADD_ARGUMENT("libx264");
+    ADD_ARGUMENT("-preset");
+    ADD_ARGUMENT("medium");
+    ADD_ARGUMENT("-profile:v");
+    ADD_ARGUMENT("main");
+    ADD_ARGUMENT("-level:v");
+    ADD_ARGUMENT("3.1");
+  }
+  else
+  {
+    ADD_ARGUMENT("libvpx-vp9");
+    ADD_ARGUMENT("-deadline");
+    ADD_ARGUMENT("good");
+    ADD_ARGUMENT("-cpu-used");
+    ADD_ARGUMENT("4");
+  }
+  ADD_ARGUMENT("-pix_fmt");
+  ADD_ARGUMENT("yuv420p");
+  ADD_ARGUMENT("-b:v");
+  ADD_ARGUMENT(video_bitrate);
+  ADD_ARGUMENT("-vf");
+  ADD_ARGUMENT(scale_filter);
+  ADD_ARGUMENT("-force_key_frames");
+  ADD_ARGUMENT(keyframe_expression);
+  if (opts->include_audio)
+  {
+    ADD_ARGUMENT("-c:a");
+    ADD_ARGUMENT(
+        opts->codec == FFMPEG_HLS_H264_AAC
+            ? "aac"
+            : "libopus");
+    ADD_ARGUMENT("-b:a");
+    ADD_ARGUMENT(audio_bitrate);
+  }
+  else
+  {
+    ADD_ARGUMENT("-an");
+  }
+  ADD_ARGUMENT("-f");
+  ADD_ARGUMENT("hls");
+  ADD_ARGUMENT("-hls_time");
+  ADD_ARGUMENT(segment_duration);
+  ADD_ARGUMENT("-hls_playlist_type");
+  ADD_ARGUMENT("vod");
+  if (!opts->use_mpeg_ts)
+  {
+    ADD_ARGUMENT("-hls_segment_type");
+    ADD_ARGUMENT("fmp4");
+    ADD_ARGUMENT("-hls_fmp4_init_filename");
+    ADD_ARGUMENT(init_name);
+  }
+  ADD_ARGUMENT("-hls_segment_filename");
+  ADD_ARGUMENT(segment_pattern);
+  ADD_ARGUMENT(playlist_path);
+  arguments[count] = NULL;
+#undef ADD_ARGUMENT
+
+  int result = run_process(arguments);
+  if (result != 0)
+  {
+    set_error("ffmpeg HLS conversion failed with exit code %d", result);
+    return FFMPEG_ERR_PROCESS_FAILED;
+  }
+  if (!file_exists(playlist_path))
+  {
+    set_error("playlist was not created: %s", playlist_path);
+    return FFMPEG_ERR_OUTPUT_FAILED;
+  }
+
+  return FFMPEG_OK;
+}