|
13 | 13 | #include <stdlib.h> |
14 | 14 | #include <string.h> |
15 | 15 | #include <sys/stat.h> |
16 | | -#include <sys/wait.h> |
17 | 16 | #include <unistd.h> |
18 | | -#include <fcntl.h> |
19 | 17 | #include <time.h> |
20 | 18 | #include <errno.h> |
21 | | -#include <signal.h> |
22 | 19 |
|
23 | 20 | #include "web/api_handlers_recordings_thumbnail.h" |
24 | 21 | #include "web/request_response.h" |
@@ -80,89 +77,6 @@ static void thumbnail_complete_callback(deferred_action_handle_t handle, |
80 | 77 | } |
81 | 78 | } |
82 | 79 |
|
83 | | -/** |
84 | | - * @brief Generate a thumbnail using ffmpeg |
85 | | - */ |
86 | | -static int generate_thumbnail(const char *input_path, const char *output_path, |
87 | | - double seek_seconds) { |
88 | | - // Clamp seek time to 0 minimum |
89 | | - if (seek_seconds < 0) seek_seconds = 0; |
90 | | - |
91 | | - char seek_str[32]; |
92 | | - snprintf(seek_str, sizeof(seek_str), "%.2f", seek_seconds); |
93 | | - |
94 | | - log_debug("Generating thumbnail: ffmpeg -ss %s -i \"%s\" -> \"%s\"", |
95 | | - seek_str, input_path, output_path); |
96 | | - |
97 | | - // Use fork/execvp instead of system() to avoid spawning a shell |
98 | | - pid_t pid = fork(); |
99 | | - if (pid < 0) { |
100 | | - log_warn("fork() failed for thumbnail generation: %s", strerror(errno)); |
101 | | - return -1; |
102 | | - } |
103 | | - |
104 | | - if (pid == 0) { |
105 | | - // Child process: redirect stderr to /dev/null, then exec ffmpeg |
106 | | - int devnull = open("/dev/null", O_WRONLY); |
107 | | - if (devnull >= 0) { |
108 | | - dup2(devnull, STDERR_FILENO); |
109 | | - close(devnull); |
110 | | - } |
111 | | - const char *args[] = { |
112 | | - "ffmpeg", "-ss", seek_str, |
113 | | - "-i", input_path, |
114 | | - "-frames:v", "1", |
115 | | - "-vf", "scale=320:-1", |
116 | | - "-q:v", "8", |
117 | | - "-y", output_path, |
118 | | - NULL |
119 | | - }; |
120 | | - execvp("ffmpeg", (char *const *)args); |
121 | | - _exit(127); |
122 | | - } |
123 | | - |
124 | | - // Parent: wait up to 5 seconds for child to complete |
125 | | - int ret = -1; |
126 | | - time_t deadline = time(NULL) + 5; |
127 | | - while (time(NULL) < deadline) { |
128 | | - int status = 0; |
129 | | - pid_t result = waitpid(pid, &status, WNOHANG); |
130 | | - if (result == pid) { |
131 | | - if (WIFEXITED(status) && WEXITSTATUS(status) == 0) { |
132 | | - ret = 0; |
133 | | - } |
134 | | - pid = -1; |
135 | | - break; |
136 | | - } |
137 | | - if (result < 0) { |
138 | | - pid = -1; |
139 | | - break; |
140 | | - } |
141 | | - usleep(50000); |
142 | | - } |
143 | | - if (pid > 0) { |
144 | | - kill(pid, SIGKILL); |
145 | | - waitpid(pid, NULL, 0); |
146 | | - } |
147 | | - |
148 | | - if (ret != 0) { |
149 | | - log_warn("ffmpeg thumbnail generation failed for: %s", input_path); |
150 | | - return -1; |
151 | | - } |
152 | | - |
153 | | - // Verify the file was created |
154 | | - struct stat st; |
155 | | - if (stat(output_path, &st) != 0 || st.st_size == 0) { |
156 | | - log_warn("Thumbnail file not created or empty: %s", output_path); |
157 | | - unlink(output_path); // Clean up empty file |
158 | | - return -1; |
159 | | - } |
160 | | - |
161 | | - log_debug("Generated thumbnail: %s (%lld bytes)", output_path, |
162 | | - (long long)st.st_size); |
163 | | - return 0; |
164 | | -} |
165 | | - |
166 | 80 | void handle_recordings_thumbnail(const http_request_t *req, http_response_t *res) { |
167 | 81 | if (!req || !res) { |
168 | 82 | log_error("Invalid parameters for handle_recordings_thumbnail"); |
@@ -220,6 +134,14 @@ void handle_recordings_thumbnail(const http_request_t *req, http_response_t *res |
220 | 134 | http_response_set_json_error(res, 400, "Invalid thumbnail index (must be 0, 1, or 2)"); |
221 | 135 | return; |
222 | 136 | } |
| 137 | + // When thumbnails_per_recording is 1 (CPU-save mode, #364) the client |
| 138 | + // shouldn't be requesting hover frames at all. Reject cleanly so a |
| 139 | + // stale frontend can't force extra ffmpeg work. |
| 140 | + if (index > 0 && g_config.thumbnails_per_recording <= 1) { |
| 141 | + http_response_set_json_error(res, 403, |
| 142 | + "Hover frames disabled (thumbnails_per_recording=1)"); |
| 143 | + return; |
| 144 | + } |
223 | 145 |
|
224 | 146 | // Build thumbnail path |
225 | 147 | char thumb_path[MAX_PATH_LENGTH]; |
@@ -267,7 +189,12 @@ void handle_recordings_thumbnail(const http_request_t *req, http_response_t *res |
267 | 189 | double seek_seconds; |
268 | 190 | switch (index) { |
269 | 191 | case 0: |
270 | | - seek_seconds = 1.0; // 1 second in (avoid black frames at start) |
| 192 | + // First frame — no seek at all. On slow CPUs (#364) the seek |
| 193 | + // dominates the cost, and the earlier "1s in to dodge black |
| 194 | + // frames" heuristic isn't worth it: modern IP cameras don't |
| 195 | + // emit black intros, and the mount-time thumbnail is the one |
| 196 | + // users see by default, so it must be the cheapest path. |
| 197 | + seek_seconds = 0.0; |
271 | 198 | break; |
272 | 199 | case 1: |
273 | 200 | seek_seconds = duration / 2.0; |
|
0 commit comments