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