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