Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
11 changes: 11 additions & 0 deletions db/migrations/0040_add_go2rtc_source_override.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-- Add go2rtc_source_override column to streams table
-- When non-empty, this value is written directly into go2rtc.yaml streams
-- section instead of auto-constructing the source URL from the stream URL.
-- Supports single source URLs or multi-source YAML lists for advanced
-- go2rtc features like failover, transcoding, and hardware acceleration.

-- migrate:up
ALTER TABLE streams ADD COLUMN go2rtc_source_override 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
5 changes: 5 additions & 0 deletions include/core/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ 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];
} stream_config_t;

// Size of recording schedule text buffer: 168 values + 167 commas + null terminator
Expand Down
15 changes: 14 additions & 1 deletion include/database/db_embedded_migrations.h
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,12 @@ 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 '';";

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

static const migration_t embedded_migrations_data[] = {
{
.version = "0001",
Expand Down Expand Up @@ -897,8 +903,15 @@ static const migration_t embedded_migrations_data[] = {
.sql_down = migration_0039_down,
.is_embedded = true
},
{
.version = "0040",
.description = "add_go2rtc_source_override",
.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 */
45 changes: 33 additions & 12 deletions src/database/db_streams.c
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ 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 = ? "
"WHERE id = ?;";

rc = sqlite3_prepare_v2(db, update_sql, -1, &stmt, NULL);
Expand Down Expand Up @@ -214,9 +214,10 @@ 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);

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

// Execute statement
rc = sqlite3_step(stmt);
Expand Down Expand Up @@ -264,8 +265,8 @@ 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) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);";

rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
Expand Down Expand Up @@ -342,11 +343,12 @@ 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, and go2rtc override 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);

// Execute statement
rc = sqlite3_step(stmt);
Expand Down Expand Up @@ -417,7 +419,7 @@ 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 = ? "
"WHERE name = ?;";

rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
Expand Down Expand Up @@ -495,14 +497,15 @@ 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, and go2rtc override 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);

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

// Execute statement
rc = sqlite3_step(stmt);
Expand Down Expand Up @@ -774,7 +777,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 "
"FROM streams WHERE name = ?;";

// Column index constants for readability
Expand All @@ -790,7 +794,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
};

rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
Expand Down Expand Up @@ -951,6 +955,14 @@ 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';
}

result = 0;
}

Expand Down Expand Up @@ -1002,7 +1014,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 "
"FROM streams ORDER BY name;";

// Column index constants (same as get_stream_config_by_name)
Expand All @@ -1018,7 +1031,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
};

rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
Expand Down Expand Up @@ -1178,6 +1191,14 @@ 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';
}

count++;
}

