]> git.basschouten.com Git - openhab-addons.git/blob
4af99acb981222e7cccee1aa033711c4e5d2d22c
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.ipcamera.internal.handler;
14
15 import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
16
17 import java.io.File;
18 import java.io.FileNotFoundException;
19 import java.io.FileOutputStream;
20 import java.io.IOException;
21 import java.io.OutputStream;
22 import java.math.BigDecimal;
23 import java.net.InetSocketAddress;
24 import java.net.MalformedURLException;
25 import java.net.URL;
26 import java.nio.charset.StandardCharsets;
27 import java.time.Duration;
28 import java.time.Instant;
29 import java.util.ArrayList;
30 import java.util.Collection;
31 import java.util.LinkedList;
32 import java.util.List;
33 import java.util.Map;
34 import java.util.Set;
35 import java.util.concurrent.ConcurrentHashMap;
36 import java.util.concurrent.Executors;
37 import java.util.concurrent.Future;
38 import java.util.concurrent.ScheduledExecutorService;
39 import java.util.concurrent.ScheduledFuture;
40 import java.util.concurrent.TimeUnit;
41 import java.util.concurrent.locks.ReentrantLock;
42
43 import org.eclipse.jdt.annotation.NonNullByDefault;
44 import org.eclipse.jdt.annotation.Nullable;
45 import org.openhab.binding.ipcamera.internal.AmcrestHandler;
46 import org.openhab.binding.ipcamera.internal.CameraConfig;
47 import org.openhab.binding.ipcamera.internal.ChannelTracking;
48 import org.openhab.binding.ipcamera.internal.DahuaHandler;
49 import org.openhab.binding.ipcamera.internal.DoorBirdHandler;
50 import org.openhab.binding.ipcamera.internal.Ffmpeg;
51 import org.openhab.binding.ipcamera.internal.FoscamHandler;
52 import org.openhab.binding.ipcamera.internal.GroupTracker;
53 import org.openhab.binding.ipcamera.internal.Helper;
54 import org.openhab.binding.ipcamera.internal.HikvisionHandler;
55 import org.openhab.binding.ipcamera.internal.HttpOnlyHandler;
56 import org.openhab.binding.ipcamera.internal.InstarHandler;
57 import org.openhab.binding.ipcamera.internal.IpCameraActions;
58 import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
59 import org.openhab.binding.ipcamera.internal.IpCameraDynamicStateDescriptionProvider;
60 import org.openhab.binding.ipcamera.internal.MyNettyAuthHandler;
61 import org.openhab.binding.ipcamera.internal.ReolinkHandler;
62 import org.openhab.binding.ipcamera.internal.onvif.OnvifConnection;
63 import org.openhab.binding.ipcamera.internal.servlet.CameraServlet;
64 import org.openhab.core.OpenHAB;
65 import org.openhab.core.library.types.DecimalType;
66 import org.openhab.core.library.types.IncreaseDecreaseType;
67 import org.openhab.core.library.types.OnOffType;
68 import org.openhab.core.library.types.PercentType;
69 import org.openhab.core.library.types.RawType;
70 import org.openhab.core.library.types.StringType;
71 import org.openhab.core.thing.ChannelUID;
72 import org.openhab.core.thing.Thing;
73 import org.openhab.core.thing.ThingStatus;
74 import org.openhab.core.thing.ThingStatusDetail;
75 import org.openhab.core.thing.binding.BaseThingHandler;
76 import org.openhab.core.thing.binding.ThingHandlerService;
77 import org.openhab.core.thing.binding.builder.ThingBuilder;
78 import org.openhab.core.types.Command;
79 import org.openhab.core.types.RefreshType;
80 import org.openhab.core.types.State;
81 import org.osgi.framework.FrameworkUtil;
82 import org.osgi.service.http.HttpService;
83 import org.slf4j.Logger;
84 import org.slf4j.LoggerFactory;
85
86 import io.netty.bootstrap.Bootstrap;
87 import io.netty.buffer.ByteBuf;
88 import io.netty.buffer.Unpooled;
89 import io.netty.channel.Channel;
90 import io.netty.channel.ChannelDuplexHandler;
91 import io.netty.channel.ChannelFuture;
92 import io.netty.channel.ChannelFutureListener;
93 import io.netty.channel.ChannelHandlerContext;
94 import io.netty.channel.ChannelInitializer;
95 import io.netty.channel.ChannelOption;
96 import io.netty.channel.EventLoopGroup;
97 import io.netty.channel.group.ChannelGroup;
98 import io.netty.channel.group.DefaultChannelGroup;
99 import io.netty.channel.nio.NioEventLoopGroup;
100 import io.netty.channel.socket.SocketChannel;
101 import io.netty.channel.socket.nio.NioSocketChannel;
102 import io.netty.handler.codec.base64.Base64;
103 import io.netty.handler.codec.http.DefaultFullHttpRequest;
104 import io.netty.handler.codec.http.FullHttpRequest;
105 import io.netty.handler.codec.http.HttpClientCodec;
106 import io.netty.handler.codec.http.HttpContent;
107 import io.netty.handler.codec.http.HttpHeaderValues;
108 import io.netty.handler.codec.http.HttpMessage;
109 import io.netty.handler.codec.http.HttpMethod;
110 import io.netty.handler.codec.http.HttpResponse;
111 import io.netty.handler.codec.http.HttpVersion;
112 import io.netty.handler.codec.http.LastHttpContent;
113 import io.netty.handler.timeout.IdleState;
114 import io.netty.handler.timeout.IdleStateEvent;
115 import io.netty.handler.timeout.IdleStateHandler;
116 import io.netty.util.CharsetUtil;
117 import io.netty.util.ReferenceCountUtil;
118 import io.netty.util.concurrent.GlobalEventExecutor;
119
120 /**
121  * The {@link IpCameraHandler} is responsible for handling commands, which are
122  * sent to one of the channels.
123  *
124  * @author Matthew Skinner - Initial contribution
125  */
126
127 @NonNullByDefault
128 public class IpCameraHandler extends BaseThingHandler {
129     public final Logger logger = LoggerFactory.getLogger(getClass());
130     public final IpCameraDynamicStateDescriptionProvider stateDescriptionProvider;
131     private ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(2);
132     private GroupTracker groupTracker;
133     public CameraConfig cameraConfig = new CameraConfig();
134
135     // ChannelGroup is thread safe
136     public final ChannelGroup openChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
137     private final HttpService httpService;
138     private @Nullable CameraServlet servlet;
139     public String mjpegContentType = "";
140     public @Nullable Ffmpeg ffmpegHLS = null;
141     public @Nullable Ffmpeg ffmpegRecord = null;
142     public @Nullable Ffmpeg ffmpegGIF = null;
143     public @Nullable Ffmpeg ffmpegRtspHelper = null;
144     public @Nullable Ffmpeg ffmpegMjpeg = null;
145     public @Nullable Ffmpeg ffmpegSnapshot = null;
146     public boolean streamingAutoFps = false;
147     public boolean motionDetected = false;
148     public Instant lastSnapshotRequest = Instant.now();
149     public Instant currentSnapshotTime = Instant.now();
150     private @Nullable ScheduledFuture<?> cameraConnectionJob = null;
151     private @Nullable ScheduledFuture<?> pollCameraJob = null;
152     private @Nullable ScheduledFuture<?> snapshotJob = null;
153     private @Nullable ScheduledFuture<?> authenticationJob = null;
154     private @Nullable Bootstrap mainBootstrap;
155     private EventLoopGroup mainEventLoopGroup = new NioEventLoopGroup(1);
156     private FullHttpRequest putRequestWithBody = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.PUT, "");
157     private FullHttpRequest postRequestWithBody = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "");
158     private String gifFilename = "ipcamera";
159     private String gifHistory = "";
160     private String mp4History = "";
161     public int gifHistoryLength;
162     public int mp4HistoryLength;
163     private String mp4Filename = "ipcamera";
164     private int mp4RecordTime;
165     private int gifRecordTime = 5;
166     private LinkedList<byte[]> fifoSnapshotBuffer = new LinkedList<byte[]>();
167     private int snapCount;
168     private boolean updateImageChannel = false;
169     private byte lowPriorityCounter = 0;
170     public String hostIp;
171     public Map<String, ChannelTracking> channelTrackingMap = new ConcurrentHashMap<>();
172     public List<String> lowPriorityRequests = new ArrayList<>(0);
173
174     // basicAuth MUST remain private as it holds the cameraConfig.getPassword()
175     private String basicAuth = "";
176     public String reolinkAuth = "&token=null";
177     public boolean useBasicAuth = false;
178     public boolean useDigestAuth = false;
179     public boolean newInstarApi = false;
180     public String snapshotUri = "";
181     public String mjpegUri = "";
182     private byte[] currentSnapshot = new byte[] { (byte) 0x00 };
183     public ReentrantLock lockCurrentSnapshot = new ReentrantLock();
184     public String rtspUri = "";
185     public boolean audioAlarmUpdateSnapshot = false;
186     private boolean motionAlarmUpdateSnapshot = false;
187     private boolean isOnline = false; // Used so only 1 error is logged when a network issue occurs.
188     private boolean firstAudioAlarm = false;
189     private boolean firstMotionAlarm = false;
190     public BigDecimal motionThreshold = BigDecimal.ZERO;
191     public int audioThreshold = 35;
192     public boolean streamingSnapshotMjpeg = false;
193     public boolean ffmpegMotionAlarmEnabled = false;
194     public boolean ffmpegAudioAlarmEnabled = false;
195     public boolean ffmpegSnapshotGeneration = false;
196     public boolean snapshotPolling = false;
197     public OnvifConnection onvifCamera = new OnvifConnection(this, "", "", "");
198
199     // These methods handle the response from all camera brands, nothing specific to 1 brand.
200     private class CommonCameraHandler extends ChannelDuplexHandler {
201         private int bytesToRecieve = 0;
202         private int bytesAlreadyRecieved = 0;
203         private byte[] incomingJpeg = new byte[0];
204         private String incomingMessage = "";
205         private String contentType = "empty";
206         private String boundary = "";
207         private Object reply = new Object();
208         private String requestUrl = "";
209         private boolean isChunked = false;
210
211         public void setURL(String url) {
212             requestUrl = url;
213         }
214
215         @Override
216         public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
217             if (msg == null || ctx == null) {
218                 return;
219             }
220             try {
221                 if (msg instanceof HttpResponse response) {
222                     if (response.status().code() == 200) {
223                         if (!response.headers().isEmpty()) {
224                             for (String name : response.headers().names()) {
225                                 // Some cameras use first letter uppercase and others dont.
226                                 switch (name.toLowerCase()) { // Possible localization issues doing this
227                                     case "content-type":
228                                         contentType = response.headers().getAsString(name);
229                                         break;
230                                     case "content-length":
231                                         bytesToRecieve = Integer.parseInt(response.headers().getAsString(name));
232                                         break;
233                                     case "transfer-encoding":
234                                         if (response.headers().getAsString(name).contains("chunked")) {
235                                             isChunked = true;
236                                         }
237                                         break;
238                                 }
239                             }
240                             if (contentType.contains("multipart")) {
241                                 boundary = Helper.searchString(contentType, "boundary=");
242                                 if (mjpegUri.equals(requestUrl)) {
243                                     if (msg instanceof HttpMessage) {
244                                         // very start of stream only
245                                         mjpegContentType = contentType;
246                                         CameraServlet localServlet = servlet;
247                                         if (localServlet != null) {
248                                             logger.debug("Setting Content-Type to:{}", contentType);
249                                             localServlet.openStreams.updateContentType(contentType, boundary);
250                                         }
251                                     }
252                                 }
253                             } else if (contentType.contains("image/jp")) {
254                                 if (bytesToRecieve == 0) {
255                                     bytesToRecieve = 768000; // 0.768 Mbyte when no Content-Length is sent
256                                     logger.debug("Camera has no Content-Length header, we have to guess how much RAM.");
257                                 }
258                                 incomingJpeg = new byte[bytesToRecieve];
259                             }
260                         }
261                     } else {
262                         // Non 200 OK replies are logged and handled in pipeline by MyNettyAuthHandler.java
263                         return;
264                     }
265                 }
266                 if (msg instanceof HttpContent content) {
267                     if (mjpegUri.equals(requestUrl) && !(content instanceof LastHttpContent)) {
268                         // multiple MJPEG stream packets come back as this.
269                         byte[] chunkedFrame = new byte[content.content().readableBytes()];
270                         content.content().getBytes(content.content().readerIndex(), chunkedFrame);
271                         CameraServlet localServlet = servlet;
272                         if (localServlet != null) {
273                             localServlet.openStreams.queueFrame(chunkedFrame);
274                         }
275                     } else {
276                         // Found some cameras use Content-Type: image/jpg instead of image/jpeg
277                         if (contentType.contains("image/jp")) {
278                             for (int i = 0; i < content.content().capacity(); i++) {
279                                 incomingJpeg[bytesAlreadyRecieved++] = content.content().getByte(i);
280                             }
281                             if (content instanceof LastHttpContent) {
282                                 processSnapshot(incomingJpeg);
283                                 ctx.close();
284                             }
285                         } else { // incomingMessage that is not an IMAGE
286                             if (incomingMessage.isEmpty()) {
287                                 incomingMessage = content.content().toString(CharsetUtil.UTF_8);
288                             } else {
289                                 incomingMessage += content.content().toString(CharsetUtil.UTF_8);
290                             }
291                             bytesAlreadyRecieved = incomingMessage.length();
292                             if (content instanceof LastHttpContent) {
293                                 // If it is not an image send it on to the next handler//
294                                 if (bytesAlreadyRecieved != 0) {
295                                     reply = incomingMessage;
296                                     super.channelRead(ctx, reply);
297                                 }
298                             }
299                             // Alarm Streams never have a LastHttpContent as they always stay open//
300                             else if (contentType.contains("multipart")) {
301                                 int beginIndex, endIndex;
302                                 if (bytesToRecieve == 0) {
303                                     beginIndex = incomingMessage.indexOf("Content-Length:");
304                                     if (beginIndex != -1) {
305                                         endIndex = incomingMessage.indexOf("\r\n", beginIndex);
306                                         if (endIndex != -1) {
307                                             bytesToRecieve = Integer.parseInt(
308                                                     incomingMessage.substring(beginIndex + 15, endIndex).strip());
309                                         }
310                                     }
311                                 }
312                                 // --boundary and headers are not included in the Content-Length value
313                                 if (bytesAlreadyRecieved > bytesToRecieve) {
314                                     // Check if message has a second --boundary
315                                     endIndex = incomingMessage.indexOf("--" + boundary, bytesToRecieve);
316                                     if (endIndex == -1) {
317                                         reply = incomingMessage;
318                                         incomingMessage = "";
319                                         bytesToRecieve = 0;
320                                         bytesAlreadyRecieved = 0;
321                                     } else {
322                                         reply = incomingMessage.substring(0, endIndex);
323                                         incomingMessage = incomingMessage.substring(endIndex, incomingMessage.length());
324                                         bytesToRecieve = 0;// Triggers search next time for Content-Length:
325                                         bytesAlreadyRecieved = incomingMessage.length() - endIndex;
326                                     }
327                                     super.channelRead(ctx, reply);
328                                 }
329                             }
330                             // Foscam needs this as will other cameras with chunks//
331                             if (isChunked && bytesAlreadyRecieved != 0) {
332                                 reply = incomingMessage;
333                             }
334                         }
335                     }
336                 } else { // msg is not HttpContent
337                     // Foscam cameras need this
338                     if (!contentType.contains("image/jp") && bytesAlreadyRecieved != 0) {
339                         reply = incomingMessage;
340                         logger.trace("Packet back from camera is {}", incomingMessage);
341                         super.channelRead(ctx, reply);
342                     }
343                 }
344             } finally {
345                 ReferenceCountUtil.release(msg);
346             }
347         }
348
349         @Override
350         public void exceptionCaught(@Nullable ChannelHandlerContext ctx, @Nullable Throwable cause) {
351             if (cause == null || ctx == null) {
352                 return;
353             }
354             if (cause instanceof ArrayIndexOutOfBoundsException) {
355                 logger.debug("Camera sent {} bytes when the content-length header was {}.", bytesAlreadyRecieved,
356                         bytesToRecieve);
357             } else {
358                 logger.warn("!!!! Camera possibly closed the channel on the binding, cause reported is: {}",
359                         cause.getMessage());
360             }
361             ctx.close();
362         }
363
364         @Override
365         public void userEventTriggered(@Nullable ChannelHandlerContext ctx, @Nullable Object evt) throws Exception {
366             if (ctx == null) {
367                 return;
368             }
369             if (evt instanceof IdleStateEvent e) {
370                 // If camera does not use the channel for X amount of time it will close.
371                 if (e.state() == IdleState.READER_IDLE) {
372                     String urlToKeepOpen = "";
373                     switch (thing.getThingTypeUID().getId()) {
374                         case DAHUA_THING:
375                             urlToKeepOpen = "/cgi-bin/eventManager.cgi?action=attach&codes=[All]";
376                             break;
377                         case DOORBIRD_THING:
378                             urlToKeepOpen = "/bha-api/monitor.cgi?ring=doorbell,motionsensor";
379                             break;
380                     }
381                     ChannelTracking channelTracking = channelTrackingMap.get(urlToKeepOpen);
382                     if (channelTracking != null) {
383                         if (channelTracking.getChannel().equals(ctx.channel())) {
384                             return; // don't auto close this as it is for the alarms.
385                         }
386                     }
387                     logger.debug("Closing an idle channel for camera:{}", cameraConfig.getIp());
388                     ctx.close();
389                 }
390             }
391         }
392     }
393
394     public IpCameraHandler(Thing thing, @Nullable String ipAddress, GroupTracker groupTracker,
395             IpCameraDynamicStateDescriptionProvider stateDescriptionProvider, HttpService httpService) {
396         super(thing);
397         this.stateDescriptionProvider = stateDescriptionProvider;
398         if (ipAddress != null) {
399             hostIp = ipAddress;
400         } else {
401             hostIp = Helper.getLocalIpAddress();
402         }
403         this.groupTracker = groupTracker;
404         this.httpService = httpService;
405     }
406
407     private IpCameraHandler getHandle() {
408         return this;
409     }
410
411     // false clears the stored user/pass hash, true creates the hash
412     public boolean setBasicAuth(boolean useBasic) {
413         if (!useBasic) {
414             logger.debug("Clearing out the stored BASIC auth now.");
415             basicAuth = "";
416             return false;
417         } else if (!basicAuth.isEmpty()) {
418             // If the binding sends multiple requests before basicAuth was set, this may trigger falsely.
419             logger.warn("Camera is reporting your username and/or password is wrong.");
420             return false;
421         }
422         if (!cameraConfig.getUser().isEmpty() && !cameraConfig.getPassword().isEmpty()) {
423             String authString = cameraConfig.getUser() + ":" + cameraConfig.getPassword();
424             ByteBuf byteBuf = null;
425             try {
426                 byteBuf = Base64.encode(Unpooled.wrappedBuffer(authString.getBytes(CharsetUtil.UTF_8)));
427                 basicAuth = byteBuf.getCharSequence(0, byteBuf.capacity(), CharsetUtil.UTF_8).toString();
428             } finally {
429                 if (byteBuf != null) {
430                     byteBuf.release();
431                 }
432             }
433             return true;
434         } else {
435             cameraConfigError("Camera is asking for Basic Auth when you have not provided a username and/or password.");
436         }
437         return false;
438     }
439
440     private String getCorrectUrlFormat(String longUrl) {
441         String temp = longUrl;
442         URL url;
443
444         if (longUrl.isEmpty() || "ffmpeg".equals(longUrl)) {
445             return longUrl;
446         }
447
448         try {
449             url = new URL(longUrl);
450             int port = url.getPort();
451             if (port == -1) {
452                 if (url.getQuery() == null) {
453                     temp = url.getPath();
454                 } else {
455                     temp = url.getPath() + "?" + url.getQuery();
456                 }
457             } else {
458                 if (url.getQuery() == null) {
459                     temp = ":" + url.getPort() + url.getPath();
460                 } else {
461                     temp = ":" + url.getPort() + url.getPath() + "?" + url.getQuery();
462                 }
463             }
464         } catch (MalformedURLException e) {
465             cameraConfigError("A non valid URL has been given to the binding, check they work in a browser.");
466         }
467         return temp;
468     }
469
470     public void sendHttpPUT(String httpRequestURL, FullHttpRequest request) {
471         putRequestWithBody = request; // use Global so the authhandler can use it when resent with DIGEST.
472         sendHttpRequest("PUT", httpRequestURL, null);
473     }
474
475     public void sendHttpPOST(String httpPostURL, String content) {
476         FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, httpPostURL);
477         request.headers().set("Host", cameraConfig.getIp());
478         request.headers().add("Content-Type", "application/json");
479         request.headers().add("User-Agent",
480                 "openHAB/" + FrameworkUtil.getBundle(this.getClass()).getVersion().toString());
481         request.headers().add("Accept", "*/*");
482         ByteBuf bbuf = Unpooled.copiedBuffer(content, StandardCharsets.UTF_8);
483         request.headers().set("Content-Length", bbuf.readableBytes());
484         request.content().clear().writeBytes(bbuf);
485         postRequestWithBody = request; // use Global so the authhandler can use it when resent with DIGEST.
486         sendHttpRequest("POST", httpPostURL, null);
487     }
488
489     public void sendHttpPOST(String httpRequestURL) {
490         sendHttpRequest("POST", httpRequestURL, null);
491     }
492
493     public void sendHttpGET(String httpRequestURL) {
494         sendHttpRequest("GET", httpRequestURL, null);
495     }
496
497     public int getPortFromShortenedUrl(String httpRequestURL) {
498         if (httpRequestURL.startsWith(":")) {
499             int end = httpRequestURL.indexOf("/");
500             return Integer.parseInt(httpRequestURL.substring(1, end));
501         }
502         return cameraConfig.getPort();
503     }
504
505     public String getTinyUrl(String httpRequestURL) {
506         if (httpRequestURL.startsWith(":")) {
507             int beginIndex = httpRequestURL.indexOf("/");
508             return httpRequestURL.substring(beginIndex);
509         }
510         return httpRequestURL;
511     }
512
513     private void checkCameraConnection() {
514         if (snapshotPolling) {// Currently polling a real URL for snapshots, so camera must be online.
515             return;
516         } else if (ffmpegSnapshotGeneration) {// Use RTSP stream creating snapshots to know camera is online.
517             Ffmpeg localSnapshot = ffmpegSnapshot;
518             if (localSnapshot != null && !localSnapshot.getIsAlive()) {
519                 cameraCommunicationError("FFmpeg Snapshots Stopped: Check your camera can be reached.");
520                 return;
521             }
522             return;// ffmpeg snapshot stream is still alive
523         }
524
525         // if ONVIF cam also use connection state which is updated by regular messages to camera
526         if (thing.getThingTypeUID().getId().equals(ONVIF_THING) && snapshotUri.isEmpty() && onvifCamera.isConnected()) {
527             return;
528         }
529
530         // Open a HTTP connection without sending any requests as we do not need a snapshot.
531         Bootstrap localBootstrap = mainBootstrap;
532         if (localBootstrap != null) {
533             ChannelFuture chFuture = localBootstrap
534                     .connect(new InetSocketAddress(cameraConfig.getIp(), cameraConfig.getPort()));
535             if (chFuture.awaitUninterruptibly(500)) {
536                 chFuture.channel().close();
537                 return;
538             }
539         }
540         cameraCommunicationError(
541                 "Connection Timeout: Check your IP and PORT are correct and the camera can be reached.");
542     }
543
544     // Always use this as sendHttpGET(GET/POST/PUT/DELETE, "/foo/bar",null)//
545     // The authHandler will generate a digest string and re-send using this same function when needed.
546     @SuppressWarnings("null")
547     public void sendHttpRequest(String httpMethod, String httpRequestURLFull, @Nullable String digestString) {
548         int port = getPortFromShortenedUrl(httpRequestURLFull);
549         String httpRequestURL = getTinyUrl(httpRequestURLFull);
550
551         if (mainBootstrap == null) {
552             mainBootstrap = new Bootstrap();
553             mainBootstrap.group(mainEventLoopGroup);
554             mainBootstrap.channel(NioSocketChannel.class);
555             mainBootstrap.option(ChannelOption.SO_KEEPALIVE, true);
556             mainBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 4500);
557             mainBootstrap.option(ChannelOption.SO_SNDBUF, 1024 * 8);
558             mainBootstrap.option(ChannelOption.SO_RCVBUF, 1024 * 1024);
559             mainBootstrap.option(ChannelOption.TCP_NODELAY, true);
560             mainBootstrap.handler(new ChannelInitializer<SocketChannel>() {
561
562                 @Override
563                 public void initChannel(SocketChannel socketChannel) throws Exception {
564                     // HIK Alarm stream needs > 9sec idle to stop stream closing
565                     socketChannel.pipeline().addLast(new IdleStateHandler(18, 0, 0));
566                     socketChannel.pipeline().addLast(new HttpClientCodec());
567                     socketChannel.pipeline().addLast(AUTH_HANDLER,
568                             new MyNettyAuthHandler(cameraConfig.getUser(), cameraConfig.getPassword(), getHandle()));
569                     socketChannel.pipeline().addLast(COMMON_HANDLER, new CommonCameraHandler());
570
571                     switch (thing.getThingTypeUID().getId()) {
572                         case AMCREST_THING:
573                             socketChannel.pipeline().addLast(AMCREST_HANDLER, new AmcrestHandler(getHandle()));
574                             break;
575                         case DAHUA_THING:
576                             socketChannel.pipeline()
577                                     .addLast(new DahuaHandler(getHandle(), cameraConfig.getNvrChannel()));
578                             break;
579                         case DOORBIRD_THING:
580                             socketChannel.pipeline().addLast(new DoorBirdHandler(getHandle()));
581                             break;
582                         case FOSCAM_THING:
583                             socketChannel.pipeline().addLast(
584                                     new FoscamHandler(getHandle(), cameraConfig.getUser(), cameraConfig.getPassword()));
585                             break;
586                         case HIKVISION_THING:
587                             socketChannel.pipeline()
588                                     .addLast(new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel()));
589                             break;
590                         case INSTAR_THING:
591                             socketChannel.pipeline().addLast(INSTAR_HANDLER, new InstarHandler(getHandle()));
592                             break;
593                         case REOLINK_THING:
594                             socketChannel.pipeline().addLast(REOLINK_HANDLER, new ReolinkHandler(getHandle()));
595                             break;
596                         default:
597                             socketChannel.pipeline().addLast(new HttpOnlyHandler(getHandle()));
598                             break;
599                     }
600                 }
601             });
602         }
603
604         FullHttpRequest request;
605         if ("GET".equals(httpMethod) || (useDigestAuth && digestString == null)) {
606             request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod(httpMethod), httpRequestURL);
607             request.headers().set("Host", cameraConfig.getIp() + ":" + port);
608             request.headers().set("Connection", HttpHeaderValues.CLOSE);
609         } else if ("PUT".equals(httpMethod)) {
610             request = putRequestWithBody;
611         } else {
612             request = postRequestWithBody;
613         }
614
615         if (!basicAuth.isEmpty()) {
616             if (useDigestAuth) {
617                 logger.warn("Camera at IP:{} had both Basic and Digest set to be used", cameraConfig.getIp());
618                 setBasicAuth(false);
619             } else {
620                 request.headers().set("Authorization", "Basic " + basicAuth);
621             }
622         }
623
624         if (useDigestAuth) {
625             if (digestString != null) {
626                 request.headers().set("Authorization", "Digest " + digestString);
627             }
628         }
629
630         mainBootstrap.connect(new InetSocketAddress(cameraConfig.getIp(), port))
631                 .addListener(new ChannelFutureListener() {
632
633                     @Override
634                     public void operationComplete(@Nullable ChannelFuture future) {
635                         if (future == null) {
636                             return;
637                         }
638                         if (future.isDone() && future.isSuccess()) {
639                             Channel ch = future.channel();
640                             openChannels.add(ch);
641                             if (!isOnline) {
642                                 bringCameraOnline();
643                             }
644                             logger.trace("Sending camera: {}: http://{}:{}{}", httpMethod, cameraConfig.getIp(), port,
645                                     httpRequestURL);
646
647                             openChannel(ch, httpRequestURL);
648                             CommonCameraHandler commonHandler = (CommonCameraHandler) ch.pipeline().get(COMMON_HANDLER);
649                             commonHandler.setURL(httpRequestURLFull);
650                             MyNettyAuthHandler authHandler = (MyNettyAuthHandler) ch.pipeline().get(AUTH_HANDLER);
651                             authHandler.setURL(httpMethod, httpRequestURL);
652
653                             switch (thing.getThingTypeUID().getId()) {
654                                 case AMCREST_THING:
655                                     AmcrestHandler amcrestHandler = (AmcrestHandler) ch.pipeline().get(AMCREST_HANDLER);
656                                     amcrestHandler.setURL(httpRequestURL);
657                                     break;
658                                 case INSTAR_THING:
659                                     InstarHandler instarHandler = (InstarHandler) ch.pipeline().get(INSTAR_HANDLER);
660                                     instarHandler.setURL(httpRequestURL);
661                                     break;
662                                 case REOLINK_THING:
663                                     ReolinkHandler reolinkHandler = (ReolinkHandler) ch.pipeline().get(REOLINK_HANDLER);
664                                     reolinkHandler.setURL(httpRequestURL);
665                                     break;
666                             }
667                             ch.writeAndFlush(request);
668                         } else { // an error occurred
669                             cameraCommunicationError(
670                                     "Connection Timeout: Check your IP and PORT are correct and the camera can be reached.");
671                         }
672                     }
673                 });
674     }
675
676     public void processSnapshot(byte[] incommingSnapshot) {
677         lockCurrentSnapshot.lock();
678         try {
679             currentSnapshot = incommingSnapshot;
680             if (cameraConfig.getGifPreroll() > 0) {
681                 fifoSnapshotBuffer.add(incommingSnapshot);
682                 if (fifoSnapshotBuffer.size() > (cameraConfig.getGifPreroll() + gifRecordTime)) {
683                     fifoSnapshotBuffer.removeFirst();
684                 }
685             }
686         } finally {
687             lockCurrentSnapshot.unlock();
688             currentSnapshotTime = Instant.now();
689         }
690
691         if (updateImageChannel) {
692             updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
693         } else if (firstMotionAlarm || motionAlarmUpdateSnapshot) {
694             updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
695             firstMotionAlarm = motionAlarmUpdateSnapshot = false;
696         } else if (firstAudioAlarm || audioAlarmUpdateSnapshot) {
697             updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
698             firstAudioAlarm = audioAlarmUpdateSnapshot = false;
699         }
700     }
701
702     public void startStreamServer() {
703         servlet = new CameraServlet(this, httpService);
704         updateState(CHANNEL_HLS_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
705                 + getThing().getUID().getId() + "/ipcamera.m3u8"));
706         updateState(CHANNEL_IMAGE_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
707                 + getThing().getUID().getId() + "/ipcamera.jpg"));
708         updateState(CHANNEL_MJPEG_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
709                 + getThing().getUID().getId() + "/ipcamera.mjpeg"));
710     }
711
712     public void openCamerasStream() {
713         if (mjpegUri.isEmpty() || "ffmpeg".equals(mjpegUri)) {
714             setupFfmpegFormat(FFmpegFormat.MJPEG);
715             return;
716         }
717         closeChannel(getTinyUrl(mjpegUri));
718         // Dahua cameras crash if you refresh (close and open) the stream without this delay.
719         mainEventLoopGroup.schedule(this::openMjpegStream, 300, TimeUnit.MILLISECONDS);
720     }
721
722     private void openMjpegStream() {
723         sendHttpGET(mjpegUri);
724     }
725
726     private void openChannel(Channel channel, String httpRequestURL) {
727         ChannelTracking tracker = channelTrackingMap.get(httpRequestURL);
728         if (tracker != null && !tracker.getReply().isEmpty()) {// We need to keep the stored reply
729             tracker.setChannel(channel);
730             return;
731         }
732         channelTrackingMap.put(httpRequestURL, new ChannelTracking(channel, httpRequestURL));
733     }
734
735     public void closeChannel(String url) {
736         ChannelTracking channelTracking = channelTrackingMap.get(url);
737         if (channelTracking != null) {
738             if (channelTracking.getChannel().isOpen()) {
739                 channelTracking.getChannel().close();
740                 return;
741             }
742         }
743     }
744
745     /**
746      * This method should never run under normal use, if there is a bug in a camera or binding it may be possible to
747      * open large amounts of channels. This may help to keep it under control and WARN the user every 8 seconds this is
748      * still occurring.
749      */
750     private void cleanChannels() {
751         for (Channel channel : openChannels) {
752             boolean oldChannel = true;
753             for (ChannelTracking channelTracking : channelTrackingMap.values()) {
754                 if (!channelTracking.getChannel().isOpen() && channelTracking.getReply().isEmpty()) {
755                     channelTrackingMap.remove(channelTracking.getRequestUrl());
756                 }
757                 if (channelTracking.getChannel().equals(channel)) {
758                     logger.debug("Open channel to camera is used for URL:{}", channelTracking.getRequestUrl());
759                     oldChannel = false;
760                 }
761             }
762             if (oldChannel) {
763                 channel.close();
764             }
765         }
766     }
767
768     public void storeHttpReply(String url, String content) {
769         ChannelTracking channelTracking = channelTrackingMap.get(url);
770         if (channelTracking != null) {
771             channelTracking.setReply(content);
772         }
773     }
774
775     private void storeSnapshots() {
776         int count = 0;
777         // Need to lock as fifoSnapshotBuffer is not thread safe and new snapshots can be incoming.
778         lockCurrentSnapshot.lock();
779         try {
780             for (byte[] foo : fifoSnapshotBuffer) {
781                 File file = new File(cameraConfig.getFfmpegOutput() + "snapshot" + count + ".jpg");
782                 count++;
783                 try {
784                     OutputStream fos = new FileOutputStream(file);
785                     fos.write(foo);
786                     fos.close();
787                 } catch (FileNotFoundException e) {
788                     logger.warn("FileNotFoundException {}", e.getMessage());
789                 } catch (IOException e) {
790                     logger.warn("IOException {}", e.getMessage());
791                 }
792             }
793         } finally {
794             lockCurrentSnapshot.unlock();
795         }
796     }
797
798     public void setupFfmpegFormat(FFmpegFormat format) {
799         String inputOptions = cameraConfig.getFfmpegInputOptions();
800         if (cameraConfig.getFfmpegOutput().isEmpty()) {
801             logger.warn("The camera tried to use a FFmpeg feature when the output folder is not set.");
802             return;
803         }
804         if (rtspUri.isEmpty()) {
805             logger.warn("The camera tried to use a FFmpeg feature when no valid input for FFmpeg is provided.");
806             return;
807         }
808         if (cameraConfig.getFfmpegLocation().isEmpty()) {
809             logger.warn("The camera tried to use a FFmpeg feature when the location for FFmpeg is not known.");
810             return;
811         }
812         if (rtspUri.toLowerCase().contains("rtsp")) {
813             if (inputOptions.isEmpty()) {
814                 inputOptions = "-rtsp_transport tcp";
815             }
816         }
817
818         // Make sure the folder exists, if not create it.
819         new File(cameraConfig.getFfmpegOutput()).mkdirs();
820         switch (format) {
821             case HLS:
822                 if (ffmpegHLS == null) {
823                     if (!inputOptions.isEmpty()) {
824                         ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
825                                 "-hide_banner -loglevel warning " + inputOptions, rtspUri,
826                                 cameraConfig.getHlsOutOptions(), cameraConfig.getFfmpegOutput() + "ipcamera.m3u8",
827                                 cameraConfig.getUser(), cameraConfig.getPassword());
828                     } else {
829                         ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
830                                 "-hide_banner -loglevel warning", rtspUri, cameraConfig.getHlsOutOptions(),
831                                 cameraConfig.getFfmpegOutput() + "ipcamera.m3u8", cameraConfig.getUser(),
832                                 cameraConfig.getPassword());
833                     }
834                 }
835                 Ffmpeg localHLS = ffmpegHLS;
836                 if (localHLS != null) {
837                     localHLS.startConverting();
838                 }
839                 break;
840             case GIF:
841                 if (cameraConfig.getGifPreroll() > 0) {
842                     ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
843                             "-y -r 1 -hide_banner -loglevel warning", cameraConfig.getFfmpegOutput() + "snapshot%d.jpg",
844                             "-frames:v " + (cameraConfig.getGifPreroll() + gifRecordTime) + " "
845                                     + cameraConfig.getGifOutOptions(),
846                             cameraConfig.getFfmpegOutput() + gifFilename + ".gif", cameraConfig.getUser(),
847                             cameraConfig.getPassword());
848                 } else {
849                     if (!inputOptions.isEmpty()) {
850                         inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning " + inputOptions;
851                     } else {
852                         inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning";
853                     }
854                     ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
855                             cameraConfig.getGifOutOptions(), cameraConfig.getFfmpegOutput() + gifFilename + ".gif",
856                             cameraConfig.getUser(), cameraConfig.getPassword());
857                 }
858                 if (cameraConfig.getGifPreroll() > 0) {
859                     storeSnapshots();
860                 }
861                 Ffmpeg localGIF = ffmpegGIF;
862                 if (localGIF != null) {
863                     localGIF.startConverting();
864                     if (gifHistory.isEmpty()) {
865                         gifHistory = gifFilename;
866                     } else if (!"ipcamera".equals(gifFilename)) {
867                         gifHistory = gifFilename + "," + gifHistory;
868                         if (gifHistoryLength > 49) {
869                             int endIndex = gifHistory.lastIndexOf(",");
870                             gifHistory = gifHistory.substring(0, endIndex);
871                         }
872                     }
873                     setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
874                 }
875                 break;
876             case RECORD:
877                 if (!inputOptions.isEmpty()) {
878                     inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning " + inputOptions;
879                 } else {
880                     inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning";
881                 }
882                 ffmpegRecord = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
883                         cameraConfig.getMp4OutOptions(), cameraConfig.getFfmpegOutput() + mp4Filename + ".mp4",
884                         cameraConfig.getUser(), cameraConfig.getPassword());
885                 Ffmpeg localRecord = ffmpegRecord;
886                 if (localRecord != null) {
887                     localRecord.startConverting();
888                     if (mp4History.isEmpty()) {
889                         mp4History = mp4Filename;
890                     } else if (!"ipcamera".equals(mp4Filename)) {
891                         mp4History = mp4Filename + "," + mp4History;
892                         if (mp4HistoryLength > 49) {
893                             int endIndex = mp4History.lastIndexOf(",");
894                             mp4History = mp4History.substring(0, endIndex);
895                         }
896                     }
897                 }
898                 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
899                 break;
900             case RTSP_ALARMS:
901                 Ffmpeg localAlarms = ffmpegRtspHelper;
902                 if (localAlarms != null) {
903                     localAlarms.stopConverting();
904                     if (!ffmpegAudioAlarmEnabled && !ffmpegMotionAlarmEnabled) {
905                         return;
906                     }
907                 }
908                 String input = (cameraConfig.getAlarmInputUrl().isEmpty()) ? rtspUri : cameraConfig.getAlarmInputUrl();
909                 String filterOptions = "";
910                 if (!ffmpegAudioAlarmEnabled) {
911                     filterOptions = "-an";
912                 } else {
913                     filterOptions = "-af silencedetect=n=-" + audioThreshold + "dB:d=2";
914                 }
915                 if (!ffmpegMotionAlarmEnabled && !ffmpegSnapshotGeneration) {
916                     filterOptions = filterOptions.concat(" -vn");
917                 } else if (ffmpegMotionAlarmEnabled && !cameraConfig.getMotionOptions().isEmpty()) {
918                     String usersMotionOptions = cameraConfig.getMotionOptions();
919                     if (usersMotionOptions.startsWith("-")) {
920                         // Need to put the users custom options first in the chain before the motion is detected
921                         filterOptions += " " + usersMotionOptions + ",select='gte(scene,"
922                                 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print";
923                     } else {
924                         filterOptions = filterOptions + " " + usersMotionOptions + " -vf select='gte(scene,"
925                                 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print";
926                     }
927                 } else if (ffmpegMotionAlarmEnabled) {
928                     filterOptions = filterOptions.concat(" -vf select='gte(scene,"
929                             + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print");
930                 }
931                 ffmpegRtspHelper = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, input,
932                         filterOptions, "-f null -", cameraConfig.getUser(), cameraConfig.getPassword());
933                 localAlarms = ffmpegRtspHelper;
934                 if (localAlarms != null) {
935                     localAlarms.startConverting();
936                 }
937                 break;
938             case MJPEG:
939                 if (ffmpegMjpeg == null) {
940                     if (inputOptions.isEmpty()) {
941                         inputOptions = "-hide_banner";
942                     } else {
943                         inputOptions += " -hide_banner";
944                     }
945                     ffmpegMjpeg = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
946                             cameraConfig.getMjpegOptions(), "http://127.0.0.1:" + SERVLET_PORT + "/ipcamera/"
947                                     + getThing().getUID().getId() + "/ipcamera.jpg",
948                             cameraConfig.getUser(), cameraConfig.getPassword());
949                 }
950                 Ffmpeg localMjpeg = ffmpegMjpeg;
951                 if (localMjpeg != null) {
952                     localMjpeg.startConverting();
953                 }
954                 break;
955             case SNAPSHOT:
956                 // if mjpeg stream you can use 'ffmpeg -i input -codec:v copy -bsf:v mjpeg2jpeg output.jpg'
957                 if (ffmpegSnapshot == null) {
958                     if (inputOptions.isEmpty()) {
959                         // iFrames only
960                         inputOptions = "-threads 1 -skip_frame nokey -hide_banner";
961                     } else {
962                         inputOptions += " -threads 1 -skip_frame nokey -hide_banner";
963                     }
964                     ffmpegSnapshot = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
965                             cameraConfig.getSnapshotOptions(), "http://127.0.0.1:" + SERVLET_PORT + "/ipcamera/"
966                                     + getThing().getUID().getId() + "/snapshot.jpg",
967                             cameraConfig.getUser(), cameraConfig.getPassword());
968                 }
969                 Ffmpeg localSnaps = ffmpegSnapshot;
970                 if (localSnaps != null) {
971                     localSnaps.startConverting();
972                 }
973                 break;
974         }
975     }
976
977     public void noMotionDetected(String thisAlarmsChannel) {
978         setChannelState(thisAlarmsChannel, OnOffType.OFF);
979         firstMotionAlarm = false;
980         motionAlarmUpdateSnapshot = false;
981         motionDetected = false;
982         if (streamingAutoFps) {
983             stopSnapshotPolling();
984         } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
985             stopSnapshotPolling();
986         }
987     }
988
989     /**
990      * The {@link changeAlarmState} To only be used to change alarms channels that are not counted as motion. This will
991      * allow logic to be added here in the future. Example more than 1 type of alarm may indicate that someone is
992      * tampering with the camera.
993      */
994     public void changeAlarmState(String thisAlarmsChannel, OnOffType state) {
995         updateState(thisAlarmsChannel, state);
996     }
997
998     public void motionDetected(String thisAlarmsChannel) {
999         updateState(CHANNEL_LAST_MOTION_TYPE, new StringType(thisAlarmsChannel));
1000         updateState(thisAlarmsChannel, OnOffType.ON);
1001         motionDetected = true;
1002         if (streamingAutoFps) {
1003             startSnapshotPolling();
1004         }
1005         if (cameraConfig.getUpdateImageWhen().contains("2")) {
1006             if (!firstMotionAlarm) {
1007                 if (!snapshotUri.isEmpty()) {
1008                     updateSnapshot();
1009                 }
1010                 firstMotionAlarm = true;// reset back to false when the jpg arrives.
1011             }
1012         } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1013             if (!snapshotPolling) {
1014                 startSnapshotPolling();
1015             }
1016             firstMotionAlarm = true;
1017             motionAlarmUpdateSnapshot = true;
1018         }
1019     }
1020
1021     public void audioDetected() {
1022         updateState(CHANNEL_AUDIO_ALARM, OnOffType.ON);
1023         if (cameraConfig.getUpdateImageWhen().contains("3")) {
1024             if (!firstAudioAlarm) {
1025                 if (!snapshotUri.isEmpty()) {
1026                     updateSnapshot();
1027                 }
1028                 firstAudioAlarm = true;// reset back to false when the jpg arrives.
1029             }
1030         } else if (cameraConfig.getUpdateImageWhen().contains("5")) {// During audio alarms
1031             firstAudioAlarm = true;
1032             audioAlarmUpdateSnapshot = true;
1033         }
1034     }
1035
1036     public void noAudioDetected() {
1037         setChannelState(CHANNEL_AUDIO_ALARM, OnOffType.OFF);
1038         firstAudioAlarm = false;
1039         audioAlarmUpdateSnapshot = false;
1040     }
1041
1042     public void recordMp4(String filename, int seconds) {
1043         mp4Filename = filename;
1044         mp4RecordTime = seconds;
1045         setupFfmpegFormat(FFmpegFormat.RECORD);
1046         setChannelState(CHANNEL_RECORDING_MP4, DecimalType.valueOf(new String("" + seconds)));
1047     }
1048
1049     public void recordGif(String filename, int seconds) {
1050         gifFilename = filename;
1051         gifRecordTime = seconds;
1052         if (cameraConfig.getGifPreroll() > 0) {
1053             snapCount = seconds;
1054         } else {
1055             setupFfmpegFormat(FFmpegFormat.GIF);
1056         }
1057         setChannelState(CHANNEL_RECORDING_GIF, DecimalType.valueOf(new String("" + seconds)));
1058     }
1059
1060     private void getReolinkToken() {
1061         sendHttpPOST("/api.cgi?cmd=Login",
1062                 "[{\"cmd\":\"Login\", \"param\":{ \"User\":{ \"Version\": \"0\", \"userName\":\""
1063                         + cameraConfig.getUser() + "\", \"password\":\"" + cameraConfig.getPassword() + "\"}}}]");
1064     }
1065
1066     public String returnValueFromString(String rawString, String searchedString) {
1067         String result = "";
1068         int index = rawString.indexOf(searchedString);
1069         if (index != -1) // -1 means "not found"
1070         {
1071             result = rawString.substring(index + searchedString.length(), rawString.length());
1072             index = result.indexOf("\r\n"); // find a carriage return to find the end of the value.
1073             if (index == -1) {
1074                 return result; // Did not find a carriage return.
1075             } else {
1076                 return result.substring(0, index);
1077             }
1078         }
1079         return ""; // Did not find the String we were searching for
1080     }
1081
1082     private void sendPTZRequest() {
1083         onvifCamera.sendPTZRequest(OnvifConnection.RequestType.AbsoluteMove);
1084     }
1085
1086     @Override
1087     public void channelLinked(ChannelUID channelUID) {
1088         switch (channelUID.getId()) {
1089             case CHANNEL_MJPEG_URL:
1090                 updateState(CHANNEL_MJPEG_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1091                         + getThing().getUID().getId() + "/ipcamera.mjpeg"));
1092                 break;
1093             case CHANNEL_HLS_URL:
1094                 updateState(CHANNEL_HLS_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1095                         + getThing().getUID().getId() + "/ipcamera.m3u8"));
1096                 break;
1097             case CHANNEL_IMAGE_URL:
1098                 updateState(CHANNEL_IMAGE_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1099                         + getThing().getUID().getId() + "/ipcamera.jpg"));
1100                 break;
1101         }
1102     }
1103
1104     public void removeChannels(List<org.openhab.core.thing.Channel> removeChannels) {
1105         if (!removeChannels.isEmpty()) {
1106             ThingBuilder thingBuilder = editThing();
1107             thingBuilder.withoutChannels(removeChannels);
1108             updateThing(thingBuilder.build());
1109         }
1110     }
1111
1112     @Override
1113     public void handleCommand(ChannelUID channelUID, Command command) {
1114         if (command instanceof RefreshType) {
1115             switch (channelUID.getId()) {
1116                 case CHANNEL_PAN:
1117                     if (onvifCamera.supportsPTZ()) {
1118                         updateState(CHANNEL_PAN, new PercentType(Math.round(onvifCamera.getAbsolutePan())));
1119                     }
1120                     return;
1121                 case CHANNEL_TILT:
1122                     if (onvifCamera.supportsPTZ()) {
1123                         updateState(CHANNEL_TILT, new PercentType(Math.round(onvifCamera.getAbsoluteTilt())));
1124                     }
1125                     return;
1126                 case CHANNEL_ZOOM:
1127                     if (onvifCamera.supportsPTZ()) {
1128                         updateState(CHANNEL_ZOOM, new PercentType(Math.round(onvifCamera.getAbsoluteZoom())));
1129                     }
1130                     return;
1131                 case CHANNEL_GOTO_PRESET:
1132                     if (onvifCamera.supportsPTZ()) {
1133                         onvifCamera.sendPTZRequest(OnvifConnection.RequestType.GetPresets);
1134                     }
1135                     return;
1136             }
1137         } // caution "REFRESH" can still progress to brand Handlers below the else.
1138         else {
1139             switch (channelUID.getId()) {
1140                 case CHANNEL_MP4_HISTORY_LENGTH:
1141                     if (DecimalType.ZERO.equals(command)) {
1142                         mp4HistoryLength = 0;
1143                         mp4History = "";
1144                         setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
1145                     }
1146                     return;
1147                 case CHANNEL_GIF_HISTORY_LENGTH:
1148                     if (DecimalType.ZERO.equals(command)) {
1149                         gifHistoryLength = 0;
1150                         gifHistory = "";
1151                         setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
1152                     }
1153                     return;
1154                 case CHANNEL_FFMPEG_MOTION_CONTROL:
1155                     if (OnOffType.ON.equals(command)) {
1156                         ffmpegMotionAlarmEnabled = true;
1157                     } else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
1158                         ffmpegMotionAlarmEnabled = false;
1159                         noMotionDetected(CHANNEL_FFMPEG_MOTION_ALARM);
1160                     } else if (command instanceof PercentType percentCommand) {
1161                         ffmpegMotionAlarmEnabled = true;
1162                         motionThreshold = percentCommand.toBigDecimal();
1163                     }
1164                     setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
1165                     return;
1166                 case CHANNEL_START_STREAM:
1167                     Ffmpeg localHLS;
1168                     if (OnOffType.ON.equals(command)) {
1169                         localHLS = ffmpegHLS;
1170                         if (localHLS == null) {
1171                             setupFfmpegFormat(FFmpegFormat.HLS);
1172                             localHLS = ffmpegHLS;
1173                         }
1174                         if (localHLS != null) {
1175                             localHLS.setKeepAlive(-1);// Now will run till manually stopped.
1176                             localHLS.startConverting();
1177                         }
1178                     } else {
1179                         localHLS = ffmpegHLS;
1180                         if (localHLS != null) {
1181                             // Still runs but will be able to auto stop when the HLS stream is no longer used.
1182                             localHLS.setKeepAlive(1);
1183                         }
1184                     }
1185                     return;
1186                 case CHANNEL_EXTERNAL_MOTION:
1187                     if (OnOffType.ON.equals(command)) {
1188                         motionDetected(CHANNEL_EXTERNAL_MOTION);
1189                     } else {
1190                         noMotionDetected(CHANNEL_EXTERNAL_MOTION);
1191                     }
1192                     return;
1193                 case CHANNEL_GOTO_PRESET:
1194                     if (onvifCamera.supportsPTZ()) {
1195                         onvifCamera.gotoPreset(Integer.valueOf(command.toString()));
1196                     }
1197                     return;
1198                 case CHANNEL_POLL_IMAGE:
1199                     if (OnOffType.ON.equals(command)) {
1200                         if (snapshotUri.isEmpty()) {
1201                             ffmpegSnapshotGeneration = true;
1202                             setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1203                             updateImageChannel = false;
1204                         } else {
1205                             updateImageChannel = true;
1206                             updateSnapshot();// Allows this to change Image FPS on demand
1207                         }
1208                     } else {
1209                         Ffmpeg localSnaps = ffmpegSnapshot;
1210                         if (localSnaps != null) {
1211                             localSnaps.stopConverting();
1212                             ffmpegSnapshotGeneration = false;
1213                         }
1214                         updateImageChannel = false;
1215                     }
1216                     return;
1217                 case CHANNEL_PAN:
1218                     if (onvifCamera.supportsPTZ()) {
1219                         if (command instanceof IncreaseDecreaseType) {
1220                             if (command == IncreaseDecreaseType.INCREASE) {
1221                                 if (cameraConfig.getPtzContinuous()) {
1222                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveLeft);
1223                                 } else {
1224                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveLeft);
1225                                 }
1226                             } else {
1227                                 if (cameraConfig.getPtzContinuous()) {
1228                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveRight);
1229                                 } else {
1230                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveRight);
1231                                 }
1232                             }
1233                             return;
1234                         } else if (OnOffType.OFF.equals(command)) {
1235                             onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1236                             return;
1237                         }
1238                         onvifCamera.setAbsolutePan(Float.valueOf(command.toString()));
1239                         mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1240                     }
1241                     return;
1242                 case CHANNEL_TILT:
1243                     if (onvifCamera.supportsPTZ()) {
1244                         if (command instanceof IncreaseDecreaseType) {
1245                             if (IncreaseDecreaseType.INCREASE.equals(command)) {
1246                                 if (cameraConfig.getPtzContinuous()) {
1247                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveUp);
1248                                 } else {
1249                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveUp);
1250                                 }
1251                             } else {
1252                                 if (cameraConfig.getPtzContinuous()) {
1253                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveDown);
1254                                 } else {
1255                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveDown);
1256                                 }
1257                             }
1258                             return;
1259                         } else if (OnOffType.OFF.equals(command)) {
1260                             onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1261                             return;
1262                         }
1263                         onvifCamera.setAbsoluteTilt(Float.valueOf(command.toString()));
1264                         mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1265                     }
1266                     return;
1267                 case CHANNEL_ZOOM:
1268                     if (onvifCamera.supportsPTZ()) {
1269                         if (command instanceof IncreaseDecreaseType) {
1270                             if (IncreaseDecreaseType.INCREASE.equals(command)) {
1271                                 if (cameraConfig.getPtzContinuous()) {
1272                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveIn);
1273                                 } else {
1274                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveIn);
1275                                 }
1276                             } else {
1277                                 if (cameraConfig.getPtzContinuous()) {
1278                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveOut);
1279                                 } else {
1280                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveOut);
1281                                 }
1282                             }
1283                             return;
1284                         } else if (OnOffType.OFF.equals(command)) {
1285                             onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1286                             return;
1287                         }
1288                         onvifCamera.setAbsoluteZoom(Float.valueOf(command.toString()));
1289                         mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1290                     }
1291                     return;
1292             }
1293         }
1294         // commands and refresh now get passed to brand handlers
1295         switch (thing.getThingTypeUID().getId()) {
1296             case AMCREST_THING:
1297                 AmcrestHandler amcrestHandler = new AmcrestHandler(getHandle());
1298                 amcrestHandler.handleCommand(channelUID, command);
1299                 if (lowPriorityRequests.isEmpty()) {
1300                     lowPriorityRequests = amcrestHandler.getLowPriorityRequests();
1301                 }
1302                 break;
1303             case DAHUA_THING:
1304                 DahuaHandler dahuaHandler = new DahuaHandler(getHandle(), cameraConfig.getNvrChannel());
1305                 dahuaHandler.handleCommand(channelUID, command);
1306                 if (lowPriorityRequests.isEmpty()) {
1307                     lowPriorityRequests = dahuaHandler.getLowPriorityRequests();
1308                 }
1309                 break;
1310             case DOORBIRD_THING:
1311                 DoorBirdHandler doorBirdHandler = new DoorBirdHandler(getHandle());
1312                 doorBirdHandler.handleCommand(channelUID, command);
1313                 if (lowPriorityRequests.isEmpty()) {
1314                     lowPriorityRequests = doorBirdHandler.getLowPriorityRequests();
1315                 }
1316                 break;
1317             case HIKVISION_THING:
1318                 HikvisionHandler hikvisionHandler = new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel());
1319                 hikvisionHandler.handleCommand(channelUID, command);
1320                 break;
1321             case FOSCAM_THING:
1322                 FoscamHandler foscamHandler = new FoscamHandler(getHandle(), cameraConfig.getUser(),
1323                         cameraConfig.getPassword());
1324                 foscamHandler.handleCommand(channelUID, command);
1325                 if (lowPriorityRequests.isEmpty()) {
1326                     lowPriorityRequests = foscamHandler.getLowPriorityRequests();
1327                 }
1328                 break;
1329             case INSTAR_THING:
1330                 InstarHandler instarHandler = new InstarHandler(getHandle());
1331                 instarHandler.handleCommand(channelUID, command);
1332                 if (lowPriorityRequests.isEmpty()) {
1333                     lowPriorityRequests = instarHandler.getLowPriorityRequests();
1334                 }
1335                 break;
1336             case REOLINK_THING:
1337                 ReolinkHandler reolinkHandler = new ReolinkHandler(getHandle());
1338                 reolinkHandler.handleCommand(channelUID, command);
1339                 break;
1340             default:
1341                 HttpOnlyHandler defaultHandler = new HttpOnlyHandler(getHandle());
1342                 defaultHandler.handleCommand(channelUID, command);
1343                 if (lowPriorityRequests.isEmpty()) {
1344                     lowPriorityRequests = defaultHandler.getLowPriorityRequests();
1345                 }
1346                 break;
1347         }
1348     }
1349
1350     public void setChannelState(String channelToUpdate, State valueOf) {
1351         updateState(channelToUpdate, valueOf);
1352     }
1353
1354     private void bringCameraOnline() {
1355         isOnline = true;
1356         updateStatus(ThingStatus.ONLINE);
1357         groupTracker.listOfOnlineCameraHandlers.add(this);
1358         groupTracker.listOfOnlineCameraUID.add(getThing().getUID().getId());
1359         Future<?> localFuture = cameraConnectionJob;
1360         if (localFuture != null) {
1361             localFuture.cancel(false);
1362             cameraConnectionJob = null;
1363         }
1364         if (!snapshotUri.isEmpty()) {
1365             if (cameraConfig.getGifPreroll() > 0 || cameraConfig.getUpdateImageWhen().contains("1")) {
1366                 snapshotPolling = true;
1367                 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 1000,
1368                         cameraConfig.getPollTime(), TimeUnit.MILLISECONDS);
1369             }
1370         }
1371
1372         pollCameraJob = threadPool.scheduleWithFixedDelay(this::pollCameraRunnable, 1000, 8000, TimeUnit.MILLISECONDS);
1373
1374         // auto restart mjpeg stream now camera is back online.
1375         CameraServlet localServlet = servlet;
1376         if (localServlet != null && !localServlet.openStreams.isEmpty()) {
1377             openCamerasStream();
1378         }
1379
1380         if (!rtspUri.isEmpty()) {
1381             updateState(CHANNEL_RTSP_URL, new StringType(rtspUri));
1382         }
1383         if (updateImageChannel) {
1384             updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1385         } else {
1386             updateState(CHANNEL_POLL_IMAGE, OnOffType.OFF);
1387         }
1388         if (!groupTracker.listOfGroupHandlers.isEmpty()) {
1389             for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1390                 handle.cameraOnline(getThing().getUID().getId());
1391             }
1392         }
1393     }
1394
1395     void snapshotIsFfmpeg() {
1396         snapshotUri = "";// ffmpeg is a valid option. Simplify further checks.
1397         logger.debug(
1398                 "Binding has no snapshot url. Will use your CPU and FFmpeg to create snapshots from the cameras RTSP.");
1399         bringCameraOnline();
1400         if (!rtspUri.isEmpty()) {
1401             updateImageChannel = false;
1402             ffmpegSnapshotGeneration = true;
1403             setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1404             updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1405         } else {
1406             cameraConfigError("Binding can not find a RTSP url for this camera, please provide a FFmpeg Input URL.");
1407         }
1408     }
1409
1410     void pollingCameraConnection() {
1411         keepMjpegRunning();
1412         if (thing.getThingTypeUID().getId().equals(GENERIC_THING)
1413                 || thing.getThingTypeUID().getId().equals(DOORBIRD_THING)) {
1414             if (rtspUri.isEmpty()) {
1415                 logger.warn("Binding has not been supplied with a FFmpeg Input URL, so some features will not work.");
1416             }
1417             if (snapshotUri.isEmpty() || "ffmpeg".equals(snapshotUri)) {
1418                 snapshotIsFfmpeg();
1419             } else {
1420                 ffmpegSnapshotGeneration = false;
1421                 updateSnapshot();
1422             }
1423             return;
1424         }
1425         if (cameraConfig.getOnvifPort() > 0 && !onvifCamera.isConnected()) {
1426             if (onvifCamera.isConnectError()) {
1427                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Camera is not reachable");
1428             } else if (onvifCamera.isRefusedError()) {
1429                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
1430                         "Camera refused connection on ONVIF ports.");
1431             }
1432             logger.debug("About to connect to the IP Camera using the ONVIF PORT at IP:{}:{}", cameraConfig.getIp(),
1433                     cameraConfig.getOnvifPort());
1434             onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1435             return;
1436         }
1437         if ("ffmpeg".equals(snapshotUri)) {
1438             snapshotIsFfmpeg();
1439         } else if (!snapshotUri.isEmpty()) {
1440             ffmpegSnapshotGeneration = false;
1441             updateSnapshot();
1442         } else if (!rtspUri.isEmpty()) {
1443             snapshotIsFfmpeg();
1444         } else {
1445             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
1446                     "Camera failed to report a valid Snaphot and/or RTSP URL. Check user/pass is correct, or use the advanced configs to manually provide a URL.");
1447         }
1448     }
1449
1450     public void cameraConfigError(String reason) {
1451         // wont try to reconnect again due to a config error being the cause.
1452         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, reason);
1453         dispose();
1454     }
1455
1456     public void cameraCommunicationError(String reason) {
1457         // will try to reconnect again as camera may be rebooting.
1458         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
1459         if (isOnline) {// if already offline dont try reconnecting in 6 seconds, we want 30sec wait.
1460             resetAndRetryConnecting();
1461         }
1462     }
1463
1464     private boolean streamIsStopped(String url) {
1465         ChannelTracking channelTracking = channelTrackingMap.get(url);
1466         if (channelTracking != null) {
1467             if (channelTracking.getChannel().isActive()) {
1468                 return false; // stream is running.
1469             }
1470         }
1471         return true; // Stream stopped or never started.
1472     }
1473
1474     void snapshotRunnable() {
1475         // Snapshot should be first to keep consistent time between shots
1476         updateSnapshot();
1477         if (snapCount > 0) {
1478             if (--snapCount == 0) {
1479                 setupFfmpegFormat(FFmpegFormat.GIF);
1480             }
1481         }
1482     }
1483
1484     private void takeSnapshot() {
1485         sendHttpGET(snapshotUri);
1486     }
1487
1488     private void updateSnapshot() {
1489         lastSnapshotRequest = Instant.now();
1490         mainEventLoopGroup.schedule(this::takeSnapshot, 0, TimeUnit.MILLISECONDS);
1491     }
1492
1493     public byte[] getSnapshot() {
1494         if (!isOnline) {
1495             // Single gray pixel JPG to keep streams open when the camera goes offline so they dont stop.
1496             return new byte[] { (byte) 0xff, (byte) 0xd8, (byte) 0xff, (byte) 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46,
1497                     0x00, 0x01, 0x01, 0x01, 0x00, 0x48, 0x00, 0x48, 0x00, 0x00, (byte) 0xff, (byte) 0xdb, 0x00, 0x43,
1498                     0x00, 0x03, 0x02, 0x02, 0x02, 0x02, 0x02, 0x03, 0x02, 0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x04,
1499                     0x06, 0x04, 0x04, 0x04, 0x04, 0x04, 0x08, 0x06, 0x06, 0x05, 0x06, 0x09, 0x08, 0x0a, 0x0a, 0x09,
1500                     0x08, 0x09, 0x09, 0x0a, 0x0c, 0x0f, 0x0c, 0x0a, 0x0b, 0x0e, 0x0b, 0x09, 0x09, 0x0d, 0x11, 0x0d,
1501                     0x0e, 0x0f, 0x10, 0x10, 0x11, 0x10, 0x0a, 0x0c, 0x12, 0x13, 0x12, 0x10, 0x13, 0x0f, 0x10, 0x10,
1502                     0x10, (byte) 0xff, (byte) 0xc9, 0x00, 0x0b, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00,
1503                     (byte) 0xff, (byte) 0xcc, 0x00, 0x06, 0x00, 0x10, 0x10, 0x05, (byte) 0xff, (byte) 0xda, 0x00, 0x08,
1504                     0x01, 0x01, 0x00, 0x00, 0x3f, 0x00, (byte) 0xd2, (byte) 0xcf, 0x20, (byte) 0xff, (byte) 0xd9 };
1505         }
1506         // Most cameras will return a 503 busy error if snapshot is faster than 1 second
1507         long lastUpdatedMs = Duration.between(lastSnapshotRequest, Instant.now()).toMillis();
1508         if (!snapshotPolling && !ffmpegSnapshotGeneration && lastUpdatedMs >= cameraConfig.getPollTime()) {
1509             updateSnapshot();
1510         }
1511         lockCurrentSnapshot.lock();
1512         try {
1513             return currentSnapshot;
1514         } finally {
1515             lockCurrentSnapshot.unlock();
1516         }
1517     }
1518
1519     public void stopSnapshotPolling() {
1520         Future<?> localFuture;
1521         if (!streamingSnapshotMjpeg && cameraConfig.getGifPreroll() == 0
1522                 && !cameraConfig.getUpdateImageWhen().contains("1")) {
1523             snapshotPolling = false;
1524             localFuture = snapshotJob;
1525             if (localFuture != null) {
1526                 localFuture.cancel(true);
1527             }
1528         } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // only during Motion Alarms
1529             snapshotPolling = false;
1530             localFuture = snapshotJob;
1531             if (localFuture != null) {
1532                 localFuture.cancel(true);
1533             }
1534         }
1535     }
1536
1537     public void startSnapshotPolling() {
1538         if (snapshotPolling || ffmpegSnapshotGeneration) {
1539             return; // Already polling or creating with FFmpeg from RTSP
1540         }
1541         if (streamingSnapshotMjpeg || streamingAutoFps || cameraConfig.getUpdateImageWhen().contains("4")) {
1542             snapshotPolling = true;
1543             snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 0, cameraConfig.getPollTime(),
1544                     TimeUnit.MILLISECONDS);
1545         }
1546     }
1547
1548     /**
1549      * {@link pollCameraRunnable} Polls every 8 seconds, to check camera is still ONLINE and keep alarm
1550      * streams open and more.
1551      *
1552      */
1553     void pollCameraRunnable() {
1554         // NOTE: Use lowPriorityRequests if get request is not needed every poll.
1555         if (!lowPriorityRequests.isEmpty()) {
1556             if (lowPriorityCounter >= lowPriorityRequests.size()) {
1557                 lowPriorityCounter = 0;
1558             }
1559             sendHttpGET(lowPriorityRequests.get(lowPriorityCounter++));
1560         }
1561         // what needs to be done every poll//
1562         switch (thing.getThingTypeUID().getId()) {
1563             case GENERIC_THING:
1564                 if (!snapshotPolling) {
1565                     checkCameraConnection();
1566                 }
1567                 break;
1568             case ONVIF_THING:
1569                 if (!snapshotPolling) {
1570                     checkCameraConnection();
1571                 }
1572                 break;
1573             case INSTAR_THING:
1574                 if (!snapshotPolling) {
1575                     checkCameraConnection();
1576                 }
1577                 noMotionDetected(CHANNEL_MOTION_ALARM);
1578                 noMotionDetected(CHANNEL_PIR_ALARM);
1579                 noMotionDetected(CHANNEL_HUMAN_ALARM);
1580                 noMotionDetected(CHANNEL_CAR_ALARM);
1581                 noMotionDetected(CHANNEL_ANIMAL_ALARM);
1582                 noAudioDetected();
1583                 break;
1584             case HIKVISION_THING:
1585                 if (streamIsStopped("/ISAPI/Event/notification/alertStream")) {
1586                     logger.info("The alarm stream was not running for camera {}, re-starting it now",
1587                             cameraConfig.getIp());
1588                     sendHttpGET("/ISAPI/Event/notification/alertStream");
1589                 }
1590                 break;
1591             case AMCREST_THING:
1592                 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=VideoMotion");
1593                 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=AudioMutation");
1594                 break;
1595             case REOLINK_THING:
1596                 if (cameraConfig.getNvrChannel() > 0) {
1597                     sendHttpGET("/api.cgi?cmd=GetAiState&channel=" + cameraConfig.getNvrChannel() + "&user="
1598                             + cameraConfig.getUser() + "&password=" + cameraConfig.getPassword());
1599                     sendHttpGET("/api.cgi?cmd=GetMdState&channel=" + cameraConfig.getNvrChannel() + "&user="
1600                             + cameraConfig.getUser() + "&password=" + cameraConfig.getPassword());
1601                 } else {
1602                     if (!snapshotPolling) {
1603                         checkCameraConnection();
1604                     }
1605                     if (!onvifCamera.isConnected()) {
1606                         onvifCamera.connect(true);
1607                     }
1608                 }
1609                 break;
1610             case DAHUA_THING:
1611                 if (!snapshotPolling) {
1612                     checkCameraConnection();
1613                 }
1614                 // Check for alarms, channel for NVRs appears not to work at filtering.
1615                 if (streamIsStopped("/cgi-bin/eventManager.cgi?action=attach&codes=[All]")) {
1616                     logger.info("The alarm stream was not running for camera {}, re-starting it now",
1617                             cameraConfig.getIp());
1618                     sendHttpGET("/cgi-bin/eventManager.cgi?action=attach&codes=[All]");
1619                 }
1620                 break;
1621             case DOORBIRD_THING:
1622                 if (!snapshotPolling) {
1623                     checkCameraConnection();
1624                 }
1625                 // Check for alarms, channel for NVRs appears not to work at filtering.
1626                 if (streamIsStopped("/bha-api/monitor.cgi?ring=doorbell,motionsensor")) {
1627                     logger.info("The alarm stream was not running for camera {}, re-starting it now",
1628                             cameraConfig.getIp());
1629                     sendHttpGET("/bha-api/monitor.cgi?ring=doorbell,motionsensor");
1630                 }
1631                 break;
1632             case FOSCAM_THING:
1633                 sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=getDevState&usr=" + cameraConfig.getUser() + "&pwd="
1634                         + cameraConfig.getPassword());
1635                 break;
1636         }
1637         Ffmpeg localFfmpeg = ffmpegHLS;
1638         if (localFfmpeg != null) {
1639             localFfmpeg.checkKeepAlive();
1640         }
1641         if (ffmpegMotionAlarmEnabled || ffmpegAudioAlarmEnabled) {
1642             localFfmpeg = ffmpegRtspHelper;
1643             if (localFfmpeg == null || !localFfmpeg.getIsAlive()) {
1644                 setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
1645             }
1646         }
1647         // check if the thread has frozen due to camera doing a soft reboot
1648         localFfmpeg = ffmpegMjpeg;
1649         if (localFfmpeg != null && !localFfmpeg.getIsAlive()) {
1650             logger.debug("MJPEG was not being produced by FFmpeg when it should have been, restarting FFmpeg.");
1651             setupFfmpegFormat(FFmpegFormat.MJPEG);
1652         }
1653         if (openChannels.size() > 10) {
1654             logger.debug("There are {} open Channels being tracked.", openChannels.size());
1655             cleanChannels();
1656         }
1657     }
1658
1659     @Override
1660     public void initialize() {
1661         cameraConfig = getConfigAs(CameraConfig.class);
1662         threadPool = Executors.newScheduledThreadPool(2);
1663         mainEventLoopGroup = new NioEventLoopGroup(3);
1664         snapshotUri = getCorrectUrlFormat(cameraConfig.getSnapshotUrl());
1665         mjpegUri = getCorrectUrlFormat(cameraConfig.getMjpegUrl());
1666         rtspUri = cameraConfig.getFfmpegInput();
1667         if (cameraConfig.getFfmpegOutput().isEmpty()) {
1668             cameraConfig
1669                     .setFfmpegOutput(OpenHAB.getUserDataFolder() + "/ipcamera/" + this.thing.getUID().getId() + "/");
1670         }
1671         // Known cameras will connect quicker if we skip ONVIF questions.
1672         switch (thing.getThingTypeUID().getId()) {
1673             case AMCREST_THING:
1674             case DAHUA_THING:
1675                 if (mjpegUri.isEmpty()) {
1676                     mjpegUri = "/cgi-bin/mjpg/video.cgi?channel=" + cameraConfig.getNvrChannel() + "&subtype=1";
1677                 }
1678                 if (snapshotUri.isEmpty()) {
1679                     snapshotUri = "/cgi-bin/snapshot.cgi?channel=" + cameraConfig.getNvrChannel();
1680                 }
1681                 break;
1682             case DOORBIRD_THING:
1683                 if (mjpegUri.isEmpty()) {
1684                     mjpegUri = "/bha-api/video.cgi";
1685                 }
1686                 if (snapshotUri.isEmpty()) {
1687                     snapshotUri = "/bha-api/image.cgi";
1688                 }
1689                 break;
1690             case FOSCAM_THING:
1691                 // Foscam needs any special char like spaces (%20) to be encoded for URLs.
1692                 cameraConfig.setUser(Helper.encodeSpecialChars(cameraConfig.getUser()));
1693                 cameraConfig.setPassword(Helper.encodeSpecialChars(cameraConfig.getPassword()));
1694                 if (mjpegUri.isEmpty()) {
1695                     mjpegUri = "/cgi-bin/CGIStream.cgi?cmd=GetMJStream&usr=" + cameraConfig.getUser() + "&pwd="
1696                             + cameraConfig.getPassword();
1697                 }
1698                 if (snapshotUri.isEmpty()) {
1699                     snapshotUri = "/cgi-bin/CGIProxy.fcgi?usr=" + cameraConfig.getUser() + "&pwd="
1700                             + cameraConfig.getPassword() + "&cmd=snapPicture2";
1701                 }
1702                 break;
1703             case HIKVISION_THING:// The 02 gives you the first sub stream which needs to be set to MJPEG
1704                 if (mjpegUri.isEmpty()) {
1705                     mjpegUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "02" + "/httppreview";
1706                 }
1707                 if (snapshotUri.isEmpty()) {
1708                     snapshotUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "01/picture";
1709                 }
1710                 if (lowPriorityRequests.isEmpty()) {
1711                     lowPriorityRequests.add("/ISAPI/System/IO/inputs/" + cameraConfig.getNvrChannel() + "/status");
1712                 }
1713                 break;
1714             case INSTAR_THING:
1715                 if (snapshotUri.isEmpty()) {
1716                     snapshotUri = "/tmpfs/snap.jpg";
1717                 }
1718                 if (mjpegUri.isEmpty()) {
1719                     mjpegUri = "/mjpegstream.cgi?-chn=12";
1720                 }
1721                 // Newer Instar cameras use this to setup the Alarm Server, plus it is used to work out which API is
1722                 // implemented based on the response to these two requests.
1723                 sendHttpGET(
1724                         "/param.cgi?cmd=setasaction&-server=1&enable=1&-interval=1&cmd=setasattr&-as_index=1&-as_server="
1725                                 + hostIp + "&-as_port=" + SERVLET_PORT + "&-as_path=/ipcamera/"
1726                                 + getThing().getUID().getId()
1727                                 + "/instar&-as_ssl=0&-as_insecure=0&-as_mode=0&-as_activequery=1&-as_auth=0&-as_query1=0&-as_query2=0&-as_query3=0&-as_query4=0&-as_query5=0");
1728                 // Older Instar cameras use this to setup the Alarm Server
1729                 sendHttpGET(
1730                         "/param.cgi?cmd=setmdalarm&-aname=server2&-switch=on&-interval=1&cmd=setalarmserverattr&-as_index=3&-as_server="
1731                                 + hostIp + "&-as_port=" + SERVLET_PORT + "&-as_path=/ipcamera/"
1732                                 + getThing().getUID().getId()
1733                                 + "/instar&-as_ssl=0&-as_mode=1&-as_activequery=1&-as_auth=0&-as_query1=0&-as_query2=0&-as_query3=0&-as_query4=0&-as_query5=0");
1734                 break;
1735             case REOLINK_THING:
1736                 if (cameraConfig.useToken) {
1737                     authenticationJob = threadPool.scheduleWithFixedDelay(this::getReolinkToken, 0, 45,
1738                             TimeUnit.MINUTES);
1739                 } else {
1740                     reolinkAuth = "&user=" + cameraConfig.getUser() + "&password=" + cameraConfig.getPassword();
1741                 }
1742                 if (snapshotUri.isEmpty()) {
1743                     if (cameraConfig.getNvrChannel() < 1) {
1744                         snapshotUri = "/cgi-bin/api.cgi?cmd=Snap&channel=0&rs=openHAB" + reolinkAuth;
1745                     } else {
1746                         snapshotUri = "/cgi-bin/api.cgi?cmd=Snap&channel=" + (cameraConfig.getNvrChannel() - 1)
1747                                 + "&rs=openHAB" + reolinkAuth;
1748                     }
1749                 }
1750                 if (rtspUri.isEmpty()) {
1751                     if (cameraConfig.getNvrChannel() < 1) {
1752                         rtspUri = "rtsp://" + cameraConfig.getIp() + ":554/h264Preview_01_main";
1753                     } else {
1754                         rtspUri = "rtsp://" + cameraConfig.getIp() + ":554/h264Preview_0" + cameraConfig.getNvrChannel()
1755                                 + "_main";
1756                     }
1757                 }
1758                 break;
1759         }
1760         // for poll times 9 seconds and above don't display a warning about the Image channel.
1761         if (9000 > cameraConfig.getPollTime() && cameraConfig.getUpdateImageWhen().contains("1")) {
1762             logger.warn(
1763                     "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.");
1764         }
1765         // ONVIF and Instar event handling need the server started before connecting.
1766         startStreamServer();
1767         tryConnecting();
1768     }
1769
1770     private void tryConnecting() {
1771         int firstDelay = 4;
1772         int normalDelay = 12; // doesn't make sense to have faster retry than CONNECT_TIMEOUT, which is 10 seconds, if
1773                               // camera is off
1774         if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)
1775                 && !thing.getThingTypeUID().getId().equals(DOORBIRD_THING) && cameraConfig.getOnvifPort() > 0) {
1776             onvifCamera = new OnvifConnection(this, cameraConfig.getIp() + ":" + cameraConfig.getOnvifPort(),
1777                     cameraConfig.getUser(), cameraConfig.getPassword());
1778             onvifCamera.setSelectedMediaProfile(cameraConfig.getOnvifMediaProfile());
1779             // Only use ONVIF events if it is not an API camera.
1780             onvifCamera.connect(supportsOnvifEvents());
1781
1782             if (supportsOnvifEvents()) {
1783                 // it takes some time to try to retrieve the ONVIF snapshot and stream URLs and update internal members
1784                 // on first connect; if connection lost, doesn't make sense to poll to often
1785                 firstDelay = 12;
1786                 normalDelay = 30;
1787             }
1788         }
1789         cameraConnectionJob = threadPool.scheduleWithFixedDelay(this::pollingCameraConnection, firstDelay, normalDelay,
1790                 TimeUnit.SECONDS);
1791     }
1792
1793     private boolean supportsOnvifEvents() {
1794         switch (thing.getThingTypeUID().getId()) {
1795             case ONVIF_THING:
1796                 return true;
1797             case REOLINK_THING:
1798                 if (cameraConfig.getNvrChannel() < 1) {
1799                     return true;
1800                 }
1801         }
1802         return false;
1803     }
1804
1805     private void keepMjpegRunning() {
1806         CameraServlet localServlet = servlet;
1807         if (localServlet != null && !localServlet.openStreams.isEmpty()) {
1808             if (!mjpegUri.isEmpty() && !"ffmpeg".equals(mjpegUri)) {
1809                 localServlet.openStreams.queueFrame(("--" + localServlet.openStreams.boundary + "\r\n\r\n").getBytes());
1810             }
1811             localServlet.openStreams.queueFrame(getSnapshot());
1812         }
1813     }
1814
1815     // What the camera needs to re-connect if the initialize() is not called.
1816     private void resetAndRetryConnecting() {
1817         offline();
1818         tryConnecting();
1819     }
1820
1821     private void offline() {
1822         isOnline = false;
1823         snapshotPolling = false;
1824         Future<?> localFuture = pollCameraJob;
1825         if (localFuture != null) {
1826             localFuture.cancel(true);
1827             pollCameraJob = null;
1828         }
1829         localFuture = authenticationJob;
1830         if (localFuture != null) {
1831             localFuture.cancel(true);
1832             authenticationJob = null;
1833         }
1834         localFuture = snapshotJob;
1835         if (localFuture != null) {
1836             localFuture.cancel(true);
1837             snapshotJob = null;
1838         }
1839         localFuture = cameraConnectionJob;
1840         if (localFuture != null) {
1841             localFuture.cancel(true);
1842             cameraConnectionJob = null;
1843         }
1844         Ffmpeg localFfmpeg = ffmpegHLS;
1845         if (localFfmpeg != null) {
1846             localFfmpeg.stopConverting();
1847             ffmpegHLS = null;
1848         }
1849         localFfmpeg = ffmpegRecord;
1850         if (localFfmpeg != null) {
1851             localFfmpeg.stopConverting();
1852             ffmpegRecord = null;
1853         }
1854         localFfmpeg = ffmpegGIF;
1855         if (localFfmpeg != null) {
1856             localFfmpeg.stopConverting();
1857             ffmpegGIF = null;
1858         }
1859         localFfmpeg = ffmpegRtspHelper;
1860         if (localFfmpeg != null) {
1861             localFfmpeg.stopConverting();
1862             ffmpegRtspHelper = null;
1863         }
1864         localFfmpeg = ffmpegMjpeg;
1865         if (localFfmpeg != null) {
1866             localFfmpeg.stopConverting();
1867             ffmpegMjpeg = null;
1868         }
1869         localFfmpeg = ffmpegSnapshot;
1870         if (localFfmpeg != null) {
1871             localFfmpeg.stopConverting();
1872             ffmpegSnapshot = null;
1873         }
1874         if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)) {// generic cameras do not have ONVIF support
1875             onvifCamera.disconnect();
1876         }
1877         openChannels.close();
1878     }
1879
1880     @Override
1881     public void dispose() {
1882         offline();
1883         CameraServlet localServlet = servlet;
1884         if (localServlet != null) {
1885             localServlet.dispose();
1886             servlet = null;
1887         }
1888         threadPool.shutdown();
1889         // inform all group handlers that this camera has gone offline
1890         groupTracker.listOfOnlineCameraHandlers.remove(this);
1891         groupTracker.listOfOnlineCameraUID.remove(getThing().getUID().getId());
1892         for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1893             handle.cameraOffline(this);
1894         }
1895         basicAuth = ""; // clear out stored Password hash
1896         useDigestAuth = false;
1897         mainEventLoopGroup.shutdownGracefully();
1898         mainBootstrap = null;
1899         channelTrackingMap.clear();
1900     }
1901
1902     public String getWhiteList() {
1903         return cameraConfig.getIpWhitelist();
1904     }
1905
1906     @Override
1907     public Collection<Class<? extends ThingHandlerService>> getServices() {
1908         return Set.of(IpCameraActions.class);
1909     }
1910 }