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:
Ben Dailey
2026-01-21 12:58:21 -05:00
parent 620814fd06
commit 3ee2ab6dde
2 changed files with 88 additions and 171 deletions

View File

@@ -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;

View File

@@ -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