Merge pull request #40 from lunasaw/dev-fix-obj

Implement lazy initialization and caching for ONVIF services
This commit is contained in:
Flavio Pompermaier
2025-07-02 09:52:03 +02:00
committed by GitHub
4 changed files with 560 additions and 57 deletions

View File

@@ -45,12 +45,15 @@ public class OnvifDevice {
private final URL url; // Example http://host:port, https://host, http://host, http://ip_address
private Device device;
private Media media;
private PTZ ptz;
private ImagingPort imaging;
private EventPortType events;
private volatile Media media;
private volatile PTZ ptz;
private volatile ImagingPort imaging;
private volatile EventPortType events;
public final EventService eventService = new EventService();
// Cache capabilities to avoid repeated network requests
private volatile Capabilities capabilities;
private static boolean verbose = false; // enable/disable logging of SOAP messages
final SimpleSecurityHandler securityHandler;
@@ -112,56 +115,43 @@ public class OnvifDevice {
}
/**
* Initalizes the addresses used for SOAP messages and to get the internal IP, if given IP is a
* proxy.
* Initialize ONVIF device connection, only creates Device service, other services use lazy initialization
* This strategy significantly reduces initial memory usage and startup time
*
* @throws ConnectException Get thrown if device doesn't give answers to GetCapabilities()
* @throws SOAPException
* @throws ConnectException Thrown when device is inaccessible or invalid, doesn't respond to SOAP messages
* @throws SOAPException SOAP related exceptions
*/
protected void init() throws ConnectException, SOAPException {
logger.debug("Initializing ONVIF device connection: {}", url);
DeviceService deviceService = new DeviceService(null, DeviceService.SERVICE);
BindingProvider deviceServicePort = (BindingProvider) deviceService.getDevicePort();
this.device =
getServiceProxy(deviceServicePort, url.toString() + DEVICE_SERVICE).create(Device.class);
// resetSystemDateAndTime(); // don't modify the camera in a constructor.. :)
// Use OnvifServiceFactory to create Device service proxy
this.device = OnvifServiceFactory.createServiceProxy(
deviceServicePort,
url.toString() + DEVICE_SERVICE,
Device.class,
securityHandler,
verbose
);
Capabilities capabilities = this.device.getCapabilities(List.of(CapabilityCategory.ALL));
if (capabilities == null) {
// Get and cache capabilities to avoid subsequent repeated network requests
this.capabilities = this.device.getCapabilities(List.of(CapabilityCategory.ALL));
if (this.capabilities == null) {
throw new ConnectException("Capabilities not reachable.");
}
if (capabilities.getMedia() != null && capabilities.getMedia().getXAddr() != null) {
this.media = new MediaService().getMediaPort();
this.media =
getServiceProxy((BindingProvider) media, capabilities.getMedia().getXAddr())
.create(Media.class);
}
if (capabilities.getPTZ() != null && capabilities.getPTZ().getXAddr() != null) {
this.ptz = new PtzService().getPtzPort();
this.ptz =
getServiceProxy((BindingProvider) ptz, capabilities.getPTZ().getXAddr())
.create(PTZ.class);
}
if (capabilities.getImaging() != null && capabilities.getImaging().getXAddr() != null) {
this.imaging = new ImagingService().getImagingPort();
this.imaging =
getServiceProxy((BindingProvider) imaging, capabilities.getImaging().getXAddr())
.create(ImagingPort.class);
}
if (capabilities.getEvents() != null && capabilities.getEvents().getXAddr() != null) {
this.events = eventService.getEventPort();
this.events =
getServiceProxy((BindingProvider) events, capabilities.getEvents().getXAddr())
.create(EventPortType.class);
}
logger.debug("Device initialization completed, capabilities cached, other services will be initialized on demand");
// Note: Other services (Media, PTZ, Imaging, Events) now use lazy initialization
// They will only be created on first access, significantly reducing memory usage
}
/**
* @deprecated Use OnvifServiceFactory instead for better memory management and Schema caching
* This method is retained only to ensure backward compatibility
*/
@Deprecated
public JaxWsProxyFactoryBean getServiceProxy(BindingProvider servicePort, String serviceAddr) {
JaxWsProxyFactoryBean proxyFactory = new JaxWsProxyFactoryBean();
@@ -236,22 +226,125 @@ public class OnvifDevice {
return device;
}
/**
* Get cached device capabilities information
* This information was obtained and cached during initialization to avoid repeated network requests
*/
public Capabilities getCapabilities() {
return capabilities;
}
public PTZ getPtz() {
initPtzService();
return ptz;
}
/**
* Lazy initialization of PTZ service
* Uses double-checked locking pattern to ensure thread safety
*/
private void initPtzService() {
if (ptz == null && capabilities.getPTZ() != null && capabilities.getPTZ().getXAddr() != null) {
synchronized (this) {
if (ptz == null) {
logger.debug("Lazy initializing PTZ service");
PtzService ptzService = new PtzService();
BindingProvider ptzServicePort = (BindingProvider) ptzService.getPtzPort();
this.ptz = OnvifServiceFactory.createServiceProxy(
ptzServicePort,
capabilities.getPTZ().getXAddr(),
PTZ.class,
securityHandler,
verbose
);
}
}
}
}
public Media getMedia() {
initMediaService();
return media;
}
/**
* Lazy initialization of Media service
* Uses double-checked locking pattern to ensure thread safety
*/
private void initMediaService() {
if (media == null && capabilities.getMedia() != null && capabilities.getMedia().getXAddr() != null) {
synchronized (this) {
if (media == null) {
logger.debug("Lazy initializing Media service");
MediaService mediaService = new MediaService();
BindingProvider mediaServicePort = (BindingProvider) mediaService.getMediaPort();
this.media = OnvifServiceFactory.createServiceProxy(
mediaServicePort,
capabilities.getMedia().getXAddr(),
Media.class,
securityHandler,
verbose
);
}
}
}
}
public ImagingPort getImaging() {
initImagingService();
return imaging;
}
/**
* Lazy initialization of Imaging service
* Uses double-checked locking pattern to ensure thread safety
*/
private void initImagingService() {
if (imaging == null && capabilities.getImaging() != null && capabilities.getImaging().getXAddr() != null) {
synchronized (this) {
if (imaging == null) {
logger.debug("Lazy initializing Imaging service");
ImagingService imagingService = new ImagingService();
BindingProvider imagingServicePort = (BindingProvider) imagingService.getImagingPort();
this.imaging = OnvifServiceFactory.createServiceProxy(
imagingServicePort,
capabilities.getImaging().getXAddr(),
ImagingPort.class,
securityHandler,
verbose
);
}
}
}
}
public EventPortType getEvents() {
initEventsService();
return events;
}
/**
* Lazy initialization of Events service
* Uses double-checked locking pattern to ensure thread safety
*/
private void initEventsService() {
if (events == null && capabilities.getEvents() != null && capabilities.getEvents().getXAddr() != null) {
synchronized (this) {
if (events == null) {
logger.debug("Lazy initializing Events service");
BindingProvider eventsServicePort = (BindingProvider) eventService.getEventPort();
this.events = OnvifServiceFactory.createServiceProxy(
eventsServicePort,
capabilities.getEvents().getXAddr(),
EventPortType.class,
securityHandler,
verbose
);
}
}
}
}
public DateTime getDate() {
return device.getSystemDateAndTime().getLocalDateTime();
}
@@ -281,9 +374,12 @@ public class OnvifDevice {
// returns http://host[:port]/path_for_snapshot
public String getSnapshotUri(String profileToken) {
MediaUri sceenshotUri = media.getSnapshotUri(profileToken);
if (sceenshotUri != null) {
return sceenshotUri.getUri();
Media mediaService = getMedia();
if (mediaService != null) {
MediaUri sceenshotUri = mediaService.getSnapshotUri(profileToken);
if (sceenshotUri != null) {
return sceenshotUri.getUri();
}
}
return "";
}
@@ -298,24 +394,34 @@ public class OnvifDevice {
// Get snapshot uri for profile with index
public String getSnapshotUri(int index) {
if (media.getProfiles().size() >= index)
return getSnapshotUri(media.getProfiles().get(index).getToken());
Media mediaService = getMedia();
if (mediaService != null && mediaService.getProfiles().size() > index) {
return getSnapshotUri(mediaService.getProfiles().get(index).getToken());
}
return "";
}
public String getStreamUri(int index) {
return getStreamUri(media.getProfiles().get(index).getToken());
Media mediaService = getMedia();
if (mediaService != null && mediaService.getProfiles().size() > index) {
return getStreamUri(mediaService.getProfiles().get(index).getToken());
}
return "";
}
// returns rtsp://host[:port]/path_for_rtsp
public String getStreamUri(String profileToken) {
StreamSetup streamSetup = new StreamSetup();
Transport t = new Transport();
t.setProtocol(TransportProtocol.RTSP);
streamSetup.setTransport(t);
streamSetup.setStream(StreamType.RTP_UNICAST);
MediaUri rtsp = media.getStreamUri(streamSetup, profileToken);
return rtsp != null ? rtsp.getUri() : "";
Media mediaService = getMedia();
if (mediaService != null) {
StreamSetup streamSetup = new StreamSetup();
Transport t = new Transport();
t.setProtocol(TransportProtocol.RTSP);
streamSetup.setTransport(t);
streamSetup.setStream(StreamType.RTP_UNICAST);
MediaUri rtsp = mediaService.getStreamUri(streamSetup, profileToken);
return rtsp != null ? rtsp.getUri() : "";
}
return "";
}
public static boolean isVerbose() {
@@ -325,4 +431,39 @@ public class OnvifDevice {
public static void setVerbose(boolean verbose) {
OnvifDevice.verbose = verbose;
}
/**
* Check if service is initialized
*/
public boolean isServiceInitialized(String serviceName) {
switch (serviceName.toLowerCase()) {
case "media": return media != null;
case "ptz": return ptz != null;
case "imaging": return imaging != null;
case "events": return events != null;
case "device": return device != null;
default: return false;
}
}
/**
* Get count of initialized services
*/
public int getInitializedServicesCount() {
int count = 0;
if (device != null) count++;
if (media != null) count++;
if (ptz != null) count++;
if (imaging != null) count++;
if (events != null) count++;
return count;
}
/**
* Clean up resources and clear cache (static method, affects all instances)
* Recommended to call when application shuts down
*/
public static void cleanupResources() {
OnvifServiceFactory.clearCache();
}
}

View File

@@ -0,0 +1,147 @@
package de.onvif.soap;
import jakarta.xml.ws.BindingProvider;
import org.apache.cxf.binding.soap.Soap12;
import org.apache.cxf.binding.soap.SoapBindingConfiguration;
import org.apache.cxf.endpoint.Client;
import org.apache.cxf.frontend.ClientProxy;
import org.apache.cxf.interceptor.LoggingInInterceptor;
import org.apache.cxf.interceptor.LoggingOutInterceptor;
import org.apache.cxf.jaxws.JaxWsProxyFactoryBean;
import org.apache.cxf.transport.http.HTTPConduit;
import org.apache.cxf.transports.http.configuration.HTTPClientPolicy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Service proxy factory that caches and reuses WSDL Schema parsing results.
* Significantly reduces memory usage and initialization time through caching mechanism.
*
* @author ONVIF Optimization Team
*/
public class OnvifServiceFactory {
private static final Logger logger = LoggerFactory.getLogger(OnvifServiceFactory.class);
// Core: Cache parsed proxy factory configurations using Map
private static final Map<String, JaxWsProxyFactoryBean> proxyCache = new ConcurrentHashMap<>();
/**
* Creates service proxy using cached Schema parsing results
*
* @param servicePort Service port binding provider
* @param serviceAddr Service address
* @param serviceClass Service class type
* @param securityHandler Security handler
* @param verbose Whether to enable verbose logging
* @return Service proxy instance
*/
public static synchronized <T> T createServiceProxy(
BindingProvider servicePort,
String serviceAddr,
Class<T> serviceClass,
SimpleSecurityHandler securityHandler,
boolean verbose) {
// Use service class name as cache key
String cacheKey = serviceClass.getName();
JaxWsProxyFactoryBean proxyFactory = proxyCache.get(cacheKey);
if (proxyFactory == null) {
logger.debug("First-time creating service proxy {} - parsing and caching Schema", serviceClass.getSimpleName());
// Parse Schema and cache on first creation
proxyFactory = createProxyFactory(servicePort, securityHandler, verbose);
proxyCache.put(cacheKey, proxyFactory);
} else {
logger.debug("Reusing cached service proxy configuration {} - skipping Schema parsing", serviceClass.getSimpleName());
}
// Set specific address for each instance - create new factory instance to avoid concurrency issues
JaxWsProxyFactoryBean instanceFactory = cloneProxyFactory(proxyFactory);
if (serviceAddr != null) {
instanceFactory.setAddress(serviceAddr);
}
return instanceFactory.create(serviceClass);
}
/**
* Create basic configuration for proxy factory
*/
private static JaxWsProxyFactoryBean createProxyFactory(
BindingProvider servicePort,
SimpleSecurityHandler securityHandler,
boolean verbose) {
JaxWsProxyFactoryBean proxyFactory = new JaxWsProxyFactoryBean();
proxyFactory.getHandlers();
proxyFactory.setServiceClass(servicePort.getClass());
SoapBindingConfiguration config = new SoapBindingConfiguration();
config.setVersion(Soap12.getInstance());
proxyFactory.setBindingConfig(config);
Client deviceClient = ClientProxy.getClient(servicePort);
if (verbose) {
// Enable SOAP message logging (for debugging/development only)
proxyFactory.getOutInterceptors().add(new LoggingOutInterceptor());
proxyFactory.getInInterceptors().add(new LoggingInInterceptor());
}
HTTPConduit http = (HTTPConduit) deviceClient.getConduit();
if (securityHandler != null) {
proxyFactory.getHandlers().add(securityHandler);
}
HTTPClientPolicy httpClientPolicy = http.getClient();
httpClientPolicy.setConnectionTimeout(36000);
httpClientPolicy.setReceiveTimeout(32000);
httpClientPolicy.setAllowChunking(false);
return proxyFactory;
}
/**
* Clone proxy factory to create independent instances
*/
private static JaxWsProxyFactoryBean cloneProxyFactory(JaxWsProxyFactoryBean original) {
JaxWsProxyFactoryBean clone = new JaxWsProxyFactoryBean();
// Copy basic configuration
clone.setServiceClass(original.getServiceClass());
clone.setBindingConfig(original.getBindingConfig());
// Copy handlers
clone.getHandlers().addAll(original.getHandlers());
clone.getInInterceptors().addAll(original.getInInterceptors());
clone.getOutInterceptors().addAll(original.getOutInterceptors());
return clone;
}
/**
* Clear cache - recommended to call when application shuts down
*/
public static void clearCache() {
logger.info("Clearing OnvifServiceFactory cache, releasing {} cached entries", proxyCache.size());
proxyCache.clear();
}
/**
* Get current cache size
*/
public static int getCacheSize() {
return proxyCache.size();
}
/**
* Check if specified service type is already cached
*/
public static boolean isCached(Class<?> serviceClass) {
return proxyCache.containsKey(serviceClass.getName());
}
}

View File

@@ -64,8 +64,22 @@ public class PullPointSubscriptionHandler {
eventWs.createPullPointSubscription(cpps);
final String serviceAddress = getWSAAddress(resp.getSubscriptionReference());
pullPointSubscription = device.getServiceProxy((BindingProvider) device.eventService.getEventPort(), serviceAddress).create(PullPointSubscription.class);
subscriptionManager = device.getServiceProxy((BindingProvider) device.eventService.getEventPort(), serviceAddress).create(SubscriptionManager.class);
// Use OnvifServiceFactory instead of deprecated getServiceProxy method
BindingProvider eventServicePort = (BindingProvider) device.eventService.getEventPort();
pullPointSubscription = OnvifServiceFactory.createServiceProxy(
eventServicePort,
serviceAddress,
PullPointSubscription.class,
device.securityHandler,
OnvifDevice.isVerbose()
);
subscriptionManager = OnvifServiceFactory.createServiceProxy(
eventServicePort,
serviceAddress,
SubscriptionManager.class,
device.securityHandler,
OnvifDevice.isVerbose()
);
final Client pullPointSubscriptionProxy = ClientProxy.getClient(pullPointSubscription);
final Client subscriptionManagerProxy = ClientProxy.getClient(subscriptionManager);
@@ -102,7 +116,7 @@ public class PullPointSubscriptionHandler {
var messageIdHdr = new Header(messageID, messageIDEl);
var toHdr = new Header(to, toEl);
var replyToHdr = new Header(replyTo, replyToEl);
headers.clear();
headers.add(actionHdr);
headers.add(messageIdHdr);

View File

@@ -0,0 +1,201 @@
package org.onvif.client;
import de.onvif.soap.OnvifDevice;
import de.onvif.soap.OnvifServiceFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* ONVIF Device Memory Optimization Demo
*
* Demonstrates the effects of new lazy initialization and Schema caching features:
* 1. Schema Caching: Avoid repeated WSDL parsing
* 2. Lazy Initialization: Create services only when needed
* 3. Capabilities Caching: Avoid repeated network requests
*
* @author ONVIF Optimization Team
*/
public class MemoryOptimizationDemo {
private static final Logger LOG = LoggerFactory.getLogger(MemoryOptimizationDemo.class);
public static void main(String[] args) throws IOException {
LOG.info("=== ONVIF Device Memory Optimization Demo ===");
OnvifCredentials creds = GetTestDevice.getOnvifCredentials(args);
if (creds == null) {
LOG.error("Please provide valid ONVIF device credentials");
return;
}
// Demo 1: Single device memory optimization
demonstrateSingleDeviceOptimization(creds);
// Demo 2: Multi-device Schema caching effects
demonstrateMultiDeviceCaching(creds);
// Demo 3: Lazy initialization effects
demonstrateLazyInitialization(creds);
// Clean up resources
OnvifDevice.cleanupResources();
LOG.info("=== Demo Completed ===");
}
/**
* Demonstrate memory optimization effects for single device
*/
private static void demonstrateSingleDeviceOptimization(OnvifCredentials creds) {
LOG.info("\n--- Demo 1: Single Device Memory Optimization ---");
try {
long startTime = System.currentTimeMillis();
long startMemory = getUsedMemory();
// Create device instance
OnvifDevice device = new OnvifDevice(creds.getHost(), creds.getUser(), creds.getPassword());
long initTime = System.currentTimeMillis() - startTime;
long initMemory = getUsedMemory() - startMemory;
LOG.info("🚀 Device initialization completed:");
LOG.info(" ⏱️ Initialization time: {}ms", initTime);
LOG.info(" 🧠 Initial memory usage: {}MB", initMemory / 1024 / 1024);
LOG.info(" 📊 Initialized services: {}/5", device.getInitializedServicesCount());
LOG.info(" 📋 Device info: {}", device.getDeviceInfo());
// Access services on demand
LOG.info("\n📺 Accessing Media service...");
if (device.getMedia() != null) {
LOG.info(" ✅ Media service initialized (lazy loading)");
LOG.info(" 📊 Currently initialized services: {}/5", device.getInitializedServicesCount());
}
LOG.info("\n🎮 Checking PTZ service...");
if (device.getPtz() != null) {
LOG.info(" ✅ PTZ service initialized (lazy loading)");
} else {
LOG.info(" ❌ PTZ service not available");
}
LOG.info("\n📊 Final statistics:");
LOG.info(" Initialized services: {}/5", device.getInitializedServicesCount());
LOG.info(" Total memory usage: {}MB", (getUsedMemory() - startMemory) / 1024 / 1024);
} catch (Exception e) {
LOG.error("Device connection failed: {}", e.getMessage());
}
}
/**
* Demonstrate multi-device Schema caching effects
*/
private static void demonstrateMultiDeviceCaching(OnvifCredentials creds) {
LOG.info("\n--- Demo 2: Multi-Device Schema Caching Effects ---");
List<OnvifDevice> devices = new ArrayList<>();
long totalStartTime = System.currentTimeMillis();
long startMemory = getUsedMemory();
try {
// Create multiple device instances to demonstrate caching effects
int deviceCount = 3; // Create 3 connections to the same device for demo purposes
for (int i = 0; i < deviceCount; i++) {
long deviceStartTime = System.currentTimeMillis();
OnvifDevice device = new OnvifDevice(creds.getHost(), creds.getUser(), creds.getPassword());
devices.add(device);
long deviceTime = System.currentTimeMillis() - deviceStartTime;
LOG.info("📱 Device {} initialization time: {}ms (cache {})",
i + 1, deviceTime, OnvifServiceFactory.getCacheSize() > 0 ? "active" : "inactive");
}
long totalTime = System.currentTimeMillis() - totalStartTime;
long totalMemory = getUsedMemory() - startMemory;
LOG.info("\n📈 Multi-device statistics:");
LOG.info(" Device count: {}", deviceCount);
LOG.info(" Total initialization time: {}ms", totalTime);
LOG.info(" Average initialization time: {}ms", totalTime / deviceCount);
LOG.info(" Total memory usage: {}MB", totalMemory / 1024 / 1024);
LOG.info(" Average memory usage: {}MB", totalMemory / deviceCount / 1024 / 1024);
LOG.info(" Schema cache entries: {}", OnvifServiceFactory.getCacheSize());
} catch (Exception e) {
LOG.error("Multi-device creation failed: {}", e.getMessage());
}
}
/**
* Demonstrate lazy initialization effects
*/
private static void demonstrateLazyInitialization(OnvifCredentials creds) {
LOG.info("\n--- Demo 3: Lazy Initialization Effects ---");
try {
OnvifDevice device = new OnvifDevice(creds.getHost(), creds.getUser(), creds.getPassword());
LOG.info("🏗️ Device creation completed, checking service initialization status:");
logServiceStatus(device);
LOG.info("\n📺 First access to Media service...");
device.getMedia();
logServiceStatus(device);
LOG.info("\n🎮 First access to PTZ service...");
device.getPtz();
logServiceStatus(device);
LOG.info("\n🖼 First access to Imaging service...");
device.getImaging();
logServiceStatus(device);
LOG.info("\n📡 First access to Events service...");
device.getEvents();
logServiceStatus(device);
LOG.info("\n🎯 Lazy initialization demo completed");
LOG.info(" Advantage: Resources are allocated only when services are actually needed");
LOG.info(" Result: Significantly reduced initial memory usage and startup time");
} catch (Exception e) {
LOG.error("Lazy initialization demo failed: {}", e.getMessage());
}
}
/**
* Log service initialization status
*/
private static void logServiceStatus(OnvifDevice device) {
LOG.info(" 📊 Service initialization status:");
LOG.info(" Device: {} | Media: {} | PTZ: {} | Imaging: {} | Events: {}",
device.isServiceInitialized("device") ? "" : "",
device.isServiceInitialized("media") ? "" : "",
device.isServiceInitialized("ptz") ? "" : "",
device.isServiceInitialized("imaging") ? "" : "",
device.isServiceInitialized("events") ? "" : ""
);
LOG.info(" Total: {}/5 services initialized", device.getInitializedServicesCount());
}
/**
* Get current used memory
*/
private static long getUsedMemory() {
Runtime runtime = Runtime.getRuntime();
runtime.gc(); // Suggest garbage collection for more accurate memory readings
return runtime.totalMemory() - runtime.freeMemory();
}
/**
* Format memory size
*/
private static String formatMemory(long bytes) {
return String.format("%.2f MB", bytes / 1024.0 / 1024.0);
}
}