Skip to content

Commit 27844db

Browse files
matteiusclaude
andauthored
Add go2rtc config override and update submodule to dev (#384)
* Update go2rtc submodule to track upstream dev branch Rebase opensensor fork onto AlexxIT/go2rtc dev branch to pull in WebCodecs player, WebP streaming, HKSV support, system monitoring API, WebRTC error handling, and UI improvements. The two go.mod patches (security alert, dependency cleanup) were already addressed in dev; only the Stream.Stop() fix needed cherry-picking. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add go2rtc config override for per-stream sources and global settings Allow power users to customize go2rtc behavior without lightNVR needing to expose every option. Two levels of override: - Per-stream source override: a text field on each stream that replaces the auto-constructed go2rtc source URL. Supports single URLs or multi-source YAML lists for failover, transcoding, and hardware acceleration. Written directly into go2rtc.yaml streams section; API-based auto-registration is skipped for overridden streams. - Global config override: a YAML text field in settings (stored in system_settings table) appended to go2rtc.yaml after auto-generated sections. Handles custom ffmpeg presets, publish destinations, preload settings, log levels, etc. Uses last-key-wins for duplicates. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add migration 0040 to embedded migrations header The unit tests use embedded migrations (not filesystem SQL files) to initialize test databases. Without the embedded entry for 0040, the go2rtc_source_override column was missing and caused test_db_streams, test_db_zones, test_db_motion_config, and test_zone_filter to fail. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * git ignore * Add unit tests for go2rtc_source_override DB round-trip Cover the new column in db_streams.c: default empty, INSERT round-trip with multi-line YAML, UPDATE, get_all bulk read, and clearing the override back to empty. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Preserve user indentation in go2rtc source override YAML Stop stripping leading whitespace from override lines so that nested YAML (e.g. list items with indented subkeys) is preserved correctly. Base indentation (4 spaces) is still prepended to every line. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add sub-stream support and address Copilot review feedback Sub-stream support (closes discussion #366): - Add sub_stream_url field to stream config, coupled into migration 0040 - Register sub-streams with go2rtc as "{name}_sub" when URL is provided - Frontend grid view uses sub-stream (low-res) when available; fullscreen and recording always use the main stream - Sub-stream URL field added to stream config modal (Basic Information) - MSE, HLS, and WebRTC video cells accept useSubStream prop Copilot review fixes: - Single-URL overrides now emit valid inline YAML scalar form ("cam": rtsp://...) instead of block form - Stream names are YAML-escaped in double-quoted keys (handles " and \) - DB calls in config generation guarded with get_db_handle() != NULL to avoid noisy errors when DB isn't initialized yet - go2rtc_config_override restart dispatch moved outside if(settings_changed) so DB-backed overrides actually trigger a go2rtc restart Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Address Copilot review: sub-stream with override, length validation, log level - Sub-stream registration no longer bypassed when main stream has go2rtc_source_override — all three registration paths (register_all, sync_from_database, register_stream) now skip only the main stream API call while still registering {name}_sub via API - Validate go2rtc_config_override length on save (reject >= 4096 bytes) - Downgrade full config file dump from INFO to DEBUG to avoid leaking credentials from overrides into production logs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Wire go2rtc override and sub-stream changes through PUT worker The PUT worker previously only reloaded go2rtc registration when URL/protocol/record_audio/credentials changed. Now: - go2rtc_source_override changes: remove the old API-registered stream, then re-register via go2rtc_integration_register_stream() which handles the override-vs-API decision - sub_stream_url changes: remove old {name}_sub from go2rtc, then re-register with the new URL (or leave removed if cleared) Both paths are tracked with dedicated flags in put_stream_task_t so the worker can act on them independently of URL changes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9bb6089 commit 27844db

23 files changed

Lines changed: 714 additions & 115 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,4 @@ lightnvr-buildroot/
111111
*.ico
112112
*.png
113113
lightnvr-provisioning-prd.docx.md
114+
.mcp.json

.gitmodules

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
[submodule "go2rtc"]
22
path = go2rtc
33
url = https://github.com/opensensor/go2rtc.git
4+
branch = dev
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
-- Add go2rtc_source_override and sub_stream_url columns to streams table
2+
--
3+
-- go2rtc_source_override: when non-empty, written directly into go2rtc.yaml
4+
-- streams section instead of auto-constructing the source URL.
5+
--
6+
-- sub_stream_url: optional low-resolution stream URL used for the dashboard
7+
-- grid view while the main URL is used for recording and fullscreen viewing.
8+
9+
-- migrate:up
10+
ALTER TABLE streams ADD COLUMN go2rtc_source_override TEXT DEFAULT '';
11+
ALTER TABLE streams ADD COLUMN sub_stream_url TEXT DEFAULT '';
12+
13+
-- migrate:down
14+
-- SQLite does not support DROP COLUMN in older versions; migration is left intentionally empty.

go2rtc

Submodule go2rtc updated 168 files

include/core/config.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,16 @@ typedef struct {
9898
// Useful for dual-lens cameras where one lens provides ONVIF events and
9999
// the other (e.g. PTZ) does not expose its own motion events.
100100
char motion_trigger_source[MAX_STREAM_NAME];
101+
102+
// go2rtc source override: when non-empty, written directly into go2rtc.yaml
103+
// streams section instead of auto-constructing the source URL.
104+
// Supports single URLs or multi-source YAML lists (e.g. "- rtsp://cam/main\n- ffmpeg:cam#video=h264")
105+
char go2rtc_source_override[2048];
106+
107+
// Sub-stream URL: optional low-resolution stream for dashboard grid view.
108+
// When non-empty, registered with go2rtc as "{name}_sub" and used by the
109+
// frontend in grid view while the main URL is used for fullscreen/recording.
110+
char sub_stream_url[MAX_URL_LENGTH];
101111
} stream_config_t;
102112

103113
// Size of recording schedule text buffer: 168 values + 167 commas + null terminator

include/database/db_embedded_migrations.h

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,13 @@ static const char migration_0039_up[] =
623623
static const char migration_0039_down[] =
624624
"SELECT 1;";
625625

626+
static const char migration_0040_up[] =
627+
"ALTER TABLE streams ADD COLUMN go2rtc_source_override TEXT DEFAULT '';\n"
628+
"ALTER TABLE streams ADD COLUMN sub_stream_url TEXT DEFAULT '';";
629+
630+
static const char migration_0040_down[] =
631+
"SELECT 1;";
632+
626633
static const migration_t embedded_migrations_data[] = {
627634
{
628635
.version = "0001",
@@ -897,8 +904,15 @@ static const migration_t embedded_migrations_data[] = {
897904
.sql_down = migration_0039_down,
898905
.is_embedded = true
899906
},
907+
{
908+
.version = "0040",
909+
.description = "add_go2rtc_source_override_and_sub_stream_url",
910+
.sql_up = migration_0040_up,
911+
.sql_down = migration_0040_down,
912+
.is_embedded = true
913+
},
900914
};
901915

902-
#define EMBEDDED_MIGRATIONS_COUNT 39
916+
#define EMBEDDED_MIGRATIONS_COUNT 40
903917

904918
#endif /* DB_EMBEDDED_MIGRATIONS_H */

src/database/db_streams.c

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,8 @@ uint64_t add_stream_config(const stream_config_t *stream) {
132132
"ptz_enabled = ?, ptz_max_x = ?, ptz_max_y = ?, ptz_max_z = ?, ptz_has_home = ?, "
133133
"onvif_username = ?, onvif_password = ?, onvif_profile = ?, onvif_port = ?, "
134134
"record_on_schedule = ?, recording_schedule = ?, tags = ?, admin_url = ?, "
135-
"privacy_mode = ?, motion_trigger_source = ? "
135+
"privacy_mode = ?, motion_trigger_source = ?, go2rtc_source_override = ?, "
136+
"sub_stream_url = ? "
136137
"WHERE id = ?;";
137138

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

218221
// Bind ID parameter
219-
sqlite3_bind_int64(stmt, 46, (sqlite3_int64)existing_id);
222+
sqlite3_bind_int64(stmt, 48, (sqlite3_int64)existing_id);
220223

221224
// Execute statement
222225
rc = sqlite3_step(stmt);
@@ -264,8 +267,9 @@ uint64_t add_stream_config(const stream_config_t *stream) {
264267
"tier_critical_multiplier, tier_important_multiplier, tier_ephemeral_multiplier, storage_priority, "
265268
"ptz_enabled, ptz_max_x, ptz_max_y, ptz_max_z, ptz_has_home, "
266269
"onvif_username, onvif_password, onvif_profile, onvif_port, "
267-
"record_on_schedule, recording_schedule, tags, admin_url, privacy_mode, motion_trigger_source) "
268-
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);";
270+
"record_on_schedule, recording_schedule, tags, admin_url, privacy_mode, motion_trigger_source, "
271+
"go2rtc_source_override, sub_stream_url) "
272+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);";
269273

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

345-
// Bind tags, admin URL, privacy_mode, and motion_trigger_source parameters
349+
// Bind tags, admin URL, privacy_mode, motion_trigger_source, go2rtc override, and sub-stream parameters
346350
sqlite3_bind_text(stmt, 43, stream->tags, -1, SQLITE_STATIC);
347351
sqlite3_bind_text(stmt, 44, stream->admin_url, -1, SQLITE_STATIC);
348352
sqlite3_bind_int(stmt, 45, stream->privacy_mode ? 1 : 0);
349353
sqlite3_bind_text(stmt, 46, stream->motion_trigger_source, -1, SQLITE_STATIC);
354+
sqlite3_bind_text(stmt, 47, stream->go2rtc_source_override, -1, SQLITE_STATIC);
355+
sqlite3_bind_text(stmt, 48, stream->sub_stream_url, -1, SQLITE_STATIC);
350356

351357
// Execute statement
352358
rc = sqlite3_step(stmt);
@@ -417,7 +423,8 @@ int update_stream_config(const char *name, const stream_config_t *stream) {
417423
"ptz_enabled = ?, ptz_max_x = ?, ptz_max_y = ?, ptz_max_z = ?, ptz_has_home = ?, "
418424
"onvif_username = ?, onvif_password = ?, onvif_profile = ?, onvif_port = ?, "
419425
"record_on_schedule = ?, recording_schedule = ?, tags = ?, admin_url = ?, privacy_mode = ?, "
420-
"motion_trigger_source = ? "
426+
"motion_trigger_source = ?, go2rtc_source_override = ?, "
427+
"sub_stream_url = ? "
421428
"WHERE name = ?;";
422429

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

498-
// Bind tags, admin URL, privacy_mode, and motion_trigger_source parameters
505+
// Bind tags, admin URL, privacy_mode, motion_trigger_source, go2rtc override, and sub-stream parameters
499506
sqlite3_bind_text(stmt, 43, stream->tags, -1, SQLITE_STATIC);
500507
sqlite3_bind_text(stmt, 44, stream->admin_url, -1, SQLITE_STATIC);
501508
sqlite3_bind_int(stmt, 45, stream->privacy_mode ? 1 : 0);
502509
sqlite3_bind_text(stmt, 46, stream->motion_trigger_source, -1, SQLITE_STATIC);
510+
sqlite3_bind_text(stmt, 47, stream->go2rtc_source_override, -1, SQLITE_STATIC);
511+
sqlite3_bind_text(stmt, 48, stream->sub_stream_url, -1, SQLITE_STATIC);
503512

504513
// Bind the WHERE clause parameter
505-
sqlite3_bind_text(stmt, 47, name, -1, SQLITE_STATIC);
514+
sqlite3_bind_text(stmt, 49, name, -1, SQLITE_STATIC);
506515

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

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

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

964+
// go2rtc source override
965+
const char *go2rtc_source_override = (const char *)sqlite3_column_text(stmt, COL_GO2RTC_SOURCE_OVERRIDE);
966+
if (go2rtc_source_override) {
967+
safe_strcpy(stream->go2rtc_source_override, go2rtc_source_override, sizeof(stream->go2rtc_source_override), 0);
968+
} else {
969+
stream->go2rtc_source_override[0] = '\0';
970+
}
971+
972+
// Sub-stream URL
973+
const char *sub_stream_url_val = (const char *)sqlite3_column_text(stmt, COL_SUB_STREAM_URL);
974+
if (sub_stream_url_val) {
975+
safe_strcpy(stream->sub_stream_url, sub_stream_url_val, sizeof(stream->sub_stream_url), 0);
976+
} else {
977+
stream->sub_stream_url[0] = '\0';
978+
}
979+
954980
result = 0;
955981
}
956982

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

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

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

1208+
// go2rtc source override
1209+
const char *go2rtc_src_override = (const char *)sqlite3_column_text(stmt, COL_GO2RTC_SOURCE_OVERRIDE);
1210+
if (go2rtc_src_override) {
1211+
safe_strcpy(s->go2rtc_source_override, go2rtc_src_override, sizeof(s->go2rtc_source_override), 0);
1212+
} else {
1213+
s->go2rtc_source_override[0] = '\0';
1214+
}
1215+
1216+
// Sub-stream URL
1217+
const char *sub_url = (const char *)sqlite3_column_text(stmt, COL_SUB_STREAM_URL);
1218+
if (sub_url) {
1219+
safe_strcpy(s->sub_stream_url, sub_url, sizeof(s->sub_stream_url), 0);
1220+
} else {
1221+
s->sub_stream_url[0] = '\0';
1222+
}
1223+
11811224
count++;
11821225
}
11831226

0 commit comments

Comments
 (0)