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