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