From 415193bb028241600fd97faf274462b4525f90b8 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Tue, 14 Apr 2026 01:58:17 -0400 Subject: [PATCH 1/9] 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) --- .gitmodules | 1 + go2rtc | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index 54f895b0..d338a47b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,4 @@ [submodule "go2rtc"] path = go2rtc url = https://github.com/opensensor/go2rtc.git + branch = dev diff --git a/go2rtc b/go2rtc index bd8b4796..63c58e77 160000 --- a/go2rtc +++ b/go2rtc @@ -1 +1 @@ -Subproject commit bd8b4796525db4ba6746d74504ed03fdc9755000 +Subproject commit 63c58e77a6661fcced56e0ad795ee875c74ebc39 From 40794447553b3e9cc2b427cc7feb79a742803f1c Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Tue, 14 Apr 2026 01:58:29 -0400 Subject: [PATCH 2/9] 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) --- .../0040_add_go2rtc_source_override.sql | 11 ++++ include/core/config.h | 5 ++ src/database/db_streams.c | 45 +++++++++++---- src/video/go2rtc/go2rtc_integration.c | 22 +++++++ src/video/go2rtc/go2rtc_process.c | 57 ++++++++++++++++++- src/web/api_handlers_settings.c | 23 ++++++++ src/web/api_handlers_streams_get.c | 3 + src/web/api_handlers_streams_modify.c | 29 ++++++++++ web/js/components/preact/SettingsView.jsx | 22 +++++++ .../components/preact/StreamConfigModal.jsx | 25 ++++++++ web/js/components/preact/StreamsView.jsx | 16 ++++-- web/public/locales/en.json | 5 ++ web/public/locales/pt-BR.json | 5 ++ 13 files changed, 248 insertions(+), 20 deletions(-) create mode 100644 db/migrations/0040_add_go2rtc_source_override.sql diff --git a/db/migrations/0040_add_go2rtc_source_override.sql b/db/migrations/0040_add_go2rtc_source_override.sql new file mode 100644 index 00000000..fd9dd6b4 --- /dev/null +++ b/db/migrations/0040_add_go2rtc_source_override.sql @@ -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. diff --git a/include/core/config.h b/include/core/config.h index 5f00663a..930304bb 100644 --- a/include/core/config.h +++ b/include/core/config.h @@ -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 diff --git a/src/database/db_streams.c b/src/database/db_streams.c index 6b5c06fe..3047d010 100644 --- a/src/database/db_streams.c +++ b/src/database/db_streams.c @@ -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); @@ -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); @@ -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) { @@ -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); @@ -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); @@ -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); @@ -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 @@ -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); @@ -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; } @@ -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) @@ -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); @@ -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++; } diff --git a/src/video/go2rtc/go2rtc_integration.c b/src/video/go2rtc/go2rtc_integration.c index bf7c445f..516ea886 100644 --- a/src/video/go2rtc/go2rtc_integration.c +++ b/src/video/go2rtc/go2rtc_integration.c @@ -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; + } + log_info("Registering stream %s with go2rtc", streams[i].name); // Register the stream with go2rtc @@ -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; + } // Check if stream already exists in go2rtc if (go2rtc_api_stream_exists(db_streams[i].name)) { @@ -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; + } + } + stream_config_t config; if (get_stream_config(stream, &config) != 0) { log_error("Failed to get config for stream %s", stream_name); diff --git a/src/video/go2rtc/go2rtc_process.c b/src/video/go2rtc/go2rtc_process.c index 67704108..1114e3ed 100644 --- a/src/video/go2rtc/go2rtc_process.c +++ b/src/video/go2rtc/go2rtc_process.c @@ -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 @@ -697,9 +699,58 @@ 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; + 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); + + // Write each line of the override with proper indentation + const char *p = streams[i].go2rtc_source_override; + while (*p) { + // Skip leading whitespace on each line + while (*p == ' ' || *p == '\t') p++; + if (*p == '\0') break; + + const char *eol = strchr(p, '\n'); + if (eol) { + if (eol > p) { // skip empty lines + fprintf(config_file, " %.*s\n", (int)(eol - p), p); + } + p = eol + 1; + } else { + fprintf(config_file, " %s\n", p); + break; + } + } + } + + 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 + && global_override[0] != '\0') { + fprintf(config_file, "\n# User config override\n"); + fprintf(config_file, "%s\n", global_override); + } + } fclose(config_file); log_info("Generated go2rtc configuration file: %s", config_path); diff --git a/src/web/api_handlers_settings.c b/src/web/api_handlers_settings.c index 38ebd36f..67763150 100644 --- a/src/web/api_handlers_settings.c +++ b/src/web/api_handlers_settings.c @@ -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" @@ -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", ""); + } + } + // MQTT settings cJSON_AddBoolToObject(settings, "mqtt_enabled", g_config.mqtt_enabled); cJSON_AddStringToObject(settings, "mqtt_broker_host", g_config.mqtt_broker_host); @@ -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; + } + } + // MQTT enabled cJSON *mqtt_enabled = cJSON_GetObjectItem(settings, "mqtt_enabled"); if (mqtt_enabled && cJSON_IsBool(mqtt_enabled)) { diff --git a/src/web/api_handlers_streams_get.c b/src/web/api_handlers_streams_get.c index 470fa0cd..65458a71 100644 --- a/src/web/api_handlers_streams_get.c +++ b/src/web/api_handlers_streams_get.c @@ -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); @@ -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. @@ -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). diff --git a/src/web/api_handlers_streams_modify.c b/src/web/api_handlers_streams_modify.c index 393368dd..cc61a141 100644 --- a/src/web/api_handlers_streams_modify.c +++ b/src/web/api_handlers_streams_modify.c @@ -658,6 +658,15 @@ void handle_post_stream(const http_request_t *req, http_response_t *res) { config.motion_trigger_source[0] = '\0'; } + // Parse go2rtc source override + cJSON *go2rtc_source_override_post = cJSON_GetObjectItem(stream_json, "go2rtc_source_override"); + if (go2rtc_source_override_post && cJSON_IsString(go2rtc_source_override_post)) { + safe_strcpy(config.go2rtc_source_override, go2rtc_source_override_post->valuestring, + sizeof(config.go2rtc_source_override), 0); + } else { + config.go2rtc_source_override[0] = '\0'; + } + // Check if isOnvif flag is set in the request cJSON *is_onvif = cJSON_GetObjectItem(stream_json, "isOnvif"); if (is_onvif && cJSON_IsBool(is_onvif)) { @@ -1265,6 +1274,26 @@ void handle_put_stream(const http_request_t *req, http_response_t *res) { } } + // Parse go2rtc source override + cJSON *go2rtc_source_override_put = cJSON_GetObjectItem(stream_json, "go2rtc_source_override"); + if (go2rtc_source_override_put && cJSON_IsString(go2rtc_source_override_put)) { + if (strncmp(config.go2rtc_source_override, go2rtc_source_override_put->valuestring, + sizeof(config.go2rtc_source_override) - 1) != 0) { + safe_strcpy(config.go2rtc_source_override, go2rtc_source_override_put->valuestring, + sizeof(config.go2rtc_source_override), 0); + config_changed = true; + requires_restart = true; + log_info("go2rtc source override changed for stream %s", config.name); + } + } else if (go2rtc_source_override_put && cJSON_IsNull(go2rtc_source_override_put)) { + if (config.go2rtc_source_override[0] != '\0') { + config.go2rtc_source_override[0] = '\0'; + config_changed = true; + requires_restart = true; + log_info("go2rtc source override cleared for stream %s", config.name); + } + } + // Update is_onvif flag based on request or URL bool original_is_onvif = config.is_onvif; diff --git a/web/js/components/preact/SettingsView.jsx b/web/js/components/preact/SettingsView.jsx index 594af8ff..112fa756 100644 --- a/web/js/components/preact/SettingsView.jsx +++ b/web/js/components/preact/SettingsView.jsx @@ -74,6 +74,7 @@ export function SettingsView() { go2rtcExternalIp: '', go2rtcIceServers: '', go2rtcForceNativeHls: false, + go2rtcConfigOverride: '', // MQTT settings mqttEnabled: false, mqttBrokerHost: 'localhost', @@ -297,6 +298,7 @@ export function SettingsView() { go2rtcExternalIp: settingsData.go2rtc_external_ip || '', go2rtcIceServers: settingsData.go2rtc_ice_servers || '', go2rtcForceNativeHls: settingsData.go2rtc_force_native_hls || false, + go2rtcConfigOverride: settingsData.go2rtc_config_override || '', // MQTT settings mqttEnabled: settingsData.mqtt_enabled || false, mqttBrokerHost: settingsData.mqtt_broker_host || 'localhost', @@ -393,6 +395,7 @@ export function SettingsView() { go2rtc_external_ip: settings.go2rtcExternalIp, go2rtc_ice_servers: settings.go2rtcIceServers, go2rtc_force_native_hls: settings.go2rtcForceNativeHls, + go2rtc_config_override: settings.go2rtcConfigOverride, // MQTT settings mqtt_enabled: settings.mqttEnabled, mqtt_broker_host: settings.mqttBrokerHost, @@ -1512,6 +1515,25 @@ export function SettingsView() { {t('settings.iceServersHelp')} + +
+ +
+