Expand Down
22 changes: 22 additions & 0 deletions src/video/go2rtc/go2rtc_integration.c
Original file line number Diff line number Diff line change
Expand Up @@ -1355,6 +1355,13 @@ bool go2rtc_integration_register_all_streams(void) {
bool all_success = true;
for (int i = 0; i < count; i++) {
if (streams[i].enabled) {
// Skip streams with go2rtc source override — they are already
// defined in the go2rtc.yaml config file and don't need API registration.
if (streams[i].go2rtc_source_override[0] != '\0') {
log_info("Skipping API registration for stream %s (has go2rtc source override)", streams[i].name);
continue;
Comment thread
matteius marked this conversation as resolved.
Outdated
}
Comment thread
matteius marked this conversation as resolved.
Outdated

log_info("Registering stream %s with go2rtc", streams[i].name);

// Register the stream with go2rtc
Expand Down Expand Up @@ -1434,6 +1441,11 @@ bool go2rtc_sync_streams_from_database(void) {
skipped++;
continue;
}
if (db_streams[i].go2rtc_source_override[0] != '\0') {
log_debug("Skipping API sync for stream %s (has go2rtc source override)", db_streams[i].name);
skipped++;
continue;
}
Comment thread
matteius marked this conversation as resolved.
Outdated

// Check if stream already exists in go2rtc
if (go2rtc_api_stream_exists(db_streams[i].name)) {
Expand Down Expand Up @@ -1773,6 +1785,16 @@ bool go2rtc_integration_register_stream(const char *stream_name) {
return false;
}

// Check for go2rtc source override — these streams are defined in go2rtc.yaml
// and don't need API registration
{
stream_config_t check_config;
if (get_stream_config(stream, &check_config) == 0 && check_config.go2rtc_source_override[0] != '\0') {
log_info("Stream %s has go2rtc source override, skipping API registration", stream_name);
return true;
}
Comment thread
matteius marked this conversation as resolved.
Outdated
}

stream_config_t config;
if (get_stream_config(stream, &config) != 0) {
log_error("Failed to get config for stream %s", stream_name);
Expand Down
52 changes: 49 additions & 3 deletions src/video/go2rtc/go2rtc_process.c
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
#include "core/config.h"
#include "core/path_utils.h"
#include "utils/strings.h"
#include "database/db_streams.h"
#include "database/db_system_settings.h"


// Define PATH_MAX if not defined
Expand Down Expand Up @@ -697,9 +699,53 @@ bool go2rtc_process_generate_config(const char *config_path, int api_port) {
fprintf(config_file, " h264: \"-codec:v libx264 -g:v 30 -preset:v superfast\"\n");
fprintf(config_file, " h265: \"-codec:v libx265 -g:v 30 -preset:v superfast\"\n");

// Streams section (will be populated dynamically)
fprintf(config_file, "streams:\n");
fprintf(config_file, " # Streams will be added dynamically\n");
// Streams section — write overridden streams directly into config,
// other streams will be registered dynamically via the go2rtc API.
fprintf(config_file, "\nstreams:\n");
{
int ms = g_config.max_streams > 0 ? g_config.max_streams : 32;
Comment thread
matteius marked this conversation as resolved.
Outdated
stream_config_t *streams = calloc(ms, sizeof(stream_config_t));
int count = streams ? get_all_stream_configs(streams, ms) : 0;
bool has_overridden = false;

for (int i = 0; i < count; i++) {
if (!streams[i].enabled) continue;
if (streams[i].go2rtc_source_override[0] == '\0') continue;

has_overridden = true;
// Quote stream name for YAML safety (handles spaces, colons, etc.)
fprintf(config_file, " \"%s\":\n", streams[i].name);

Comment thread
matteius marked this conversation as resolved.
Outdated
Comment thread
matteius marked this conversation as resolved.
Outdated
Comment thread
matteius marked this conversation as resolved.
Outdated
// Write each line of the override with base indentation while
// preserving any user-provided indentation within the YAML.
const char *p = streams[i].go2rtc_source_override;
while (*p) {
const char *eol = strchr(p, '\n');
if (eol) {
fprintf(config_file, " %.*s\n", (int)(eol - p), p);
p = eol + 1;
} else {
fprintf(config_file, " %s\n", p);
break;
}
}
Comment thread
matteius marked this conversation as resolved.
Outdated
}

if (!has_overridden) {
fprintf(config_file, " # Streams will be added dynamically via API\n");
}
free(streams);
}

// Global go2rtc config override from system settings
{
char global_override[4096] = {0};
if (db_get_system_setting("go2rtc_config_override", global_override, sizeof(global_override)) == 0
Comment thread
matteius marked this conversation as resolved.
Outdated
&& global_override[0] != '\0') {
Comment thread
matteius marked this conversation as resolved.
fprintf(config_file, "\n# User config override\n");
fprintf(config_file, "%s\n", global_override);
}
Comment thread
matteius marked this conversation as resolved.
}

fclose(config_file);
log_info("Generated go2rtc configuration file: %s", config_path);
Expand Down
23 changes: 23 additions & 0 deletions src/web/api_handlers_settings.c
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include "database/db_core.h"
#include "database/db_streams.h"
#include "database/db_auth.h"
#include "database/db_system_settings.h"
#include "video/stream_manager.h"
#include "video/streams.h"
#include "video/mp4_recording.h"
Expand Down Expand Up @@ -361,6 +362,16 @@ void handle_get_settings(const http_request_t *req, http_response_t *res) {
cJSON_AddStringToObject(settings, "go2rtc_ice_servers", g_config.go2rtc_ice_servers);
cJSON_AddBoolToObject(settings, "go2rtc_force_native_hls", g_config.go2rtc_force_native_hls);

// go2rtc global config override (stored in system_settings table)
{
char go2rtc_config_override_buf[4096] = {0};
if (db_get_system_setting("go2rtc_config_override", go2rtc_config_override_buf, sizeof(go2rtc_config_override_buf)) == 0) {
cJSON_AddStringToObject(settings, "go2rtc_config_override", go2rtc_config_override_buf);
} else {
cJSON_AddStringToObject(settings, "go2rtc_config_override", "");
Comment thread
matteius marked this conversation as resolved.
}
Comment thread
matteius marked this conversation as resolved.
}

// MQTT settings
cJSON_AddBoolToObject(settings, "mqtt_enabled", g_config.mqtt_enabled);
cJSON_AddStringToObject(settings, "mqtt_broker_host", g_config.mqtt_broker_host);
Expand Down Expand Up @@ -949,6 +960,18 @@ void handle_post_settings(const http_request_t *req, http_response_t *res) {
log_info("Updated go2rtc_force_native_hls: %s", g_config.go2rtc_force_native_hls ? "true" : "false");
}

// go2rtc global config override (stored in system_settings)
cJSON *go2rtc_config_override = cJSON_GetObjectItem(settings, "go2rtc_config_override");
if (go2rtc_config_override && cJSON_IsString(go2rtc_config_override)) {
if (db_set_system_setting("go2rtc_config_override", go2rtc_config_override->valuestring) != 0) {
log_error("Failed to save go2rtc_config_override to system_settings");
} else {
log_info("Updated go2rtc_config_override");
go2rtc_config_changed = true;
go2rtc_becoming_enabled = g_config.go2rtc_enabled;
}
}
Comment thread
matteius marked this conversation as resolved.

// MQTT enabled
cJSON *mqtt_enabled = cJSON_GetObjectItem(settings, "mqtt_enabled");
if (mqtt_enabled && cJSON_IsBool(mqtt_enabled)) {
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 @@ -236,6 +236,7 @@ void handle_get_streams(const http_request_t *req, http_response_t *res) {
cJSON_AddStringToObject(stream_obj, "admin_url", db_streams[i].admin_url);
cJSON_AddBoolToObject(stream_obj, "privacy_mode", db_streams[i].privacy_mode);
cJSON_AddStringToObject(stream_obj, "motion_trigger_source", db_streams[i].motion_trigger_source);
cJSON_AddStringToObject(stream_obj, "go2rtc_source_override", db_streams[i].go2rtc_source_override);

// Get stream status
stream_handle_t stream = get_stream_by_name(db_streams[i].name);
Expand Down Expand Up @@ -392,6 +393,7 @@ void handle_get_stream(const http_request_t *req, http_response_t *res) {
cJSON_AddStringToObject(stream_obj, "admin_url", config.admin_url);
cJSON_AddBoolToObject(stream_obj, "privacy_mode", config.privacy_mode);
cJSON_AddStringToObject(stream_obj, "motion_trigger_source", config.motion_trigger_source);
cJSON_AddStringToObject(stream_obj, "go2rtc_source_override", config.go2rtc_source_override);

// Get stream status — resolve using UDT state so that go2rtc-managed
// streams (which stay INACTIVE in the state manager) report accurately.
Expand Down Expand Up @@ -542,6 +544,7 @@ void handle_get_stream_full(const http_request_t *req, http_response_t *res) {
cJSON_AddStringToObject(stream_obj, "admin_url", config.admin_url);
cJSON_AddBoolToObject(stream_obj, "privacy_mode", config.privacy_mode);
cJSON_AddStringToObject(stream_obj, "motion_trigger_source", config.motion_trigger_source);
cJSON_AddStringToObject(stream_obj, "go2rtc_source_override", config.go2rtc_source_override);

// Status — resolve using UDT state for accurate reporting when go2rtc
// manages the stream (state manager stays INACTIVE/STOPPED at startup).
Expand Down
Loading
Loading