Skip to content
Open
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
25 changes: 25 additions & 0 deletions db/migrations/0048_add_camera_uuid.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
-- Stable camera identity for fleet organization and policy bindings.
-- 0047 is reserved by recording-history query optimization work.

-- migrate:up

ALTER TABLE streams ADD COLUMN camera_uuid TEXT NOT NULL DEFAULT '';

UPDATE streams
SET camera_uuid = lower(
hex(randomblob(4)) || '-' ||
hex(randomblob(2)) || '-4' ||
substr(hex(randomblob(2)), 2) || '-' ||
substr('89ab', (abs(random()) % 4) + 1, 1) ||
substr(hex(randomblob(2)), 2) || '-' ||
hex(randomblob(6))
)
WHERE camera_uuid = '';

CREATE UNIQUE INDEX IF NOT EXISTS idx_streams_camera_uuid
ON streams(camera_uuid);

-- migrate:down

DROP INDEX IF EXISTS idx_streams_camera_uuid;
SELECT 1;
3 changes: 3 additions & 0 deletions include/core/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
#define MAX_PATH_LENGTH 512
// Maximum length for stream names
#define MAX_STREAM_NAME 256
// Canonical UUID string plus null terminator
#define CAMERA_UUID_STRING_SIZE 37
// Maximum length for URLs
#define MAX_URL_LENGTH 512
// Compile-time ceiling for per-stream static arrays (pointer arrays, watchdog trackers, etc.).
Expand All @@ -23,6 +25,7 @@ typedef enum {

// Stream configuration structure
typedef struct {
char camera_uuid[CAMERA_UUID_STRING_SIZE]; // Immutable fleet identity
char name[MAX_STREAM_NAME];
char url[MAX_URL_LENGTH];
bool enabled;
Expand Down
30 changes: 29 additions & 1 deletion include/database/db_embedded_migrations.h
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,27 @@ static const char migration_0046_up[] =
static const char migration_0046_down[] =
"SELECT 1;";

static const char migration_0048_up[] =
"ALTER TABLE streams ADD COLUMN camera_uuid TEXT NOT NULL DEFAULT '';\n"
"\n"
"UPDATE streams\n"
"SET camera_uuid = lower(\n"
" hex(randomblob(4)) || '-' ||\n"
" hex(randomblob(2)) || '-4' ||\n"
" substr(hex(randomblob(2)), 2) || '-' ||\n"
" substr('89ab', (abs(random()) % 4) + 1, 1) ||\n"
" substr(hex(randomblob(2)), 2) || '-' ||\n"
" hex(randomblob(6))\n"
")\n"
"WHERE camera_uuid = '';\n"
"\n"
"CREATE UNIQUE INDEX IF NOT EXISTS idx_streams_camera_uuid\n"
"ON streams(camera_uuid);";

static const char migration_0048_down[] =
"DROP INDEX IF EXISTS idx_streams_camera_uuid;\n"
"SELECT 1;";

static const migration_t embedded_migrations_data[] = {
{
.version = "0001",
Expand Down Expand Up @@ -1001,8 +1022,15 @@ static const migration_t embedded_migrations_data[] = {
.sql_down = migration_0046_down,
.is_embedded = true
},
{
.version = "0048",
.description = "add_camera_uuid",
.sql_up = migration_0048_up,
.sql_down = migration_0048_down,
.is_embedded = true
},
};

#define EMBEDDED_MIGRATIONS_COUNT 46
#define EMBEDDED_MIGRATIONS_COUNT 47

#endif /* DB_EMBEDDED_MIGRATIONS_H */
9 changes: 9 additions & 0 deletions include/database/db_streams.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@ int delete_stream_config_internal(const char *name, bool permanent);
*/
int get_stream_config_by_name(const char *name, stream_config_t *stream);

/**
* Get a stream configuration by its immutable camera UUID.
*
* @param camera_uuid Camera UUID to look up
* @param stream Stream configuration to fill
* @return 0 on success, non-zero on failure
*/
int get_stream_config_by_uuid(const char *camera_uuid, stream_config_t *stream);

/**
* Get all stream configurations from the database
*
Expand Down
28 changes: 27 additions & 1 deletion src/core/config.c
Original file line number Diff line number Diff line change
Expand Up @@ -1206,6 +1206,22 @@ int save_stream_configs(const config_t *config) {
}

if (identical) {
/* The database owns stable camera identity. Hydrate UUIDs even when
* no stream configuration write is necessary. */
for (int i = 0; i < config->max_streams; i++) {
if (config->streams[i].name[0] == '\0') {
continue;
}
for (int j = 0; j < loaded; j++) {
if (strcmp(config->streams[i].name,
db_streams[j].name) == 0) {
safe_strcpy(config->streams[i].camera_uuid,
db_streams[j].camera_uuid,
sizeof(config->streams[i].camera_uuid), 0);
break;
}
}
}
log_info("Stream configurations unchanged, skipping update");
free(db_streams);
commit_transaction();
Expand All @@ -1229,9 +1245,19 @@ int save_stream_configs(const config_t *config) {
// Add stream configurations to database
for (int i = 0; i < config->max_streams; i++) {
if (strlen(config->streams[i].name) > 0) {
char stream_name[MAX_STREAM_NAME];
safe_strcpy(stream_name, config->streams[i].name,
sizeof(stream_name), 0);
uint64_t result = add_stream_config(&config->streams[i]);
if (result == 0) {
log_error("Failed to add stream configuration: %s", config->streams[i].name);
log_error("Failed to add stream configuration: %s", stream_name);
rollback_transaction();
return -1;
}
if (get_stream_config_by_name(stream_name,
&config->streams[i]) != 0) {
log_error("Failed to reload stable identity for stream: %s",
stream_name);
rollback_transaction();
return -1;
}
Expand Down
78 changes: 72 additions & 6 deletions src/database/db_streams.c
Original file line number Diff line number Diff line change
Expand Up @@ -279,8 +279,12 @@ uint64_t add_stream_config(const stream_config_t *stream) {
"onvif_username, onvif_password, onvif_profile, onvif_port, "
"record_on_schedule, recording_schedule, tags, admin_url, privacy_mode, motion_trigger_source, "
"go2rtc_source_override, sub_stream_url, audio_voice_enhancement, detection_url, publish_url, "
"detection_record_on_schedule, detection_recording_schedule) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);";
"detection_record_on_schedule, detection_recording_schedule, camera_uuid) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, "
"lower(hex(randomblob(4)) || '-' || hex(randomblob(2)) || '-4' || "
"substr(hex(randomblob(2)), 2) || '-' || "
"substr('89ab', (abs(random()) % 4) + 1, 1) || "
"substr(hex(randomblob(2)), 2) || '-' || hex(randomblob(6))));";

rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
Expand Down Expand Up @@ -814,7 +818,7 @@ int get_stream_config_by_name(const char *name, stream_config_t *stream) {
"onvif_username, onvif_password, onvif_profile, onvif_port, "
"record_on_schedule, recording_schedule, tags, admin_url, privacy_mode, motion_trigger_source, "
"go2rtc_source_override, sub_stream_url, audio_voice_enhancement, detection_url, publish_url, "
"detection_record_on_schedule, detection_recording_schedule "
"detection_record_on_schedule, detection_recording_schedule, camera_uuid "
"FROM streams WHERE name = ?;";

// Column index constants for readability
Expand All @@ -832,7 +836,8 @@ int get_stream_config_by_name(const char *name, stream_config_t *stream) {
COL_RECORD_ON_SCHEDULE, COL_RECORDING_SCHEDULE, COL_TAGS, COL_ADMIN_URL, COL_PRIVACY_MODE,
COL_MOTION_TRIGGER_SOURCE, COL_GO2RTC_SOURCE_OVERRIDE, COL_SUB_STREAM_URL,
COL_AUDIO_VOICE_ENHANCEMENT, COL_DETECTION_URL, COL_PUBLISH_URL,
COL_DETECTION_RECORD_ON_SCHEDULE, COL_DETECTION_RECORDING_SCHEDULE
COL_DETECTION_RECORD_ON_SCHEDULE, COL_DETECTION_RECORDING_SCHEDULE,
COL_CAMERA_UUID
};

rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
Expand All @@ -847,6 +852,13 @@ int get_stream_config_by_name(const char *name, stream_config_t *stream) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
memset(stream, 0, sizeof(stream_config_t));

const char *camera_uuid =
(const char *)sqlite3_column_text(stmt, COL_CAMERA_UUID);
if (camera_uuid) {
safe_strcpy(stream->camera_uuid, camera_uuid,
sizeof(stream->camera_uuid), 0);
}

// Basic stream settings
const char *stream_name = (const char *)sqlite3_column_text(stmt, COL_NAME);
if (stream_name) {
Expand Down Expand Up @@ -1048,6 +1060,52 @@ int get_stream_config_by_name(const char *name, stream_config_t *stream) {
return result;
}

/**
* Get a stream configuration by immutable camera UUID.
*/
int get_stream_config_by_uuid(const char *camera_uuid, stream_config_t *stream) {
sqlite3 *db = get_db_handle();
pthread_mutex_t *db_mutex = get_db_mutex();
sqlite3_stmt *stmt = NULL;
char stream_name[MAX_STREAM_NAME] = {0};

if (!db) {
log_error("Database not initialized");
return -1;
}
if (!camera_uuid || strlen(camera_uuid) != CAMERA_UUID_STRING_SIZE - 1 ||
!stream) {
log_error("Valid camera UUID and stream configuration pointer are required");
return -1;
}

pthread_mutex_lock(db_mutex);

int rc = sqlite3_prepare_v2(
db, "SELECT name FROM streams WHERE camera_uuid = ?;", -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, camera_uuid, -1, SQLITE_STATIC);
if (sqlite3_step(stmt) == SQLITE_ROW) {
const char *name = (const char *)sqlite3_column_text(stmt, 0);
if (name) {
safe_strcpy(stream_name, name, sizeof(stream_name), 0);
}
}
} else {
log_error("Failed to prepare camera UUID lookup: %s", sqlite3_errmsg(db));
}

if (stmt) {
sqlite3_finalize(stmt);
}
pthread_mutex_unlock(db_mutex);

if (stream_name[0] == '\0') {
return -1;
}
return get_stream_config_by_name(stream_name, stream);
}

/**
* Get all stream configurations from the database
*
Expand Down Expand Up @@ -1088,7 +1146,7 @@ int get_all_stream_configs(stream_config_t *streams, int max_count) {
"onvif_username, onvif_password, onvif_profile, onvif_port, "
"record_on_schedule, recording_schedule, tags, admin_url, privacy_mode, motion_trigger_source, "
"go2rtc_source_override, sub_stream_url, audio_voice_enhancement, detection_url, publish_url, "
"detection_record_on_schedule, detection_recording_schedule "
"detection_record_on_schedule, detection_recording_schedule, camera_uuid "
"FROM streams ORDER BY name;";

// Column index constants (same as get_stream_config_by_name)
Expand All @@ -1106,7 +1164,8 @@ int get_all_stream_configs(stream_config_t *streams, int max_count) {
COL_RECORD_ON_SCHEDULE, COL_RECORDING_SCHEDULE, COL_TAGS, COL_ADMIN_URL, COL_PRIVACY_MODE,
COL_MOTION_TRIGGER_SOURCE, COL_GO2RTC_SOURCE_OVERRIDE, COL_SUB_STREAM_URL,
COL_AUDIO_VOICE_ENHANCEMENT, COL_DETECTION_URL, COL_PUBLISH_URL,
COL_DETECTION_RECORD_ON_SCHEDULE, COL_DETECTION_RECORDING_SCHEDULE
COL_DETECTION_RECORD_ON_SCHEDULE, COL_DETECTION_RECORDING_SCHEDULE,
COL_CAMERA_UUID
};

rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
Expand All @@ -1120,6 +1179,13 @@ int get_all_stream_configs(stream_config_t *streams, int max_count) {
stream_config_t *s = &streams[count];
memset(s, 0, sizeof(stream_config_t));

const char *camera_uuid =
(const char *)sqlite3_column_text(stmt, COL_CAMERA_UUID);
if (camera_uuid) {
safe_strcpy(s->camera_uuid, camera_uuid,
sizeof(s->camera_uuid), 0);
}

// Basic settings
const char *name = (const char *)sqlite3_column_text(stmt, COL_NAME);
if (name) {
Expand Down
6 changes: 6 additions & 0 deletions src/video/onvif_device_management.c
Original file line number Diff line number Diff line change
Expand Up @@ -794,6 +794,12 @@ int add_onvif_device_as_stream(const onvif_device_info_t *device_info,

log_info("Added ONVIF device stream configuration to database with ID %llu: %s",
(unsigned long long)stream_id, stream_name);

if (get_stream_config_by_name(stream_name, &config) != 0) {
log_error("Failed to load generated camera UUID for ONVIF stream: %s",
stream_name);
return -1;
}

// Then add stream to memory
stream_handle_t handle = add_stream(&config);
Expand Down
3 changes: 3 additions & 0 deletions src/web/api_handlers_streams_get.c
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ void handle_get_streams(const http_request_t *req, http_response_t *res) {
expose_sensitive_config);

// Add stream properties
cJSON_AddStringToObject(stream_obj, "camera_uuid", db_streams[i].camera_uuid);
cJSON_AddStringToObject(stream_obj, "name", db_streams[i].name);
cJSON_AddStringToObject(stream_obj, "url", safe_url);
cJSON_AddBoolToObject(stream_obj, "enabled", db_streams[i].enabled);
Expand Down Expand Up @@ -427,6 +428,7 @@ void handle_get_stream(const http_request_t *req, http_response_t *res) {
expose_sensitive_config);

// Add stream properties
cJSON_AddStringToObject(stream_obj, "camera_uuid", config.camera_uuid);
cJSON_AddStringToObject(stream_obj, "name", config.name);
cJSON_AddStringToObject(stream_obj, "url", safe_url);
cJSON_AddBoolToObject(stream_obj, "enabled", config.enabled);
Expand Down Expand Up @@ -628,6 +630,7 @@ void handle_get_stream_full(const http_request_t *req, http_response_t *res) {
api_onvif_password_full, sizeof(api_onvif_password_full),
expose_sensitive_config);

cJSON_AddStringToObject(stream_obj, "camera_uuid", config.camera_uuid);
cJSON_AddStringToObject(stream_obj, "name", config.name);
cJSON_AddStringToObject(stream_obj, "url", safe_url_full);
cJSON_AddBoolToObject(stream_obj, "enabled", config.enabled);
Expand Down
18 changes: 18 additions & 0 deletions src/web/api_handlers_streams_modify.c
Original file line number Diff line number Diff line change
Expand Up @@ -1042,6 +1042,23 @@ void handle_post_stream(const http_request_t *req, http_response_t *res) {
return;
}

/* camera_uuid is generated by SQLite so callers cannot choose or reuse an
* identity accidentally. Load it back before placing the configuration in
* the runtime stream manager. */
stream_config_t *persisted_config = calloc(1, sizeof(*persisted_config));
if (!persisted_config ||
get_stream_config_by_name(config.name, persisted_config) != 0) {
log_error("Failed to load generated camera UUID for stream: %s", config.name);
free(persisted_config);
delete_stream_config_internal(config.name, true);
http_response_set_json_error(res, 500,
"Failed to load created stream identity");
return;
}
safe_strcpy(config.camera_uuid, persisted_config->camera_uuid,
sizeof(config.camera_uuid), 0);
free(persisted_config);

// Create stream in memory from the database configuration
// This also registers the stream with go2rtc via go2rtc_integration_register_stream()
stream_handle_t stream = add_stream(&config);
Expand Down Expand Up @@ -1105,6 +1122,7 @@ void handle_post_stream(const http_request_t *req, http_response_t *res) {
}

cJSON_AddBoolToObject(success, "success", true);
cJSON_AddStringToObject(success, "camera_uuid", config.camera_uuid);

// Add ONVIF detection result if applicable
if (onvif_test_performed) {
Expand Down
2 changes: 2 additions & 0 deletions src/web/api_handlers_system.c
Original file line number Diff line number Diff line change
Expand Up @@ -1448,6 +1448,8 @@ void handle_post_system_backup(const http_request_t *req, http_response_t *res)
continue;
}

cJSON_AddStringToObject(stream, "camera_uuid",
g_config.streams[i].camera_uuid);
cJSON_AddStringToObject(stream, "name", g_config.streams[i].name);
cJSON_AddStringToObject(stream, "url", g_config.streams[i].url);
cJSON_AddBoolToObject(stream, "enabled", g_config.streams[i].enabled);
Expand Down
Loading
Loading