Mercurial
comparison 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 |
comparison
equal
deleted
inserted
replaced
| 241:9c2eec61a152 | 242:543df0fe7168 |
|---|---|
| 1 #include "third_party/ffmpeg/ffmpeg_cli.h" | |
| 2 | |
| 3 #include <errno.h> | |
| 4 #include <stdarg.h> | |
| 5 #include <stdio.h> | |
| 6 #include <stdlib.h> | |
| 7 #include <string.h> | |
| 8 #include <sys/stat.h> | |
| 9 #include <sys/types.h> | |
| 10 #include <sys/wait.h> | |
| 11 #include <unistd.h> | |
| 12 | |
| 13 #define MAX_ERROR_LENGTH 512 | |
| 14 #define MAX_PATH_LENGTH 1024 | |
| 15 | |
| 16 static char g_last_error[MAX_ERROR_LENGTH]; | |
| 17 | |
| 18 static void set_error(const char *format, ...) | |
| 19 { | |
| 20 va_list arguments; | |
| 21 va_start(arguments, format); | |
| 22 vsnprintf(g_last_error, sizeof(g_last_error), format, arguments); | |
| 23 va_end(arguments); | |
| 24 } | |
| 25 | |
| 26 static bool file_exists(const char *path) | |
| 27 { | |
| 28 struct stat status; | |
| 29 return path && stat(path, &status) == 0 && S_ISREG(status.st_mode); | |
| 30 } | |
| 31 | |
| 32 static bool ensure_directory(const char *path) | |
| 33 { | |
| 34 struct stat status; | |
| 35 if (stat(path, &status) == 0) | |
| 36 return S_ISDIR(status.st_mode); | |
| 37 return mkdir(path, 0755) == 0; | |
| 38 } | |
| 39 | |
| 40 static int run_process(char *const arguments[]) | |
| 41 { | |
| 42 pid_t child = fork(); | |
| 43 if (child < 0) | |
| 44 { | |
| 45 set_error("fork failed: %s", strerror(errno)); | |
| 46 return -1; | |
| 47 } | |
| 48 if (child == 0) | |
| 49 { | |
| 50 execvp(arguments[0], arguments); | |
| 51 _exit(127); | |
| 52 } | |
| 53 | |
| 54 int status = 0; | |
| 55 while (waitpid(child, &status, 0) < 0) | |
| 56 { | |
| 57 if (errno == EINTR) | |
| 58 continue; | |
| 59 set_error("waitpid failed: %s", strerror(errno)); | |
| 60 return -1; | |
| 61 } | |
| 62 if (!WIFEXITED(status)) | |
| 63 return -1; | |
| 64 return WEXITSTATUS(status); | |
| 65 } | |
| 66 | |
| 67 Fmp4HlsOpts ffmpeg_fmp4_hls_opts_default(void) | |
| 68 { | |
| 69 return (Fmp4HlsOpts){ | |
| 70 .segment_duration = 2, | |
| 71 .video_bitrate = 900, | |
| 72 .audio_bitrate = 96, | |
| 73 .max_width = 960, | |
| 74 .max_height = 720, | |
| 75 .include_audio = true, | |
| 76 .use_mpeg_ts = false, | |
| 77 .codec = FFMPEG_HLS_VP9_OPUS, | |
| 78 }; | |
| 79 } | |
| 80 | |
| 81 bool ffmpeg_is_available(void) | |
| 82 { | |
| 83 char *const arguments[] = { | |
| 84 "ffmpeg", "-version", NULL, | |
| 85 }; | |
| 86 return run_process(arguments) == 0; | |
| 87 } | |
| 88 | |
| 89 const char *ffmpeg_last_error(void) | |
| 90 { | |
| 91 return g_last_error; | |
| 92 } | |
| 93 | |
| 94 FfmpegResult ffmpeg_video_to_fmp4_hls( | |
| 95 const char *input_path, | |
| 96 const char *output_dir, | |
| 97 const char *name, | |
| 98 const Fmp4HlsOpts *options) | |
| 99 { | |
| 100 if (!input_path || !output_dir || !name || name[0] == '\0') | |
| 101 { | |
| 102 set_error("input path, output directory, and name are required"); | |
| 103 return FFMPEG_ERR_INVALID_ARGS; | |
| 104 } | |
| 105 if (!file_exists(input_path)) | |
| 106 { | |
| 107 set_error("input file not found: %s", input_path); | |
| 108 return FFMPEG_ERR_INPUT_NOT_FOUND; | |
| 109 } | |
| 110 if (!ensure_directory(output_dir)) | |
| 111 { | |
| 112 set_error("unable to create output directory: %s", output_dir); | |
| 113 return FFMPEG_ERR_OUTPUT_FAILED; | |
| 114 } | |
| 115 | |
| 116 Fmp4HlsOpts defaults = ffmpeg_fmp4_hls_opts_default(); | |
| 117 const Fmp4HlsOpts *opts = options ? options : &defaults; | |
| 118 if (opts->segment_duration <= 0 || | |
| 119 opts->video_bitrate <= 0 || | |
| 120 opts->max_width <= 0 || | |
| 121 opts->max_height <= 0) | |
| 122 { | |
| 123 set_error("invalid fMP4 HLS options"); | |
| 124 return FFMPEG_ERR_INVALID_ARGS; | |
| 125 } | |
| 126 | |
| 127 char playlist_path[MAX_PATH_LENGTH]; | |
| 128 char init_name[256]; | |
| 129 char segment_pattern[MAX_PATH_LENGTH]; | |
| 130 char segment_duration[16]; | |
| 131 char video_bitrate[32]; | |
| 132 char audio_bitrate[32]; | |
| 133 char keyframe_expression[32]; | |
| 134 char scale_filter[256]; | |
| 135 | |
| 136 snprintf( | |
| 137 playlist_path, | |
| 138 sizeof(playlist_path), | |
| 139 "%s/%s-stream.m3u8", | |
| 140 output_dir, | |
| 141 name); | |
| 142 snprintf(init_name, sizeof(init_name), "%s-init.mp4", name); | |
| 143 snprintf( | |
| 144 segment_pattern, | |
| 145 sizeof(segment_pattern), | |
| 146 "%s/%s-segment%%03d.%s", | |
| 147 output_dir, | |
| 148 name, | |
| 149 opts->use_mpeg_ts ? "ts" : "m4s"); | |
| 150 snprintf(segment_duration, sizeof(segment_duration), "%d", opts->segment_duration); | |
| 151 snprintf(video_bitrate, sizeof(video_bitrate), "%dk", opts->video_bitrate); | |
| 152 snprintf(audio_bitrate, sizeof(audio_bitrate), "%dk", opts->audio_bitrate); | |
| 153 snprintf( | |
| 154 keyframe_expression, | |
| 155 sizeof(keyframe_expression), | |
| 156 "expr:gte(t,n_forced*%d)", | |
| 157 opts->segment_duration); | |
| 158 snprintf( | |
| 159 scale_filter, | |
| 160 sizeof(scale_filter), | |
| 161 "scale=w='min(iw,%d)':h='min(ih,%d)':force_original_aspect_ratio=decrease," | |
| 162 "scale=trunc(iw/2)*2:trunc(ih/2)*2", | |
| 163 opts->max_width, | |
| 164 opts->max_height); | |
| 165 | |
| 166 char *arguments[48]; | |
| 167 size_t count = 0; | |
| 168 #define ADD_ARGUMENT(value) arguments[count++] = (char *)(value) | |
| 169 ADD_ARGUMENT("ffmpeg"); | |
| 170 ADD_ARGUMENT("-hide_banner"); | |
| 171 ADD_ARGUMENT("-loglevel"); | |
| 172 ADD_ARGUMENT("error"); | |
| 173 ADD_ARGUMENT("-nostdin"); | |
| 174 ADD_ARGUMENT("-y"); | |
| 175 ADD_ARGUMENT("-i"); | |
| 176 ADD_ARGUMENT(input_path); | |
| 177 ADD_ARGUMENT("-map"); | |
| 178 ADD_ARGUMENT("0:v:0"); | |
| 179 if (opts->include_audio) | |
| 180 { | |
| 181 ADD_ARGUMENT("-map"); | |
| 182 ADD_ARGUMENT("0:a:0?"); | |
| 183 } | |
| 184 ADD_ARGUMENT("-c:v"); | |
| 185 if (opts->codec == FFMPEG_HLS_H264_AAC) | |
| 186 { | |
| 187 ADD_ARGUMENT("libx264"); | |
| 188 ADD_ARGUMENT("-preset"); | |
| 189 ADD_ARGUMENT("medium"); | |
| 190 ADD_ARGUMENT("-profile:v"); | |
| 191 ADD_ARGUMENT("main"); | |
| 192 ADD_ARGUMENT("-level:v"); | |
| 193 ADD_ARGUMENT("3.1"); | |
| 194 } | |
| 195 else | |
| 196 { | |
| 197 ADD_ARGUMENT("libvpx-vp9"); | |
| 198 ADD_ARGUMENT("-deadline"); | |
| 199 ADD_ARGUMENT("good"); | |
| 200 ADD_ARGUMENT("-cpu-used"); | |
| 201 ADD_ARGUMENT("4"); | |
| 202 } | |
| 203 ADD_ARGUMENT("-pix_fmt"); | |
| 204 ADD_ARGUMENT("yuv420p"); | |
| 205 ADD_ARGUMENT("-b:v"); | |
| 206 ADD_ARGUMENT(video_bitrate); | |
| 207 ADD_ARGUMENT("-vf"); | |
| 208 ADD_ARGUMENT(scale_filter); | |
| 209 ADD_ARGUMENT("-force_key_frames"); | |
| 210 ADD_ARGUMENT(keyframe_expression); | |
| 211 if (opts->include_audio) | |
| 212 { | |
| 213 ADD_ARGUMENT("-c:a"); | |
| 214 ADD_ARGUMENT( | |
| 215 opts->codec == FFMPEG_HLS_H264_AAC | |
| 216 ? "aac" | |
| 217 : "libopus"); | |
| 218 ADD_ARGUMENT("-b:a"); | |
| 219 ADD_ARGUMENT(audio_bitrate); | |
| 220 } | |
| 221 else | |
| 222 { | |
| 223 ADD_ARGUMENT("-an"); | |
| 224 } | |
| 225 ADD_ARGUMENT("-f"); | |
| 226 ADD_ARGUMENT("hls"); | |
| 227 ADD_ARGUMENT("-hls_time"); | |
| 228 ADD_ARGUMENT(segment_duration); | |
| 229 ADD_ARGUMENT("-hls_playlist_type"); | |
| 230 ADD_ARGUMENT("vod"); | |
| 231 if (!opts->use_mpeg_ts) | |
| 232 { | |
| 233 ADD_ARGUMENT("-hls_segment_type"); | |
| 234 ADD_ARGUMENT("fmp4"); | |
| 235 ADD_ARGUMENT("-hls_fmp4_init_filename"); | |
| 236 ADD_ARGUMENT(init_name); | |
| 237 } | |
| 238 ADD_ARGUMENT("-hls_segment_filename"); | |
| 239 ADD_ARGUMENT(segment_pattern); | |
| 240 ADD_ARGUMENT(playlist_path); | |
| 241 arguments[count] = NULL; | |
| 242 #undef ADD_ARGUMENT | |
| 243 | |
| 244 int result = run_process(arguments); | |
| 245 if (result != 0) | |
| 246 { | |
| 247 set_error("ffmpeg HLS conversion failed with exit code %d", result); | |
| 248 return FFMPEG_ERR_PROCESS_FAILED; | |
| 249 } | |
| 250 if (!file_exists(playlist_path)) | |
| 251 { | |
| 252 set_error("playlist was not created: %s", playlist_path); | |
| 253 return FFMPEG_ERR_OUTPUT_FAILED; | |
| 254 } | |
| 255 | |
| 256 return FFMPEG_OK; | |
| 257 } |