]> git.basschouten.com Git - openhab-addons.git/commitdiff
[ipcamera] Fix several issues and some improvements (#11300)
authorMatthew Skinner <matt@pcmus.com>
Sun, 10 Oct 2021 08:12:18 +0000 (19:12 +1100)
committerGitHub <noreply@github.com>
Sun, 10 Oct 2021 08:12:18 +0000 (10:12 +0200)
* Fixes an issue now that the binding is not polling the snapshot anymore where the jpg could return an old image from cache.
* Fixes an issue that could be caused if you spammed the refresh key whilst watching ipcamera.mjpeg
* Improvements in stopping the servlet and stopping any open streams if the pause button is pressed on a camera thing.
* Reduces memory, thread and open file descriptor resource use.
* Fixes empty passwords create bad log output for logged ffmpeg commands.
* Fix for INSTAR cameras that created a WARN about bad user/pass when setting up the alarm server.

Closes #11301

Signed-off-by: Matthew Skinner <matt@pcmus.com>
bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/Ffmpeg.java
bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/handler/IpCameraHandler.java
bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/onvif/OnvifConnection.java
bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/servlet/CameraServlet.java
bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/servlet/IpCameraServlet.java

index e2e51a11870cab6e99d6818c3b7bc53953e5af8d..9ca8a20711cd90efacdf75babf8166a7ab7bdf8d 100644 (file)
@@ -190,7 +190,12 @@ public class Ffmpeg {
     public void startConverting() {
         if (!ipCameraFfmpegThread.isAlive()) {
             ipCameraFfmpegThread = new IpCameraFfmpegThread();
-            logger.debug("Starting ffmpeg with this command now:{}", ffmpegCommand.replaceAll(password, "********"));
+            if (!password.isEmpty()) {
+                logger.debug("Starting ffmpeg with this command now:{}",
+                        ffmpegCommand.replaceAll(password, "********"));
+            } else {
+                logger.debug("Starting ffmpeg with this command now:{}", ffmpegCommand);
+            }
             ipCameraFfmpegThread.start();
             if (format.equals(FFmpegFormat.HLS)) {
                 ipCameraHandler.setChannelState(CHANNEL_START_STREAM, OnOffType.ON);
index c04077042e9ed3b78672c9f446fe4599f21387c8..92c2c462ccc7c2defa27f74e76f4030e3fbc940e 100644 (file)
@@ -23,6 +23,7 @@ import java.math.BigDecimal;
 import java.net.InetSocketAddress;
 import java.net.MalformedURLException;
 import java.net.URL;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -139,12 +140,12 @@ public class IpCameraHandler extends BaseThingHandler {
     public @Nullable Ffmpeg ffmpegSnapshot = null;
     public boolean streamingAutoFps = false;
     public boolean motionDetected = false;
-
+    public Instant currentSnapshotTime = Instant.now();
     private @Nullable ScheduledFuture<?> cameraConnectionJob = null;
     private @Nullable ScheduledFuture<?> pollCameraJob = null;
     private @Nullable ScheduledFuture<?> snapshotJob = null;
     private @Nullable Bootstrap mainBootstrap;
-    private EventLoopGroup mainEventLoopGroup = new NioEventLoopGroup();
+    private EventLoopGroup mainEventLoopGroup = new NioEventLoopGroup(1);
     private FullHttpRequest putRequestWithBody = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod("PUT"),
             "");
     private String gifFilename = "ipcamera";
@@ -436,7 +437,7 @@ public class IpCameraHandler extends BaseThingHandler {
             basicAuth = "";
             return false;
         } else if (!basicAuth.isEmpty()) {
-            // due to camera may have been sent multiple requests before the auth was set, this may trigger falsely.
+            // If the binding sends multiple requests before basicAuth was set, this may trigger falsely.
             logger.warn("Camera is reporting your username and/or password is wrong.");
             return false;
         }
@@ -662,6 +663,7 @@ public class IpCameraHandler extends BaseThingHandler {
             }
         } finally {
             lockCurrentSnapshot.unlock();
+            currentSnapshotTime = Instant.now();
         }
 
         if (updateImageChannel) {
@@ -676,10 +678,7 @@ public class IpCameraHandler extends BaseThingHandler {
     }
 
     public void startStreamServer() {
-        if (servlet == null) {
-            servlet = new CameraServlet(this, httpService);
-        }
-
+        servlet = new CameraServlet(this, httpService);
         updateState(CHANNEL_HLS_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
                 + getThing().getUID().getId() + "/ipcamera.m3u8"));
         updateState(CHANNEL_IMAGE_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
@@ -696,7 +695,7 @@ public class IpCameraHandler extends BaseThingHandler {
         sendHttpGET(mjpegUri);
     }
 
-    void openChannel(Channel channel, String httpRequestURL) {
+    private void openChannel(Channel channel, String httpRequestURL) {
         ChannelTracking tracker = channelTrackingMap.get(httpRequestURL);
         if (tracker != null && !tracker.getReply().isEmpty()) {// We need to keep the stored reply
             tracker.setChannel(channel);
@@ -1318,14 +1317,6 @@ public class IpCameraHandler extends BaseThingHandler {
         if (localFuture != null) {
             localFuture.cancel(false);
         }
-        if (thing.getThingTypeUID().getId().equals(INSTAR_THING)) {
-            logger.debug("Setting up the Alarm Server settings in the camera now");
-            sendHttpGET(
-                    "/param.cgi?cmd=setmdalarm&-aname=server2&-switch=on&-interval=1&cmd=setalarmserverattr&-as_index=3&-as_server="
-                            + hostIp + "&-as_port=" + SERVLET_PORT + "&-as_path=/ipcamera/"
-                            + getThing().getUID().getId()
-                            + "/instar&-as_queryattr1=&-as_queryval1=&-as_queryattr2=&-as_queryval2=&-as_queryattr3=&-as_queryval3=&-as_activequery=1&-as_auth=0&-as_query1=0&-as_query2=0&-as_query3=0");
-        }
         if (cameraConfig.getGifPreroll() > 0 || cameraConfig.getUpdateImageWhen().contains("1")) {
             snapshotPolling = true;
             snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 1000, cameraConfig.getPollTime(),
@@ -1428,6 +1419,18 @@ public class IpCameraHandler extends BaseThingHandler {
     }
 
     public byte[] getSnapshot() {
+        if (!isOnline) {
+            // Keep streams open when the camera goes offline so they dont stop.
+            return new byte[] { (byte) 0xff, (byte) 0xd8, (byte) 0xff, (byte) 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46,
+                    0x00, 0x01, 0x01, 0x01, 0x00, 0x48, 0x00, 0x48, 0x00, 0x00, (byte) 0xff, (byte) 0xdb, 0x00, 0x43,
+                    0x00, 0x03, 0x02, 0x02, 0x02, 0x02, 0x02, 0x03, 0x02, 0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x04,
+                    0x06, 0x04, 0x04, 0x04, 0x04, 0x04, 0x08, 0x06, 0x06, 0x05, 0x06, 0x09, 0x08, 0x0a, 0x0a, 0x09,
+                    0x08, 0x09, 0x09, 0x0a, 0x0c, 0x0f, 0x0c, 0x0a, 0x0b, 0x0e, 0x0b, 0x09, 0x09, 0x0d, 0x11, 0x0d,
+                    0x0e, 0x0f, 0x10, 0x10, 0x11, 0x10, 0x0a, 0x0c, 0x12, 0x13, 0x12, 0x10, 0x13, 0x0f, 0x10, 0x10,
+                    0x10, (byte) 0xff, (byte) 0xc9, 0x00, 0x0b, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00,
+                    (byte) 0xff, (byte) 0xcc, 0x00, 0x06, 0x00, 0x10, 0x10, 0x05, (byte) 0xff, (byte) 0xda, 0x00, 0x08,
+                    0x01, 0x01, 0x00, 0x00, 0x3f, 0x00, (byte) 0xd2, (byte) 0xcf, 0x20, (byte) 0xff, (byte) 0xd9 };
+        }
         if (!snapshotPolling && !ffmpegSnapshotGeneration) {
             sendHttpGET(snapshotUri);
         }
@@ -1478,13 +1481,7 @@ public class IpCameraHandler extends BaseThingHandler {
      *
      */
     void pollCameraRunnable() {
-        // Snapshot should be first to keep consistent time between shots
-        if (streamingAutoFps) {
-            if (!snapshotPolling && !ffmpegSnapshotGeneration) {
-                // Dont need to poll if creating from RTSP stream with FFmpeg or we are polling at full rate already.
-                sendHttpGET(snapshotUri);
-            }
-        } else if (!snapshotUri.isEmpty() && !snapshotPolling) {// we need to check camera is still online.
+        if (!snapshotUri.isEmpty() && !snapshotPolling) {// we need to check camera is still online.
             checkCameraConnection();
         }
         // NOTE: Use lowPriorityRequests if get request is not needed every poll.
@@ -1553,6 +1550,8 @@ public class IpCameraHandler extends BaseThingHandler {
     @Override
     public void initialize() {
         cameraConfig = getConfigAs(CameraConfig.class);
+        threadPool = Executors.newScheduledThreadPool(4);
+        mainEventLoopGroup = new NioEventLoopGroup(3);
         snapshotUri = getCorrectUrlFormat(cameraConfig.getSnapshotUrl());
         mjpegUri = getCorrectUrlFormat(cameraConfig.getMjpegUrl());
         rtspUri = cameraConfig.getFfmpegInput();
@@ -1607,11 +1606,24 @@ public class IpCameraHandler extends BaseThingHandler {
                 if (mjpegUri.isEmpty()) {
                     mjpegUri = "/mjpegstream.cgi?-chn=12";
                 }
+                sendHttpGET(
+                        "/param.cgi?cmd=setmdalarm&-aname=server2&-switch=on&-interval=1&cmd=setalarmserverattr&-as_index=3&-as_server="
+                                + hostIp + "&-as_port=" + SERVLET_PORT + "&-as_path=/ipcamera/"
+                                + getThing().getUID().getId()
+                                + "/instar&-as_queryattr1=&-as_queryval1=&-as_queryattr2=&-as_queryval2=&-as_queryattr3=&-as_queryval3=&-as_activequery=1&-as_auth=0&-as_query1=0&-as_query2=0&-as_query3=0");
                 break;
         }
-        // Onvif and Instar event handling need the host IP and the server started.
+        // for poll times 9 seconds and above don't display a warning about the Image channel.
+        if (9000 > cameraConfig.getPollTime() && cameraConfig.getUpdateImageWhen().contains("1")) {
+            logger.warn(
+                    "The Image channel is set to update more often than 8 seconds. This is not recommended. The Image channel is best used only for higher poll times. See the readme file on how to display the cameras picture for best results or use a higher poll time.");
+        }
+        // ONVIF and Instar event handling need the server started before connecting.
         startStreamServer();
+        tryConnecting();
+    }
 
+    private void tryConnecting() {
         if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)) {
             onvifCamera = new OnvifConnection(this, cameraConfig.getIp() + ":" + cameraConfig.getOnvifPort(),
                     cameraConfig.getUser(), cameraConfig.getPassword());
@@ -1619,78 +1631,87 @@ public class IpCameraHandler extends BaseThingHandler {
             // Only use ONVIF events if it is not an API camera.
             onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
         }
-
-        // for poll times 9 seconds and above don't display a warning about the Image channel.
-        if (9000 > cameraConfig.getPollTime() && cameraConfig.getUpdateImageWhen().contains("1")) {
-            logger.warn(
-                    "The Image channel is set to update more often than 8 seconds. This is not recommended. The Image channel is best used only for higher poll times. See the readme file on how to display the cameras picture for best results or use a higher poll time.");
-        }
-        // Waiting 3 seconds for ONVIF to discover the urls before running.
         cameraConnectionJob = threadPool.scheduleWithFixedDelay(this::pollingCameraConnection, 4, 30, TimeUnit.SECONDS);
     }
 
     // What the camera needs to re-connect if the initialize() is not called.
     private void resetAndRetryConnecting() {
-        dispose();
-        initialize();
+        offline();
+        tryConnecting();
     }
 
-    @Override
-    public void dispose() {
+    private void offline() {
         isOnline = false;
         snapshotPolling = false;
         Future<?> localFuture = pollCameraJob;
         if (localFuture != null) {
             localFuture.cancel(true);
+            localFuture = null;
         }
         localFuture = snapshotJob;
         if (localFuture != null) {
             localFuture.cancel(true);
+            localFuture = null;
         }
         localFuture = cameraConnectionJob;
         if (localFuture != null) {
             localFuture.cancel(true);
+            localFuture = null;
         }
-        threadPool.shutdown();
-        threadPool = Executors.newScheduledThreadPool(4);
-
-        groupTracker.listOfOnlineCameraHandlers.remove(this);
-        groupTracker.listOfOnlineCameraUID.remove(getThing().getUID().getId());
-        // inform all group handlers that this camera has gone offline
-        for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
-            handle.cameraOffline(this);
-        }
-        basicAuth = ""; // clear out stored Password hash
-        useDigestAuth = false;
-        openChannels.close();
-
         Ffmpeg localFfmpeg = ffmpegHLS;
         if (localFfmpeg != null) {
             localFfmpeg.stopConverting();
-            localFfmpeg = null;
+            ffmpegHLS = null;
         }
         localFfmpeg = ffmpegRecord;
         if (localFfmpeg != null) {
             localFfmpeg.stopConverting();
+            ffmpegRecord = null;
         }
         localFfmpeg = ffmpegGIF;
         if (localFfmpeg != null) {
             localFfmpeg.stopConverting();
+            ffmpegGIF = null;
         }
         localFfmpeg = ffmpegRtspHelper;
         if (localFfmpeg != null) {
             localFfmpeg.stopConverting();
+            ffmpegRtspHelper = null;
         }
         localFfmpeg = ffmpegMjpeg;
         if (localFfmpeg != null) {
             localFfmpeg.stopConverting();
+            ffmpegMjpeg = null;
         }
         localFfmpeg = ffmpegSnapshot;
         if (localFfmpeg != null) {
             localFfmpeg.stopConverting();
+            ffmpegSnapshot = null;
         }
-        channelTrackingMap.clear();
         onvifCamera.disconnect();
+        openChannels.close();
+    }
+
+    @Override
+    public void dispose() {
+        offline();
+        CameraServlet localServlet = servlet;
+        if (localServlet != null) {
+            localServlet.dispose();
+            localServlet = null;
+        }
+        threadPool.shutdown();
+        // inform all group handlers that this camera has gone offline
+        groupTracker.listOfOnlineCameraHandlers.remove(this);
+        groupTracker.listOfOnlineCameraUID.remove(getThing().getUID().getId());
+        for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
+            handle.cameraOffline(this);
+        }
+        basicAuth = ""; // clear out stored Password hash
+        useDigestAuth = false;
+        mainEventLoopGroup.shutdownGracefully();
+        mainBootstrap = null;
+        channelTrackingMap.clear();
     }
 
     public String getWhiteList() {
index be4f96e75ae37a77cb55808d0f464b094d6e08fe..81fdc50c5e6997d095417103f1dff7ba71cf1ecc 100644 (file)
@@ -114,7 +114,7 @@ public class OnvifConnection {
     private final Logger logger = LoggerFactory.getLogger(getClass());
     private ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(2);
     private @Nullable Bootstrap bootstrap;
-    private EventLoopGroup mainEventLoopGroup = new NioEventLoopGroup();
+    private EventLoopGroup mainEventLoopGroup = new NioEventLoopGroup(2);
     private String ipAddress = "";
     private String user = "";
     private String password = "";
@@ -857,10 +857,15 @@ public class OnvifConnection {
     }
 
     public void disconnect() {
-        if (usingEvents && isConnected && !mainEventLoopGroup.isShuttingDown()) {
-            sendOnvifRequest(requestBuilder(RequestType.Unsubscribe, subscriptionXAddr));
+        if (bootstrap != null) {
+            if (usingEvents && isConnected && !mainEventLoopGroup.isShuttingDown()) {
+                // Some cameras may continue to send events even when they can't reach a server.
+                sendOnvifRequest(requestBuilder(RequestType.Unsubscribe, subscriptionXAddr));
+            }
+            // give time for the Unsubscribe request to be sent to the camera.
+            threadPool.schedule(this::cleanup, 100, TimeUnit.MILLISECONDS);
+        } else {
+            cleanup();
         }
-        // Some cameras may continue to send event callbacks even when they cant reach a server.
-        threadPool.schedule(this::cleanup, 500, TimeUnit.MILLISECONDS);
     }
 }
index 12ca6b259e8997e6134fab9348a408a3c16fd8eb..d8f439151c553e1eed824720e10bc2290aca267e 100644 (file)
@@ -15,13 +15,17 @@ package org.openhab.binding.ipcamera.internal.servlet;
 import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.HLS_STARTUP_DELAY_MS;
 
 import java.io.IOException;
+import java.time.Duration;
+import java.time.Instant;
 
+import javax.servlet.AsyncContext;
 import javax.servlet.ServletInputStream;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.ipcamera.internal.ChannelTracking;
 import org.openhab.binding.ipcamera.internal.Ffmpeg;
 import org.openhab.binding.ipcamera.internal.InstarHandler;
 import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
@@ -38,9 +42,9 @@ import org.osgi.service.http.HttpService;
 public class CameraServlet extends IpCameraServlet {
     private static final long serialVersionUID = -134658667574L;
     private final IpCameraHandler handler;
-    private int autofpsStreamsOpen = 0;
-    private int snapshotStreamsOpen = 0;
     public OpenStreams openStreams = new OpenStreams();
+    private OpenStreams openSnapshotStreams = new OpenStreams();
+    private OpenStreams openAutoFpsStreams = new OpenStreams();
 
     public CameraServlet(IpCameraHandler handler, HttpService httpService) {
         super(handler, httpService);
@@ -121,22 +125,46 @@ public class CameraServlet extends IpCameraServlet {
                 sendFile(resp, pathInfo, "image/gif");
                 return;
             case "/ipcamera.jpg":
-                sendSnapshotImage(resp, "image/jpg", handler.getSnapshot());
+                // Use cached image if recent. Cameras can take > 1sec to send back a reply.
+                // Example an Image item/widget may have a 1 second refresh.
+                if (handler.ffmpegSnapshotGeneration
+                        || Duration.between(handler.currentSnapshotTime, Instant.now()).toMillis() < 1200) {
+                    sendSnapshotImage(resp, "image/jpg", handler.getSnapshot());
+                } else {
+                    handler.getSnapshot();
+                    final AsyncContext acontext = req.startAsync(req, resp);
+                    acontext.start(new Runnable() {
+                        @Override
+                        public void run() {
+                            Instant startTime = Instant.now();
+                            do {
+                                try {
+                                    Thread.sleep(100);
+                                } catch (InterruptedException e) {
+                                    return;
+                                }
+                            } // 5 sec timeout OR a new snapshot comes back from camera
+                            while (Duration.between(startTime, Instant.now()).toMillis() < 5000
+                                    && Duration.between(handler.currentSnapshotTime, Instant.now()).toMillis() > 1200);
+                            sendSnapshotImage(resp, "image/jpg", handler.getSnapshot());
+                            acontext.complete();
+                        }
+                    });
+                }
                 return;
             case "/snapshots.mjpeg":
-                req.getSession().setMaxInactiveInterval(0);
-                snapshotStreamsOpen++;
                 handler.streamingSnapshotMjpeg = true;
                 handler.startSnapshotPolling();
                 StreamOutput output = new StreamOutput(resp);
+                openSnapshotStreams.addStream(output);
                 do {
                     try {
                         output.sendSnapshotBasedFrame(handler.getSnapshot());
                         Thread.sleep(1005);
                     } catch (InterruptedException | IOException e) {
                         // Never stop streaming until IOException. Occurs when browser stops the stream.
-                        snapshotStreamsOpen--;
-                        if (snapshotStreamsOpen == 0) {
+                        openSnapshotStreams.removeStream(output);
+                        if (openSnapshotStreams.isEmpty()) {
                             handler.streamingSnapshotMjpeg = false;
                             handler.stopSnapshotPolling();
                             logger.debug("All snapshots.mjpeg streams have stopped.");
@@ -145,7 +173,6 @@ public class CameraServlet extends IpCameraServlet {
                     }
                 } while (true);
             case "/ipcamera.mjpeg":
-                req.getSession().setMaxInactiveInterval(0);
                 if (handler.mjpegUri.isEmpty() || "ffmpeg".equals(handler.mjpegUri)) {
                     if (openStreams.isEmpty()) {
                         handler.setupFfmpegFormat(FFmpegFormat.MJPEG);
@@ -158,7 +185,11 @@ public class CameraServlet extends IpCameraServlet {
                     output = new StreamOutput(resp, handler.mjpegContentType);
                     openStreams.addStream(output);
                 } else {
-                    logger.debug("Not the first stream requested. Stream from camera already open");
+                    ChannelTracking tracker = handler.channelTrackingMap.get(handler.mjpegUri);
+                    if (tracker == null || !tracker.getChannel().isOpen()) {
+                        logger.warn("Not the first stream requested but the stream from camera was closed");
+                        handler.openCamerasStream();
+                    }
                     output = new StreamOutput(resp, handler.mjpegContentType);
                     openStreams.addStream(output);
                 }
@@ -181,12 +212,11 @@ public class CameraServlet extends IpCameraServlet {
                         }
                         return;
                     }
-                } while (true);
+                } while (!openStreams.isEmpty());
             case "/autofps.mjpeg":
-                req.getSession().setMaxInactiveInterval(0);
-                autofpsStreamsOpen++;
                 handler.streamingAutoFps = true;
                 output = new StreamOutput(resp);
+                openAutoFpsStreams.addStream(output);
                 int counter = 0;
                 do {
                     try {
@@ -200,8 +230,8 @@ public class CameraServlet extends IpCameraServlet {
                         Thread.sleep(1000);
                     } catch (InterruptedException | IOException e) {
                         // Never stop streaming until IOException. Occurs when browser stops the stream.
-                        autofpsStreamsOpen--;
-                        if (autofpsStreamsOpen == 0) {
+                        openAutoFpsStreams.removeStream(output);
+                        if (openAutoFpsStreams.isEmpty()) {
                             handler.streamingAutoFps = false;
                             logger.debug("All autofps.mjpeg streams have stopped.");
                         }
@@ -237,6 +267,8 @@ public class CameraServlet extends IpCameraServlet {
     @Override
     public void dispose() {
         openStreams.closeAllStreams();
+        openSnapshotStreams.closeAllStreams();
+        openAutoFpsStreams.closeAllStreams();
         super.dispose();
     }
 }
index 9ce0efdf622067ee67650330d3c349f188ffb125..66364f8bea1d08a51ca22bae1fc1525ee4fe860b 100644 (file)
@@ -18,15 +18,14 @@ import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
 
-import javax.servlet.ServletException;
 import javax.servlet.ServletOutputStream;
+import javax.servlet.annotation.WebServlet;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletResponse;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.openhab.core.thing.binding.ThingHandler;
 import org.osgi.service.http.HttpService;
-import org.osgi.service.http.NamespaceException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -37,6 +36,7 @@ import org.slf4j.LoggerFactory;
  * @author Matthew Skinner - Initial contribution
  */
 @NonNullByDefault
+@WebServlet(asyncSupported = true)
 public abstract class IpCameraServlet extends HttpServlet {
     protected final Logger logger = LoggerFactory.getLogger(this.getClass());
     private static final long serialVersionUID = 1L;
@@ -53,7 +53,7 @@ public abstract class IpCameraServlet extends HttpServlet {
         try {
             httpService.registerServlet("/ipcamera/" + handler.getThing().getUID().getId(), this, null,
                     httpService.createDefaultHttpContext());
-        } catch (NamespaceException | ServletException e) {
+        } catch (Exception e) {
             logger.warn("Registering servlet failed:{}", e.getMessage());
         }
     }