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