Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions include/database/db_recordings.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,19 @@ uint64_t add_recording_metadata(const recording_metadata_t *metadata);
int update_recording_metadata(uint64_t id, time_t end_time,
uint64_t size_bytes, bool is_complete);

/**
* Correct the start_time of an existing recording.
*
* Called after the pre-buffer has been flushed into a detection recording so
* that the database reflects the actual first-packet wall-clock time rather
* than the time at which mp4_writer_create() was called.
*
* @param id Recording ID returned by add_recording_metadata()
* @param start_time Corrected start time (epoch seconds)
* @return 0 on success, non-zero on failure
*/
int update_recording_start_time(uint64_t id, time_t start_time);

/**
* Get recording metadata from the database
*
Expand Down
14 changes: 14 additions & 0 deletions include/video/mp4_writer.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ struct mp4_writer {
// Recording trigger type
char trigger_type[16]; // 'scheduled', 'detection', 'motion', 'manual'

// Set to true once update_recording_start_time() has been called after the
// pre-event buffer was flushed. Prevents a second correction on rotation.
bool start_time_corrected;

// RTSP thread context
mp4_writer_thread_t *thread_ctx; // Changed from void* to proper type

Expand Down Expand Up @@ -99,6 +103,16 @@ int mp4_writer_add_audio_stream(mp4_writer_t *writer, const AVCodecParameters *c
*/
void mp4_writer_close(mp4_writer_t *writer);

/**
* Return the actual encoded duration of an MP4 file in seconds by reading
* its container metadata. Used after a file has been finalized to get a more
* accurate duration than (end_time - start_time) wall-clock subtraction.
*
* @param path Full path to the closed MP4 file
* @return Duration in seconds (>= 0), or -1 on error
*/
double get_mp4_file_duration_seconds(const char *path);

/**
* Enable or disable audio recording
*
Expand Down
24 changes: 24 additions & 0 deletions include/video/unified_detection_thread.h
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,16 @@ typedef struct {
// Audio recording configuration
bool record_audio; // Whether to include audio in recordings

// External motion trigger: set to 1 by unified_detection_notify_motion() when
// an ONVIF-managed master stream propagates its motion event to this UDT-managed
// slave stream (e.g. the PTZ lens on a TP-Link C545D that has no ONVIF profile).
// The UDT processing path checks this flag when handling eligible video
// keyframes and treats a rising edge as equivalent to a local detection
// event (starts/extends recording).
// Use atomic_store/atomic_load to avoid races between the event-processor thread
// (writer) and the UDT thread (reader/resetter).
atomic_int external_motion_trigger; // 0 = idle, 1 = motion active, 2 = motion ended

// Annotation-only mode: when true, detection runs but does NOT create separate MP4 files
// Detections are stored in the database and linked to the continuous recording
bool annotation_only;
Expand Down Expand Up @@ -235,5 +245,19 @@ int get_unified_detection_stats(const char *stream_name,
uint64_t *detections,
uint64_t *recordings);

/**
* Notify a UDT-managed stream of an externally-detected motion event.
*
* Called by the ONVIF motion recording system when a master stream's ONVIF
* event must be propagated to a slave stream that is managed by a UDT
* (e.g. the PTZ lens on a dual-lens camera). The UDT thread polls
* ctx->external_motion_trigger and reacts on the next packet boundary.
*
* @param stream_name Name of the slave stream (must be running as a UDT)
* @param motion_active true = motion started / ongoing
* false = motion ended
*/
void unified_detection_notify_motion(const char *stream_name, bool motion_active);

#endif /* UNIFIED_DETECTION_THREAD_H */

51 changes: 51 additions & 0 deletions src/database/db_recordings.c
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,57 @@ int update_recording_metadata(uint64_t id, time_t end_time,
return 0;
}

/**
* Correct the start_time of an existing recording in the database.
*
* Used after flushing the pre-event circular buffer into a detection recording
* so that the stored start_time matches the actual first packet timestamp
* rather than the time mp4_writer_create() was called.
*/
int update_recording_start_time(uint64_t id, time_t start_time) {
int rc;
sqlite3_stmt *stmt;

sqlite3 *db = get_db_handle();
pthread_mutex_t *db_mutex = get_db_mutex();

if (!db) {
log_error("Database not initialized");
return -1;
}

pthread_mutex_lock(db_mutex);

const char *sql = "UPDATE recordings SET start_time = ? WHERE id = ?;";

rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
log_error("Failed to prepare update_recording_start_time statement: %s",
sqlite3_errmsg(db));
pthread_mutex_unlock(db_mutex);
return -1;
}

sqlite3_bind_int64(stmt, 1, (sqlite3_int64)start_time);
sqlite3_bind_int64(stmt, 2, (sqlite3_int64)id);

rc = sqlite3_step(stmt);
if (rc != SQLITE_DONE) {
log_error("Failed to update recording start_time (id=%llu): %s",
(unsigned long long)id, sqlite3_errmsg(db));
sqlite3_finalize(stmt);
pthread_mutex_unlock(db_mutex);
return -1;
}

sqlite3_finalize(stmt);
pthread_mutex_unlock(db_mutex);

log_debug("Corrected start_time for recording ID %llu to %ld",
(unsigned long long)id, (long)start_time);
return 0;
}

// Get recording metadata by ID
int get_recording_metadata_by_id(uint64_t id, recording_metadata_t *metadata) {
int rc;
Expand Down
5 changes: 4 additions & 1 deletion src/video/mp4_segment_recorder.c
Original file line number Diff line number Diff line change
Expand Up @@ -967,7 +967,10 @@ int record_segment(const char *rtsp_url, const char *output_file, int duration,

log_info("Found first key frame, starting recording");

// Notify caller that segment has officially started (aligned to keyframe)
// Notify caller that segment has officially started (aligned to keyframe).
// The callback (on_segment_started_cb in mp4_writer_thread.c) creates the
// database recording entry at this point so that start_time is anchored
// to a decodable keyframe rather than the wall-clock time of avformat_open_input.
if (!started_cb_called && started_cb) {
started_cb(cb_ctx);
started_cb_called = true;
Expand Down
Loading
Loading