mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2026-03-26 09:42:42 -04:00
refactor: simplify ONVIF timeout options to use seconds instead of ISO 8601
- Change pull_timeout and subscription_timeout from std::string (ISO 8601) to int (seconds) for simpler configuration and cleaner code - Add deprecation warning when PT prefix is detected in option values - Add FormatDurationSeconds() helper to construct ISO 8601 strings for SOAP - Use zm_utils.h functions: Split(), PairSplit(), StartsWith() - Remove unused parse_iso8601_duration_seconds() function New option format: pull_timeout=5,subscription_timeout=300 Old format (deprecated): pull_timeout=PT5S,subscription_timeout=PT300S Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,10 +20,9 @@
|
||||
#include "zm_monitor_onvif.h"
|
||||
#include "zm_monitor.h"
|
||||
#include "zm_signal.h"
|
||||
#include "zm_utils.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
#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;
|
||||
|
||||
|
||||
@@ -64,9 +64,9 @@ class ONVIF {
|
||||
bool warned_initialized_repeat; // Track if we've warned about repeated Initialized messages
|
||||
std::unordered_map<std::string, int> 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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user