diff --git a/src/zm_monitor_onvif.cpp b/src/zm_monitor_onvif.cpp index d645239cb..0a2cfa0e3 100644 --- a/src/zm_monitor_onvif.cpp +++ b/src/zm_monitor_onvif.cpp @@ -20,10 +20,9 @@ #include "zm_monitor_onvif.h" #include "zm_monitor.h" #include "zm_signal.h" +#include "zm_utils.h" #include -#include -#include #include "url.hpp" // ONVIF configuration constants @@ -34,57 +33,9 @@ namespace { const int ONVIF_RETRY_EXPONENT_LIMIT = 9; // 2^9 = 512, cap before overflow const int ONVIF_RENEWAL_ADVANCE_SECONDS = 60; // Renew subscription N seconds before expiration - // Parse ISO 8601 duration string to seconds - // Supports formats like "PT20S", "PT1M", "PT1H30M45S" - // Returns -1 on parse error - int parse_iso8601_duration_seconds(const std::string& duration) { - if (duration.empty() || duration.size() < 3) { - return -1; - } - - // Must start with "PT" (Period of Time) - if (duration[0] != 'P' || duration[1] != 'T') { - return -1; - } - - int total_seconds = 0; - int current_value = 0; - bool has_digit = false; - - // Parse from position 2 onwards (after "PT") - for (size_t i = 2; i < duration.size(); i++) { - char c = duration[i]; - - if (c >= '0' && c <= '9') { - current_value = current_value * 10 + (c - '0'); - has_digit = true; - } else if (c == 'H' && has_digit) { - // Hours - total_seconds += current_value * 3600; - current_value = 0; - has_digit = false; - } else if (c == 'M' && has_digit) { - // Minutes - total_seconds += current_value * 60; - current_value = 0; - has_digit = false; - } else if (c == 'S' && has_digit) { - // Seconds - total_seconds += current_value; - current_value = 0; - has_digit = false; - } else { - // Invalid character - return -1; - } - } - - // If we still have unparsed digits, format is invalid - if (has_digit) { - return -1; - } - - return total_seconds; + // Format seconds as ISO 8601 duration string (e.g., 5 -> "PT5S") + inline std::string FormatDurationSeconds(int seconds) { + return "PT" + std::to_string(seconds) + "S"; } } #endif @@ -116,8 +67,8 @@ ONVIF::ONVIF(Monitor *parent_) : ,retry_count(0) ,max_retries(5) ,warned_initialized_repeat(false) - ,pull_timeout("PT5S") - ,subscription_timeout("PT300S") + ,pull_timeout_seconds(5) + ,subscription_timeout_seconds(300) ,soap_log_fd(nullptr) ,subscription_termination_time() ,next_renewal_time() @@ -187,19 +138,11 @@ void ONVIF::start() { soap = nullptr; } - // Validate pull_timeout before creating subscription - int pull_timeout_seconds = parse_iso8601_duration_seconds(pull_timeout); - if (pull_timeout_seconds < 0) { - Error("ONVIF: Invalid pull_timeout format: %s, adjusting to PT5S", pull_timeout.c_str()); - pull_timeout = "PT5S"; - pull_timeout_seconds = 5; - } - + // Clamp pull_timeout_seconds to be less than renewal advance time if (pull_timeout_seconds >= ONVIF_RENEWAL_ADVANCE_SECONDS) { + Warning("ONVIF: pull_timeout %ds must be less than renewal advance time (%ds). Adjusting.", + pull_timeout_seconds, ONVIF_RENEWAL_ADVANCE_SECONDS); pull_timeout_seconds = ONVIF_RENEWAL_ADVANCE_SECONDS - 1; - pull_timeout = "PT" + std::to_string(pull_timeout_seconds) + "S"; - Warning("ONVIF: pull_timeout %ds must be less than renewal advance time (%ds) to ensure timely renewals. Adjusting to %s", - pull_timeout_seconds, ONVIF_RENEWAL_ADVANCE_SECONDS, pull_timeout.c_str()); } soap = soap_new(); @@ -351,11 +294,12 @@ void ONVIF::start() { _tev__PullMessages tev__PullMessages; _tev__PullMessagesResponse tev__PullMessagesResponse; - tev__PullMessages.Timeout = pull_timeout.c_str(); + std::string pull_timeout_str = FormatDurationSeconds(pull_timeout_seconds); + tev__PullMessages.Timeout = pull_timeout_str.c_str(); tev__PullMessages.MessageLimit = 10; - Debug(2, "ONVIF: Using pull_timeout=%s, subscription_timeout=%s at %s", - pull_timeout.c_str(), subscription_timeout.c_str(), response.SubscriptionReference.Address); + Debug(2, "ONVIF: Using pull_timeout=%ds, subscription_timeout=%ds at %s", + pull_timeout_seconds, subscription_timeout_seconds, response.SubscriptionReference.Address); if ((proxyEvent.PullMessages(response.SubscriptionReference.Address, nullptr, &tev__PullMessages, tev__PullMessagesResponse) != SOAP_OK) && (soap->error != SOAP_EOF) ) { //SOAP_EOF could indicate no messages to pull. @@ -418,10 +362,11 @@ void ONVIF::WaitForMessage() { _tev__PullMessages tev__PullMessages; _tev__PullMessagesResponse tev__PullMessagesResponse; - tev__PullMessages.Timeout = pull_timeout.c_str(); + std::string pull_timeout_str = FormatDurationSeconds(pull_timeout_seconds); + tev__PullMessages.Timeout = pull_timeout_str.c_str(); tev__PullMessages.MessageLimit = 10; - Debug(1, "ONVIF: Starting PullMessageRequest with Timeout=%s, MessageLimit=%d ...", - pull_timeout.c_str(), tev__PullMessages.MessageLimit); + Debug(1, "ONVIF: Starting PullMessageRequest with Timeout=%ds, MessageLimit=%d ...", + pull_timeout_seconds, tev__PullMessages.MessageLimit); int result = proxyEvent.PullMessages(response.SubscriptionReference.Address, nullptr, &tev__PullMessages, tev__PullMessagesResponse); if (result != SOAP_OK) { const char *detail = soap_fault_detail(soap); @@ -724,97 +669,76 @@ void ONVIF::cleanup_subscription() { // Parse ONVIF options from the onvif_options string // Format: key1=value1,key2=value2 // Supported options: -// pull_timeout=PT20S - Timeout for PullMessages requests -// subscription_timeout=PT60S - Timeout for subscription renewal +// pull_timeout=5 - Timeout in seconds for PullMessages requests (default: 5) +// subscription_timeout=300 - Timeout in seconds for subscription renewal (default: 300) // max_retries=5 - Maximum retry attempts // soap_log=/path/to/logfile - Enable SOAP message logging void ONVIF::parse_onvif_options() { if (parent->onvif_options.empty()) { + Info("ONVIF: Using pull_timeout=%ds, subscription_timeout=%ds", + pull_timeout_seconds, subscription_timeout_seconds); return; } - + Debug(2, "ONVIF: Parsing options: %s", parent->onvif_options.c_str()); - - // Helper lambda to parse a single option - auto parse_option = [this](const std::string &option) { - size_t eq_pos = option.find('='); - if (eq_pos != std::string::npos) { - std::string key = option.substr(0, eq_pos); - std::string value = option.substr(eq_pos + 1); - - if (key == "pull_timeout") { - pull_timeout = value; - Debug(2, "ONVIF: Set pull_timeout to %s", pull_timeout.c_str()); - - // Validate pull_timeout immediately - int pull_timeout_seconds = parse_iso8601_duration_seconds(pull_timeout); - if (pull_timeout_seconds < 0) { - Error("ONVIF: Invalid pull_timeout format: %s, adjusting to PT5S", pull_timeout.c_str()); - pull_timeout = "PT5S"; - pull_timeout_seconds = 5; - } - - if (pull_timeout_seconds >= ONVIF_RENEWAL_ADVANCE_SECONDS) { - pull_timeout_seconds = ONVIF_RENEWAL_ADVANCE_SECONDS - 1; - pull_timeout = "PT" + std::to_string(pull_timeout_seconds) + "S"; - Warning("ONVIF: pull_timeout (%ds) must be less than renewal advance time (%ds) to ensure timely renewals. Adjusting to %s", - pull_timeout_seconds, ONVIF_RENEWAL_ADVANCE_SECONDS, pull_timeout.c_str()); - } - } else if (key == "subscription_timeout") { - subscription_timeout = value; - Debug(2, "ONVIF: Set subscription_timeout to %s", subscription_timeout.c_str()); - } else if (key == "max_retries") { + + for (const std::string &option : Split(parent->onvif_options, ',')) { + auto [key, value] = PairSplit(option, '='); + if (key.empty()) continue; + + if (key == "pull_timeout") { + if (StartsWith(value, "PT")) { + Warning("ONVIF: ISO 8601 duration format (e.g., 'PT5S') is no longer supported for pull_timeout. " + "Please use seconds (e.g., '5'). Using default %d seconds.", pull_timeout_seconds); + } else { try { - max_retries = std::stoi(value); - if (max_retries < 0) max_retries = 0; - if (max_retries > ONVIF_MAX_RETRIES_LIMIT) max_retries = ONVIF_MAX_RETRIES_LIMIT; - Debug(2, "ONVIF: Set max_retries to %d", max_retries); + int val = std::stoi(value); + if (val > 0) { + pull_timeout_seconds = val; + Debug(2, "ONVIF: Set pull_timeout to %d seconds", pull_timeout_seconds); + } else { + Warning("ONVIF: Invalid pull_timeout value '%s', using default %d seconds", value.c_str(), pull_timeout_seconds); + } } catch (const std::exception &e) { - Error("ONVIF: Invalid max_retries value '%s': %s", value.c_str(), e.what()); + Warning("ONVIF: Invalid pull_timeout value '%s': %s. Using default %d seconds", value.c_str(), e.what(), pull_timeout_seconds); } - } else if (key == "soap_log") { - soap_log_file = value; - Debug(2, "ONVIF: Will enable SOAP logging to %s", soap_log_file.c_str()); - } else if (option.find("closes_event") != std::string::npos) { - // Option to indicate that ONVIF will send a close event message - closes_event = true; } + } else if (key == "subscription_timeout") { + if (StartsWith(value, "PT")) { + Warning("ONVIF: ISO 8601 duration format (e.g., 'PT300S') is no longer supported for subscription_timeout. " + "Please use seconds (e.g., '300'). Using default %d seconds.", subscription_timeout_seconds); + } else { + try { + int val = std::stoi(value); + if (val > 0) { + subscription_timeout_seconds = val; + Debug(2, "ONVIF: Set subscription_timeout to %d seconds", subscription_timeout_seconds); + } else { + Warning("ONVIF: Invalid subscription_timeout value '%s', using default %d seconds", value.c_str(), subscription_timeout_seconds); + } + } catch (const std::exception &e) { + Warning("ONVIF: Invalid subscription_timeout value '%s': %s. Using default %d seconds", value.c_str(), e.what(), subscription_timeout_seconds); + } + } + } else if (key == "max_retries") { + try { + max_retries = std::stoi(value); + if (max_retries < 0) max_retries = 0; + if (max_retries > ONVIF_MAX_RETRIES_LIMIT) max_retries = ONVIF_MAX_RETRIES_LIMIT; + Debug(2, "ONVIF: Set max_retries to %d", max_retries); + } catch (const std::exception &e) { + Error("ONVIF: Invalid max_retries value '%s': %s", value.c_str(), e.what()); + } + } else if (key == "soap_log") { + soap_log_file = value; + Debug(2, "ONVIF: Will enable SOAP logging to %s", soap_log_file.c_str()); + } else if (key == "closes_event") { + closes_event = true; } - }; - - std::string options = parent->onvif_options; - size_t start = 0; - size_t pos = 0; - - while ((pos = options.find(',', start)) != std::string::npos) { - std::string option = options.substr(start, pos - start); - parse_option(option); - start = pos + 1; - } - - // Handle last option (no trailing comma) - if (start < options.length()) { - std::string option = options.substr(start); - parse_option(option); - } - - // Final validation of pull_timeout (in case it was not set in options and we're using default) - int pull_timeout_seconds = parse_iso8601_duration_seconds(pull_timeout); - if (pull_timeout_seconds < 0) { - Error("ONVIF: Invalid pull_timeout format: %s, adjusting to PT5S", pull_timeout.c_str()); - pull_timeout = "PT5S"; - pull_timeout_seconds = 5; - } - - if (pull_timeout_seconds >= ONVIF_RENEWAL_ADVANCE_SECONDS) { - pull_timeout_seconds = ONVIF_RENEWAL_ADVANCE_SECONDS - 1; - pull_timeout = "PT" + std::to_string(pull_timeout_seconds) + "S"; - Warning("ONVIF: pull_timeout (%ds) must be less than renewal advance time (%ds) to ensure timely renewals. Adjusting to %s", - pull_timeout_seconds, ONVIF_RENEWAL_ADVANCE_SECONDS, pull_timeout.c_str()); } - Info("ONVIF: Using pull_timeout=%s (%d seconds), subscription_timeout=%s", - pull_timeout.c_str(), pull_timeout_seconds, subscription_timeout.c_str()); + Info("ONVIF: Using pull_timeout=%ds, subscription_timeout=%ds", + pull_timeout_seconds, subscription_timeout_seconds); } // Calculate exponential backoff delay for retries @@ -924,35 +848,28 @@ bool ONVIF::Renew() { _wsnt__Renew wsnt__Renew; _wsnt__RenewResponse wsnt__RenewResponse; - std::string absolute_time_str; - + std::string termination_time_str; + if (use_absolute_time_for_renewal) { // Calculate absolute termination time: current time + subscription duration - int subscription_timeout_seconds = parse_iso8601_duration_seconds(subscription_timeout); - if (subscription_timeout_seconds < 0) { - Warning("ONVIF: Invalid subscription_timeout format: %s, using default 60 seconds", subscription_timeout.c_str()); - subscription_timeout_seconds = 60; - } else { - Debug(3, "Have subscription timeout duration %dseconds", subscription_timeout_seconds); - } - time_t now = time(nullptr); time_t absolute_termination = now + subscription_timeout_seconds; - absolute_time_str = format_absolute_time_iso8601(absolute_termination); - - if (absolute_time_str.empty()) { + termination_time_str = format_absolute_time_iso8601(absolute_termination); + + if (termination_time_str.empty()) { Error("ONVIF: Failed to format absolute time for renewal"); return false; } - - wsnt__Renew.TerminationTime = &absolute_time_str; - Debug(1, "ONVIF: Setting renew termination time to absolute time: %s (camera requires absolute time format)", - absolute_time_str.c_str()); + + Debug(1, "ONVIF: Setting renew termination time to absolute time: %s (camera requires absolute time format)", + termination_time_str.c_str()); } else { // Use duration format (default behavior) - wsnt__Renew.TerminationTime = &subscription_timeout; - Debug(1, "ONVIF: Setting renew termination time to duration: %s", subscription_timeout.c_str()); + termination_time_str = FormatDurationSeconds(subscription_timeout_seconds); + Debug(1, "ONVIF: Setting renew termination time to duration: %s", termination_time_str.c_str()); } + + wsnt__Renew.TerminationTime = &termination_time_str; bool use_wsa = parent->soap_wsa_compl; diff --git a/src/zm_monitor_onvif.h b/src/zm_monitor_onvif.h index 22169f8a4..c6a94d4c4 100644 --- a/src/zm_monitor_onvif.h +++ b/src/zm_monitor_onvif.h @@ -64,9 +64,9 @@ class ONVIF { bool warned_initialized_repeat; // Track if we've warned about repeated Initialized messages std::unordered_map initialized_count; // Track Initialized message count per topic - // Configurable timeout values (can be set via onvif_options) - std::string pull_timeout; // Default "PT20S" - std::string subscription_timeout; // Default "PT60S" + // Configurable timeout values in seconds (can be set via onvif_options) + int pull_timeout_seconds; // Default 5 seconds + int subscription_timeout_seconds; // Default 300 seconds (5 minutes) std::string soap_log_file; // SOAP message logging file (empty = disabled) FILE *soap_log_fd; // File descriptor for SOAP logging