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