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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,4 @@ lightnvr-buildroot/
*.ico
*.png
lightnvr-provisioning-prd.docx.md
.mcp.json
1 change: 1 addition & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
[submodule "go2rtc"]
path = go2rtc
url = https://github.com/opensensor/go2rtc.git
branch = dev
14 changes: 14 additions & 0 deletions db/migrations/0040_add_go2rtc_source_override.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-- Add go2rtc_source_override and sub_stream_url columns to streams table
--
-- go2rtc_source_override: when non-empty, written directly into go2rtc.yaml
-- streams section instead of auto-constructing the source URL.
--
-- sub_stream_url: optional low-resolution stream URL used for the dashboard
-- grid view while the main URL is used for recording and fullscreen viewing.

-- migrate:up
ALTER TABLE streams ADD COLUMN go2rtc_source_override TEXT DEFAULT '';
ALTER TABLE streams ADD COLUMN sub_stream_url TEXT DEFAULT '';

-- migrate:down
-- SQLite does not support DROP COLUMN in older versions; migration is left intentionally empty.
2 changes: 1 addition & 1 deletion go2rtc
Submodule go2rtc updated 168 files
10 changes: 10 additions & 0 deletions include/core/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,16 @@ typedef struct {
// Useful for dual-lens cameras where one lens provides ONVIF events and
// the other (e.g. PTZ) does not expose its own motion events.
char motion_trigger_source[MAX_STREAM_NAME];

// go2rtc source override: when non-empty, written directly into go2rtc.yaml
// streams section instead of auto-constructing the source URL.
// Supports single URLs or multi-source YAML lists (e.g. "- rtsp://cam/main\n- ffmpeg:cam#video=h264")
char go2rtc_source_override[2048];

// Sub-stream URL: optional low-resolution stream for dashboard grid view.
// When non-empty, registered with go2rtc as "{name}_sub" and used by the
// frontend in grid view while the main URL is used for fullscreen/recording.
char sub_stream_url[MAX_URL_LENGTH];
} stream_config_t;

// Size of recording schedule text buffer: 168 values + 167 commas + null terminator
Expand Down
16 changes: 15 additions & 1 deletion include/database/db_embedded_migrations.h
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,13 @@ static const char migration_0039_up[] =
static const char migration_0039_down[] =
"SELECT 1;";

static const char migration_0040_up[] =
"ALTER TABLE streams ADD COLUMN go2rtc_source_override TEXT DEFAULT '';\n"
"ALTER TABLE streams ADD COLUMN sub_stream_url TEXT DEFAULT '';";

static const char migration_0040_down[] =
"SELECT 1;";

