1818#include "video/stream_manager.h"
1919#include "core/logger.h"
2020#include "core/config.h"
21+ #include "core/path_utils.h"
2122#include "core/shutdown_coordinator.h"
2223#include "database/database_manager.h"
2324#include "database/db_recordings.h"
@@ -125,27 +126,6 @@ static int push_event(const motion_event_t *event) {
125126// 0 = event successfully dequeued
126127// -1 = shutting down (no event)
127128// -2 = timeout (no event within 1 s), caller should tick the state machine
128- //
129- // FIX: recording stop bug - the old implementation used pthread_cond_wait
130- // (blocking forever), so update_recording_state() was never called again
131- // after a "no motion" event, and recordings never stopped.
132- // The new implementation uses a 1-second timedwait so the event loop can
133- // periodically tick the state machine even when no new events arrive.
134- //
135- // OLD CODE (kept for reference):
136- // static int pop_event(motion_event_t *event) {
137- // if (!event) {
138- // return -1;
139- // }
140- // pthread_mutex_lock(&event_queue.mutex);
141- // // Wait for events if queue is empty
142- // while (event_queue.count == 0 && event_processor_running) {
143- // pthread_cond_wait(&event_queue.cond, &event_queue.mutex); // <-- bug: blocks forever
144- // }
145- // if (event_queue.count == 0) {
146- // pthread_mutex_unlock(&event_queue.mutex);
147- // return -1;
148- // }
149129static int pop_event (motion_event_t * event ) {
150130 if (!event ) {
151131 return -1 ;
@@ -158,13 +138,23 @@ static int pop_event(motion_event_t *event) {
158138 // RECORDING -> FINALIZING -> stop after a "no motion" event).
159139 while (event_queue .count == 0 && event_processor_running ) {
160140 struct timespec ts ;
161- clock_gettime (CLOCK_REALTIME , & ts );
141+ if (clock_gettime (CLOCK_REALTIME , & ts ) != 0 ) {
142+ // clock_gettime failed — fall back to a short sleep
143+ pthread_mutex_unlock (& event_queue .mutex );
144+ return -2 ;
145+ }
162146 ts .tv_sec += 1 ;
163147 int rc = pthread_cond_timedwait (& event_queue .cond , & event_queue .mutex , & ts );
164148 if (rc == ETIMEDOUT ) {
165149 pthread_mutex_unlock (& event_queue .mutex );
166150 return -2 ; // periodic tick
167151 }
152+ if (rc != 0 && rc != ETIMEDOUT ) {
153+ // Unexpected error (e.g. EINVAL) — treat as a tick to avoid tight loop
154+ log_warn ("pthread_cond_timedwait unexpected error: %d" , rc );
155+ pthread_mutex_unlock (& event_queue .mutex );
156+ return -2 ;
157+ }
168158 }
169159
170160 if (event_queue .count == 0 ) {
@@ -270,31 +260,35 @@ static int generate_recording_path(const char *stream_name, char *path, size_t p
270260 char timestamp [32 ];
271261 strftime (timestamp , sizeof (timestamp ), "%Y%m%d_%H%M%S" , tm_info );
272262
263+ // Sanitize the stream name to prevent path traversal and handle spaces.
264+ char sanitized_name [MAX_STREAM_NAME ];
265+ sanitize_stream_name (stream_name , sanitized_name , MAX_STREAM_NAME );
266+
273267 // Create directory structure: /recordings/camera_name/YYYY/MM/DD/
274268 char year [8 ], month [4 ], day [4 ];
275269 strftime (year , sizeof (year ), "%Y" , tm_info );
276270 strftime (month , sizeof (month ), "%m" , tm_info );
277271 strftime (day , sizeof (day ), "%d" , tm_info );
278-
272+
279273 char dir_path [MAX_PATH_LENGTH ];
280274 snprintf (dir_path , sizeof (dir_path ), "%s/%s/%s/%s/%s" ,
281- config -> storage_path , stream_name , year , month , day );
282-
275+ config -> storage_path , sanitized_name , year , month , day );
276+
283277 // Create directories if they don't exist
284278 char temp_path [MAX_PATH_LENGTH ];
285- snprintf (temp_path , sizeof (temp_path ), "%s/%s" , config -> storage_path , stream_name );
279+ snprintf (temp_path , sizeof (temp_path ), "%s/%s" , config -> storage_path , sanitized_name );
286280 mkdir (temp_path , 0755 );
287-
288- snprintf (temp_path , sizeof (temp_path ), "%s/%s/%s" , config -> storage_path , stream_name , year );
281+
282+ snprintf (temp_path , sizeof (temp_path ), "%s/%s/%s" , config -> storage_path , sanitized_name , year );
289283 mkdir (temp_path , 0755 );
290-
291- snprintf (temp_path , sizeof (temp_path ), "%s/%s/%s/%s" , config -> storage_path , stream_name , year , month );
284+
285+ snprintf (temp_path , sizeof (temp_path ), "%s/%s/%s/%s" , config -> storage_path , sanitized_name , year , month );
292286 mkdir (temp_path , 0755 );
293-
287+
294288 mkdir (dir_path , 0755 );
295-
289+
296290 // Generate full file path
297- snprintf (path , path_size , "%s/%s_%s_motion.mp4" , dir_path , stream_name , timestamp );
291+ snprintf (path , path_size , "%s/%s_%s_motion.mp4" , dir_path , sanitized_name , timestamp );
298292
299293 log_info ("Generated recording path: %s" , path );
300294 return 0 ;
@@ -497,14 +491,6 @@ static void* event_processor_thread_func(void *arg) {
497491 // as "shutting down" and continued/broke without ever ticking the
498492 // state machine for already-active recordings.
499493 //
500- // OLD CODE (kept for reference):
501- // if (pop_event(&event) != 0) {
502- // // Queue is empty or we're shutting down
503- // if (!event_processor_running) {
504- // break;
505- // }
506- // continue; // <-- bug: skips update_recording_state entirely
507- // }
508494 int pop_ret = pop_event (& event );
509495 if (pop_ret == -1 ) {
510496 // Shutting down
@@ -518,8 +504,9 @@ static void* event_processor_thread_func(void *arg) {
518504 time_t now = time (NULL );
519505 pthread_mutex_lock (& contexts_mutex );
520506 for (int i = 0 ; i < g_config .max_streams ; i ++ ) {
521- if (recording_contexts [i ].active && recording_contexts [ i ]. enabled ) {
507+ if (recording_contexts [i ].active ) {
522508 pthread_mutex_unlock (& contexts_mutex );
509+ // update_recording_state() checks ctx->enabled under ctx->mutex
523510 update_recording_state (& recording_contexts [i ], now );
524511 pthread_mutex_lock (& contexts_mutex );
525512 }
0 commit comments