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 }