From 5e0500b12eb2e09c40d75c6a78378401e1f0bdde Mon Sep 17 00:00:00 2001 From: erocm123 Date: Thu, 6 Aug 2026 17:02:43 -0600 Subject: [PATCH 1/2] Add Synology ONVIF event compatibility --- res/media_service_files/CreateProfile.xml | 13 +++++++ src/events_service.c | 42 ++++++++++++++++++----- src/media_service.c | 12 ++++++- src/onvif_simple_server.c | 19 +++++----- 4 files changed, 68 insertions(+), 18 deletions(-) create mode 100644 res/media_service_files/CreateProfile.xml diff --git a/res/media_service_files/CreateProfile.xml b/res/media_service_files/CreateProfile.xml new file mode 100644 index 0000000..8f21c03 --- /dev/null +++ b/res/media_service_files/CreateProfile.xml @@ -0,0 +1,13 @@ + + + + + + SynoProfile + + + + diff --git a/src/events_service.c b/src/events_service.c index 5ca08f3..4ef5813 100644 --- a/src/events_service.c +++ b/src/events_service.c @@ -511,9 +511,10 @@ int events_pull_messages() if (service_ctx.events[i].topic != NULL && strcmp("tns1:Device/Trigger/Relay", service_ctx.events[i].topic) == 0) { strcpy(data_name, "LogicalState"); } else if (service_ctx.events[i].topic != NULL - && (strstr(service_ctx.events[i].topic, "VideoSource/MotionAlarm") - || strstr(service_ctx.events[i].topic, "CellMotionDetector/Motion"))) { - // Use IsMotion for motion topics to match common client expectations + && strstr(service_ctx.events[i].topic, "VideoSource/MotionAlarm")) { + strcpy(data_name, "State"); + } else if (service_ctx.events[i].topic != NULL + && strstr(service_ctx.events[i].topic, "CellMotionDetector/Motion")) { strcpy(data_name, "IsMotion"); } else { strcpy(data_name, "State"); @@ -523,6 +524,10 @@ int events_pull_messages() const char *safe_source_name = service_ctx.events[i].source_name ? service_ctx.events[i].source_name : "Unknown"; const char *safe_source_value = service_ctx.events[i].source_value ? service_ctx.events[i].source_value : "Unknown"; + if (strstr(service_ctx.events[i].topic, "VideoSource/MotionAlarm")) { + safe_source_name = "Source"; + } + size = cat(dest, "events_service_files/PullMessages_2.xml", 14, @@ -1019,8 +1024,18 @@ int events_get_event_properties() /* walk through other tokens */ for (j = 0; j < 3; j++) { - sprintf(topic_ls[j], "<%s", token); - sprintf(topic_le[j], "", token); + /* + * Topic paths commonly specify the namespace only on the + * first component, such as tns1:VideoSource/MotionAlarm. + * Keep every generated XML element in the topic namespace. + */ + if (strchr(token, ':') != NULL) { + snprintf(topic_ls[j], sizeof(topic_ls[j]), "<%s", token); + snprintf(topic_le[j], sizeof(topic_le[j]), "", token); + } else { + snprintf(topic_ls[j], sizeof(topic_ls[j]), "", token); + } token = strtok(NULL, "/"); if (token == NULL) { @@ -1040,9 +1055,15 @@ int events_get_event_properties() strcpy(data_name, "LogicalState"); strcpy(data_type, "tt:RelayLogicalState"); } else if (service_ctx.events[i].topic != NULL - && (strstr(service_ctx.events[i].topic, "VideoSource/MotionAlarm") - || strstr(service_ctx.events[i].topic, "CellMotionDetector/Motion"))) { - // Advertise IsMotion for motion topics + && strstr(service_ctx.events[i].topic, "VideoSource/MotionAlarm")) { + /* + * Advertise the standard property schema expected by + * Synology for VideoSource/MotionAlarm. + */ + strcpy(data_name, "State"); + strcpy(data_type, "xsd:boolean"); + } else if (service_ctx.events[i].topic != NULL + && strstr(service_ctx.events[i].topic, "CellMotionDetector/Motion")) { strcpy(data_name, "IsMotion"); strcpy(data_type, "xsd:boolean"); } else { @@ -1054,6 +1075,11 @@ int events_get_event_properties() const char *safe_source_name = service_ctx.events[i].source_name ? service_ctx.events[i].source_name : "Unknown"; const char *safe_source_type = service_ctx.events[i].source_type ? service_ctx.events[i].source_type : "Unknown"; + if (strstr(service_ctx.events[i].topic, "VideoSource/MotionAlarm")) { + safe_source_name = "Source"; + safe_source_type = "tt:ReferenceToken"; + } + size = cat(dest, "events_service_files/GetEventProperties_2.xml", 20, diff --git a/src/media_service.c b/src/media_service.c index ea17a2c..7cf8d18 100644 --- a/src/media_service.c +++ b/src/media_service.c @@ -2252,7 +2252,17 @@ int media_set_audio_output_configuration() int media_delete_profile() { - // All profiles are fixed — deletion is not permitted + const char *token = get_element("ProfileToken", "Body"); + + if ((service_ctx.adv_synology_nvr == 1) + && (token != NULL) + && (strcasecmp(token, "SynoProfileToken") == 0)) { + long size = cat(NULL, "media_service_files/DeleteProfile.xml", 0); + output_http_headers(size); + return cat("stdout", "media_service_files/DeleteProfile.xml", 0); + } + + // All real profiles are fixed — deletion is not permitted send_fault("media_service", "Sender", "ter:InvalidArgVal", diff --git a/src/onvif_simple_server.c b/src/onvif_simple_server.c index 8b6ee0b..1fc0a2d 100644 --- a/src/onvif_simple_server.c +++ b/src/onvif_simple_server.c @@ -510,7 +510,8 @@ int main(int argc, char **argv) pre_auth = 1; } if ((strcasecmp("events_service", prog_name) == 0) - && (strcasecmp("GetServiceCapabilities", method) == 0 || strcasecmp("CreatePullPointSubscription", method) == 0 + && (strcasecmp("GetServiceCapabilities", method) == 0 || strcasecmp("GetEventProperties", method) == 0 + || strcasecmp("CreatePullPointSubscription", method) == 0 || strcasecmp("PullMessages", method) == 0 || strcasecmp("Renew", method) == 0 || strcasecmp("Unsubscribe", method) == 0 || strcasecmp("SetSynchronizationPoint", method) == 0)) { /* @@ -555,7 +556,8 @@ int main(int argc, char **argv) * Unsubscribe, SetSynchronizationPoint and GetServiceCapabilities. */ if ((strcasecmp("events_service", prog_name) == 0) - && (strcasecmp("GetServiceCapabilities", method) == 0 || strcasecmp("CreatePullPointSubscription", method) == 0 + && (strcasecmp("GetServiceCapabilities", method) == 0 || strcasecmp("GetEventProperties", method) == 0 + || strcasecmp("CreatePullPointSubscription", method) == 0 || strcasecmp("PullMessages", method) == 0 || strcasecmp("Renew", method) == 0 || strcasecmp("Unsubscribe", method) == 0 || strcasecmp("SetSynchronizationPoint", method) == 0)) { auth_error = 0; @@ -591,13 +593,12 @@ int main(int argc, char **argv) // Special case: Synology NVR CreateProfile bypass (before authentication) if ((service_ctx.adv_synology_nvr == 1) && (strcasecmp("media_service", prog_name) == 0) && (strcasecmp("CreateProfile", method) == 0)) { - log_debug("Synology NVR mode: bypassing authentication for CreateProfile"); - send_fault("media_service", - "Receiver", - "ter:Action", - "ter:MaxNVTProfiles", - "Max profile number reached", - "The maximum number of supported profiles supported by the device has been reached"); + log_debug("Synology NVR mode: returning synthetic CreateProfile response"); + + long size = cat(NULL, "media_service_files/CreateProfile.xml", 0); + output_http_headers(size); + cat("stdout", "media_service_files/CreateProfile.xml", 0); + close_xml(); return 0; } From 365e8688ceeacfffe283d7ffe5f0ccb30d9822fa Mon Sep 17 00:00:00 2001 From: erocm123 Date: Sat, 8 Aug 2026 09:00:40 -0600 Subject: [PATCH 2/2] Update comment to cover GetEventProperties --- src/onvif_simple_server.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/onvif_simple_server.c b/src/onvif_simple_server.c index 1fc0a2d..3b92b47 100644 --- a/src/onvif_simple_server.c +++ b/src/onvif_simple_server.c @@ -552,7 +552,7 @@ int main(int argc, char **argv) /* Events (tev): allow common subscription lifecycle methods as PRE_AUTH * when the service is running in anonymous mode (no username configured). - * This covers CreatePullPointSubscription, PullMessages, Renew, + * This covers CreatePullPointSubscription, PullMessages, Renew, GetEventProperties, * Unsubscribe, SetSynchronizationPoint and GetServiceCapabilities. */ if ((strcasecmp("events_service", prog_name) == 0)