static const migration_t embedded_migrations_data[] = {
{
.version = "0001",
Expand Down Expand Up @@ -897,8 +904,15 @@ static const migration_t embedded_migrations_data[] = {
.sql_down = migration_0039_down,
.is_embedded = true
},
{
.version = "0040",
.description = "add_go2rtc_source_override_and_sub_stream_url",
.sql_up = migration_0040_up,
.sql_down = migration_0040_down,
.is_embedded = true
},
};

#define EMBEDDED_MIGRATIONS_COUNT 39
#define EMBEDDED_MIGRATIONS_COUNT 40

#endif /* DB_EMBEDDED_MIGRATIONS_H */
67 changes: 55 additions & 12 deletions src/database/db_streams.c
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@ uint64_t add_stream_config(const stream_config_t *stream) {
"ptz_enabled = ?, ptz_max_x = ?, ptz_max_y = ?, ptz_max_z = ?, ptz_has_home = ?, "
"onvif_username = ?, onvif_password = ?, onvif_profile = ?, onvif_port = ?, "
"record_on_schedule = ?, recording_schedule = ?, tags = ?, admin_url = ?, "
"privacy_mode = ?, motion_trigger_source = ? "
"privacy_mode = ?, motion_trigger_source = ?, go2rtc_source_override = ?, "
"sub_stream_url = ? "
"WHERE id = ?;";

rc = sqlite3_prepare_v2(db, update_sql, -1, &stmt, NULL);
Expand Down Expand Up @@ -214,9 +215,11 @@ uint64_t add_stream_config(const stream_config_t *stream) {
sqlite3_bind_text(stmt, 43, stream->admin_url, -1, SQLITE_STATIC);
sqlite3_bind_int(stmt, 44, stream->privacy_mode ? 1 : 0);
sqlite3_bind_text(stmt, 45, stream->motion_trigger_source, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 46, stream->go2rtc_source_override, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 47, stream->sub_stream_url, -1, SQLITE_STATIC);

// Bind ID parameter
sqlite3_bind_int64(stmt, 46, (sqlite3_int64)existing_id);
sqlite3_bind_int64(stmt, 48, (sqlite3_int64)existing_id);

// Execute statement
rc = sqlite3_step(stmt);
Expand Down Expand Up @@ -264,8 +267,9 @@ uint64_t add_stream_config(const stream_config_t *stream) {
"tier_critical_multiplier, tier_important_multiplier, tier_ephemeral_multiplier, storage_priority, "
"ptz_enabled, ptz_max_x, ptz_max_y, ptz_max_z, ptz_has_home, "
"onvif_username, onvif_password, onvif_profile, onvif_port, "
"record_on_schedule, recording_schedule, tags, admin_url, privacy_mode, motion_trigger_source) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);";
"record_on_schedule, recording_schedule, tags, admin_url, privacy_mode, motion_trigger_source, "
"go2rtc_source_override, sub_stream_url) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);";

rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
Expand Down Expand Up @@ -342,11 +346,13 @@ uint64_t add_stream_config(const stream_config_t *stream) {
serialize_recording_schedule(stream->recording_schedule, insert_schedule_buf, sizeof(insert_schedule_buf));
sqlite3_bind_text(stmt, 42, insert_schedule_buf, -1, SQLITE_TRANSIENT);

// Bind tags, admin URL, privacy_mode, and motion_trigger_source parameters
// Bind tags, admin URL, privacy_mode, motion_trigger_source, go2rtc override, and sub-stream parameters
sqlite3_bind_text(stmt, 43, stream->tags, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 44, stream->admin_url, -1, SQLITE_STATIC);
sqlite3_bind_int(stmt, 45, stream->privacy_mode ? 1 : 0);
sqlite3_bind_text(stmt, 46, stream->motion_trigger_source, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 47, stream->go2rtc_source_override, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 48, stream->sub_stream_url, -1, SQLITE_STATIC);

// Execute statement
rc = sqlite3_step(stmt);
Expand Down Expand Up @@ -417,7 +423,8 @@ int update_stream_config(const char *name, const stream_config_t *stream) {
"ptz_enabled = ?, ptz_max_x = ?, ptz_max_y = ?, ptz_max_z = ?, ptz_has_home = ?, "
"onvif_username = ?, onvif_password = ?, onvif_profile = ?, onvif_port = ?, "
"record_on_schedule = ?, recording_schedule = ?, tags = ?, admin_url = ?, privacy_mode = ?, "
"motion_trigger_source = ? "
"motion_trigger_source = ?, go2rtc_source_override = ?, "
"sub_stream_url = ? "
"WHERE name = ?;";

rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
Expand Down Expand Up @@ -495,14 +502,16 @@ int update_stream_config(const char *name, const stream_config_t *stream) {
serialize_recording_schedule(stream->recording_schedule, update_schedule_buf, sizeof(update_schedule_buf));
sqlite3_bind_text(stmt, 42, update_schedule_buf, -1, SQLITE_TRANSIENT);

// Bind tags, admin URL, privacy_mode, and motion_trigger_source parameters
// Bind tags, admin URL, privacy_mode, motion_trigger_source, go2rtc override, and sub-stream parameters
sqlite3_bind_text(stmt, 43, stream->tags, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 44, stream->admin_url, -1, SQLITE_STATIC);
sqlite3_bind_int(stmt, 45, stream->privacy_mode ? 1 : 0);
sqlite3_bind_text(stmt, 46, stream->motion_trigger_source, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 47, stream->go2rtc_source_override, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 48, stream->sub_stream_url, -1, SQLITE_STATIC);

// Bind the WHERE clause parameter
sqlite3_bind_text(stmt, 47, name, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 49, name, -1, SQLITE_STATIC);

// Execute statement
rc = sqlite3_step(stmt);
Expand Down Expand Up @@ -774,7 +783,8 @@ int get_stream_config_by_name(const char *name, stream_config_t *stream) {
"tier_critical_multiplier, tier_important_multiplier, tier_ephemeral_multiplier, storage_priority, "
"ptz_enabled, ptz_max_x, ptz_max_y, ptz_max_z, ptz_has_home, "
"onvif_username, onvif_password, onvif_profile, onvif_port, "
"record_on_schedule, recording_schedule, tags, admin_url, privacy_mode, motion_trigger_source "
"record_on_schedule, recording_schedule, tags, admin_url, privacy_mode, motion_trigger_source, "
"go2rtc_source_override, sub_stream_url "
"FROM streams WHERE name = ?;";

// Column index constants for readability
Expand All @@ -790,7 +800,7 @@ int get_stream_config_by_name(const char *name, stream_config_t *stream) {
COL_PTZ_ENABLED, COL_PTZ_MAX_X, COL_PTZ_MAX_Y, COL_PTZ_MAX_Z, COL_PTZ_HAS_HOME,
COL_ONVIF_USERNAME, COL_ONVIF_PASSWORD, COL_ONVIF_PROFILE, COL_ONVIF_PORT,
COL_RECORD_ON_SCHEDULE, COL_RECORDING_SCHEDULE, COL_TAGS, COL_ADMIN_URL, COL_PRIVACY_MODE,
COL_MOTION_TRIGGER_SOURCE
COL_MOTION_TRIGGER_SOURCE, COL_GO2RTC_SOURCE_OVERRIDE, COL_SUB_STREAM_URL
};

rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
Expand Down Expand Up @@ -951,6 +961,22 @@ int get_stream_config_by_name(const char *name, stream_config_t *stream) {
stream->motion_trigger_source[0] = '\0';
}

// go2rtc source override
const char *go2rtc_source_override = (const char *)sqlite3_column_text(stmt, COL_GO2RTC_SOURCE_OVERRIDE);
if (go2rtc_source_override) {
safe_strcpy(stream->go2rtc_source_override, go2rtc_source_override, sizeof(stream->go2rtc_source_override), 0);
} else {
stream->go2rtc_source_override[0] = '\0';
}

// Sub-stream URL
const char *sub_stream_url_val = (const char *)sqlite3_column_text(stmt, COL_SUB_STREAM_URL);
if (sub_stream_url_val) {
safe_strcpy(stream->sub_stream_url, sub_stream_url_val, sizeof(stream->sub_stream_url), 0);
} else {
stream->sub_stream_url[0] = '\0';
}

result = 0;
}

Expand Down Expand Up @@ -1002,7 +1028,8 @@ int get_all_stream_configs(stream_config_t *streams, int max_count) {
"tier_critical_multiplier, tier_important_multiplier, tier_ephemeral_multiplier, storage_priority, "
"ptz_enabled, ptz_max_x, ptz_max_y, ptz_max_z, ptz_has_home, "
"onvif_username, onvif_password, onvif_profile, onvif_port, "
"record_on_schedule, recording_schedule, tags, admin_url, privacy_mode, motion_trigger_source "
"record_on_schedule, recording_schedule, tags, admin_url, privacy_mode, motion_trigger_source, "
"go2rtc_source_override, sub_stream_url "
"FROM streams ORDER BY name;";

// Column index constants (same as get_stream_config_by_name)
Expand All @@ -1018,7 +1045,7 @@ int get_all_stream_configs(stream_config_t *streams, int max_count) {
COL_PTZ_ENABLED, COL_PTZ_MAX_X, COL_PTZ_MAX_Y, COL_PTZ_MAX_Z, COL_PTZ_HAS_HOME,
COL_ONVIF_USERNAME, COL_ONVIF_PASSWORD, COL_ONVIF_PROFILE, COL_ONVIF_PORT,
COL_RECORD_ON_SCHEDULE, COL_RECORDING_SCHEDULE, COL_TAGS, COL_ADMIN_URL, COL_PRIVACY_MODE,
COL_MOTION_TRIGGER_SOURCE
COL_MOTION_TRIGGER_SOURCE, COL_GO2RTC_SOURCE_OVERRIDE, COL_SUB_STREAM_URL
};

rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
Expand Down Expand Up @@ -1178,6 +1205,22 @@ int get_all_stream_configs(stream_config_t *streams, int max_count) {
s->motion_trigger_source[0] = '\0';
}

// go2rtc source override
const char *go2rtc_src_override = (const char *)sqlite3_column_text(stmt, COL_GO2RTC_SOURCE_OVERRIDE);
if (go2rtc_src_override) {
safe_strcpy(s->go2rtc_source_override, go2rtc_src_override, sizeof(s->go2rtc_source_override), 0);
} else {
s->go2rtc_source_override[0] = '\0';
}

// Sub-stream URL
const char *sub_url = (const char *)sqlite3_column_text(stmt, COL_SUB_STREAM_URL);
if (sub_url) {
safe_strcpy(s->sub_stream_url, sub_url, sizeof(s->sub_stream_url), 0);
} else {
s->sub_stream_url[0] = '\0';
}

count++;
}

Expand Down
Loading
Loading