]> git.basschouten.com Git - openhab-addons.git/blob
ec01d41519ede1bc3b18c43315ba0add71f8983e
[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             if (rtspUri.isEmpty()) {
1359                 logger.warn("Binding has not been supplied with a FFmpeg Input URL, so some features will not work.");
1360             }
1361             if (snapshotUri.isEmpty() || "ffmpeg".equals(snapshotUri)) {
1362                 snapshotIsFfmpeg();
1363             } else {
1364                 updateSnapshot();
1365             }
1366             return;
1367         }
1368         if (!onvifCamera.isConnected()) {
1369             logger.debug("About to connect to the IP Camera using the ONVIF PORT at IP:{}:{}", cameraConfig.getIp(),
1370                     cameraConfig.getOnvifPort());
1371             onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1372         }
1373         if ("ffmpeg".equals(snapshotUri)) {
1374             snapshotIsFfmpeg();
1375         } else if (!snapshotUri.isEmpty()) {
1376             updateSnapshot();
1377         } else if (!rtspUri.isEmpty()) {
1378             snapshotIsFfmpeg();
1379         } else {
1380             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
1381                     "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.");
1382         }
1383     }
1384
1385     public void cameraConfigError(String reason) {
1386         // wont try to reconnect again due to a config error being the cause.
1387         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, reason);
1388         dispose();
1389     }
1390
1391     public void cameraCommunicationError(String reason) {
1392         // will try to reconnect again as camera may be rebooting.
1393         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
1394         if (isOnline) {// if already offline dont try reconnecting in 6 seconds, we want 30sec wait.
1395             resetAndRetryConnecting();
1396         }
1397     }
1398
1399     boolean streamIsStopped(String url) {
1400         ChannelTracking channelTracking = channelTrackingMap.get(url);
1401         if (channelTracking != null) {
1402             if (channelTracking.getChannel().isActive()) {
1403                 return false; // stream is running.
1404             }
1405         }
1406         return true; // Stream stopped or never started.
1407     }
1408
1409     void snapshotRunnable() {
1410         // Snapshot should be first to keep consistent time between shots
1411         updateSnapshot();
1412         if (snapCount > 0) {
1413             if (--snapCount == 0) {
1414                 setupFfmpegFormat(FFmpegFormat.GIF);
1415             }
1416         }
1417     }
1418
1419     private void takeSnapshot() {
1420         sendHttpGET(snapshotUri);
1421     }
1422
1423     private void updateSnapshot() {
1424         lastSnapshotRequest = Instant.now();
1425         mainEventLoopGroup.schedule(this::takeSnapshot, 0, TimeUnit.MILLISECONDS);
1426     }
1427
1428     public byte[] getSnapshot() {
1429         if (!isOnline) {
1430             // Single gray pixel JPG to keep streams open when the camera goes offline so they dont stop.
1431             return new byte[] { (byte) 0xff, (byte) 0xd8, (byte) 0xff, (byte) 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46,
1432                     0x00, 0x01, 0x01, 0x01, 0x00, 0x48, 0x00, 0x48, 0x00, 0x00, (byte) 0xff, (byte) 0xdb, 0x00, 0x43,
1433                     0x00, 0x03, 0x02, 0x02, 0x02, 0x02, 0x02, 0x03, 0x02, 0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x04,
1434                     0x06, 0x04, 0x04, 0x04, 0x04, 0x04, 0x08, 0x06, 0x06, 0x05, 0x06, 0x09, 0x08, 0x0a, 0x0a, 0x09,
1435                     0x08, 0x09, 0x09, 0x0a, 0x0c, 0x0f, 0x0c, 0x0a, 0x0b, 0x0e, 0x0b, 0x09, 0x09, 0x0d, 0x11, 0x0d,
1436                     0x0e, 0x0f, 0x10, 0x10, 0x11, 0x10, 0x0a, 0x0c, 0x12, 0x13, 0x12, 0x10, 0x13, 0x0f, 0x10, 0x10,
1437                     0x10, (byte) 0xff, (byte) 0xc9, 0x00, 0x0b, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00,
1438                     (byte) 0xff, (byte) 0xcc, 0x00, 0x06, 0x00, 0x10, 0x10, 0x05, (byte) 0xff, (byte) 0xda, 0x00, 0x08,
1439                     0x01, 0x01, 0x00, 0x00, 0x3f, 0x00, (byte) 0xd2, (byte) 0xcf, 0x20, (byte) 0xff, (byte) 0xd9 };
1440         }
1441         // Most cameras will return a 503 busy error if snapshot is faster than 1 second
1442         long lastUpdatedMs = Duration.between(lastSnapshotRequest, Instant.now()).toMillis();
1443         if (!snapshotPolling && !ffmpegSnapshotGeneration && lastUpdatedMs >= cameraConfig.getPollTime()) {
1444             updateSnapshot();
1445         }
1446         lockCurrentSnapshot.lock();
1447         try {
1448             return currentSnapshot;
1449         } finally {
1450             lockCurrentSnapshot.unlock();
1451         }
1452     }
1453
1454     public void stopSnapshotPolling() {
1455         Future<?> localFuture;
1456         if (!streamingSnapshotMjpeg && cameraConfig.getGifPreroll() == 0
1457                 && !cameraConfig.getUpdateImageWhen().contains("1")) {
1458             snapshotPolling = false;
1459             localFuture = snapshotJob;
1460             if (localFuture != null) {
1461                 localFuture.cancel(true);
1462             }
1463         } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // only during Motion Alarms
1464             snapshotPolling = false;
1465             localFuture = snapshotJob;
1466             if (localFuture != null) {
1467                 localFuture.cancel(true);
1468             }
1469         }
1470     }
1471
1472     public void startSnapshotPolling() {
1473         if (snapshotPolling || ffmpegSnapshotGeneration) {
1474             return; // Already polling or creating with FFmpeg from RTSP
1475         }
1476         if (streamingSnapshotMjpeg || streamingAutoFps || cameraConfig.getUpdateImageWhen().contains("4")) {
1477             snapshotPolling = true;
1478             snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 0, cameraConfig.getPollTime(),
1479                     TimeUnit.MILLISECONDS);
1480         }
1481     }
1482
1483     /**
1484      * {@link pollCameraRunnable} Polls every 8 seconds, to check camera is still ONLINE and keep alarm
1485      * streams open and more.
1486      *
1487      */
1488     void pollCameraRunnable() {
1489         // NOTE: Use lowPriorityRequests if get request is not needed every poll.
1490         if (!lowPriorityRequests.isEmpty()) {
1491             if (lowPriorityCounter >= lowPriorityRequests.size()) {
1492                 lowPriorityCounter = 0;
1493             }
1494             sendHttpGET(lowPriorityRequests.get(lowPriorityCounter++));
1495         }
1496         // what needs to be done every poll//
1497         switch (thing.getThingTypeUID().getId()) {
1498             case GENERIC_THING:
1499                 if (!snapshotUri.isEmpty() && !snapshotPolling) {
1500                     checkCameraConnection();
1501                 }
1502                 // RTSP stream has stopped and we need it for snapshots
1503                 if (ffmpegSnapshotGeneration) {
1504                     Ffmpeg localSnapshot = ffmpegSnapshot;
1505                     if (localSnapshot != null && !localSnapshot.getIsAlive()) {
1506                         localSnapshot.startConverting();
1507                     }
1508                 }
1509                 break;
1510             case ONVIF_THING:
1511                 if (!snapshotPolling) {
1512                     checkCameraConnection();
1513                 }
1514                 if (!onvifCamera.isConnected()) {
1515                     onvifCamera.connect(true);
1516                 }
1517                 break;
1518             case INSTAR_THING:
1519                 if (!snapshotPolling) {
1520                     checkCameraConnection();
1521                 }
1522                 noMotionDetected(CHANNEL_MOTION_ALARM);
1523                 noMotionDetected(CHANNEL_PIR_ALARM);
1524                 noAudioDetected();
1525                 break;
1526             case HIKVISION_THING:
1527                 if (streamIsStopped("/ISAPI/Event/notification/alertStream")) {
1528                     logger.info("The alarm stream was not running for camera {}, re-starting it now",
1529                             cameraConfig.getIp());
1530                     sendHttpGET("/ISAPI/Event/notification/alertStream");
1531                 }
1532                 break;
1533             case AMCREST_THING:
1534                 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=VideoMotion");
1535                 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=AudioMutation");
1536                 break;
1537             case DAHUA_THING:
1538                 if (!snapshotPolling) {
1539                     checkCameraConnection();
1540                 }
1541                 // Check for alarms, channel for NVRs appears not to work at filtering.
1542                 if (streamIsStopped("/cgi-bin/eventManager.cgi?action=attach&codes=[All]")) {
1543                     logger.info("The alarm stream was not running for camera {}, re-starting it now",
1544                             cameraConfig.getIp());
1545                     sendHttpGET("/cgi-bin/eventManager.cgi?action=attach&codes=[All]");
1546                 }
1547                 break;
1548             case DOORBIRD_THING:
1549                 if (!snapshotPolling) {
1550                     checkCameraConnection();
1551                 }
1552                 // Check for alarms, channel for NVRs appears not to work at filtering.
1553                 if (streamIsStopped("/bha-api/monitor.cgi?ring=doorbell,motionsensor")) {
1554                     logger.info("The alarm stream was not running for camera {}, re-starting it now",
1555                             cameraConfig.getIp());
1556                     sendHttpGET("/bha-api/monitor.cgi?ring=doorbell,motionsensor");
1557                 }
1558                 break;
1559             case FOSCAM_THING:
1560                 sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=getDevState&usr=" + cameraConfig.getUser() + "&pwd="
1561                         + cameraConfig.getPassword());
1562                 break;
1563         }
1564         Ffmpeg localHLS = ffmpegHLS;
1565         if (localHLS != null) {
1566             localHLS.checkKeepAlive();
1567         }
1568         if (openChannels.size() > 10) {
1569             logger.debug("There are {} open Channels being tracked.", openChannels.size());
1570             cleanChannels();
1571         }
1572     }
1573
1574     @Override
1575     public void initialize() {
1576         cameraConfig = getConfigAs(CameraConfig.class);
1577         threadPool = Executors.newScheduledThreadPool(2);
1578         mainEventLoopGroup = new NioEventLoopGroup(3);
1579         snapshotUri = getCorrectUrlFormat(cameraConfig.getSnapshotUrl());
1580         mjpegUri = getCorrectUrlFormat(cameraConfig.getMjpegUrl());
1581         rtspUri = cameraConfig.getFfmpegInput();
1582         if (cameraConfig.getFfmpegOutput().isEmpty()) {
1583             cameraConfig
1584                     .setFfmpegOutput(OpenHAB.getUserDataFolder() + "/ipcamera/" + this.thing.getUID().getId() + "/");
1585         }
1586         // Known cameras will connect quicker if we skip ONVIF questions.
1587         switch (thing.getThingTypeUID().getId()) {
1588             case AMCREST_THING:
1589             case DAHUA_THING:
1590                 if (mjpegUri.isEmpty()) {
1591                     mjpegUri = "/cgi-bin/mjpg/video.cgi?channel=" + cameraConfig.getNvrChannel() + "&subtype=1";
1592                 }
1593                 if (snapshotUri.isEmpty()) {
1594                     snapshotUri = "/cgi-bin/snapshot.cgi?channel=" + cameraConfig.getNvrChannel();
1595                 }
1596                 break;
1597             case DOORBIRD_THING:
1598                 if (mjpegUri.isEmpty()) {
1599                     mjpegUri = "/bha-api/video.cgi";
1600                 }
1601                 if (snapshotUri.isEmpty()) {
1602                     snapshotUri = "/bha-api/image.cgi";
1603                 }
1604                 break;
1605             case FOSCAM_THING:
1606                 // Foscam needs any special char like spaces (%20) to be encoded for URLs.
1607                 cameraConfig.setUser(Helper.encodeSpecialChars(cameraConfig.getUser()));
1608                 cameraConfig.setPassword(Helper.encodeSpecialChars(cameraConfig.getPassword()));
1609                 if (mjpegUri.isEmpty()) {
1610                     mjpegUri = "/cgi-bin/CGIStream.cgi?cmd=GetMJStream&usr=" + cameraConfig.getUser() + "&pwd="
1611                             + cameraConfig.getPassword();
1612                 }
1613                 if (snapshotUri.isEmpty()) {
1614                     snapshotUri = "/cgi-bin/CGIProxy.fcgi?usr=" + cameraConfig.getUser() + "&pwd="
1615                             + cameraConfig.getPassword() + "&cmd=snapPicture2";
1616                 }
1617                 break;
1618             case HIKVISION_THING:// The 02 gives you the first sub stream which needs to be set to MJPEG
1619                 if (mjpegUri.isEmpty()) {
1620                     mjpegUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "02" + "/httppreview";
1621                 }
1622                 if (snapshotUri.isEmpty()) {
1623                     snapshotUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "01/picture";
1624                 }
1625                 break;
1626             case INSTAR_THING:
1627                 if (snapshotUri.isEmpty()) {
1628                     snapshotUri = "/tmpfs/snap.jpg";
1629                 }
1630                 if (mjpegUri.isEmpty()) {
1631                     mjpegUri = "/mjpegstream.cgi?-chn=12";
1632                 }
1633                 sendHttpGET(
1634                         "/param.cgi?cmd=setmdalarm&-aname=server2&-switch=on&-interval=1&cmd=setalarmserverattr&-as_index=3&-as_server="
1635                                 + hostIp + "&-as_port=" + SERVLET_PORT + "&-as_path=/ipcamera/"
1636                                 + getThing().getUID().getId()
1637                                 + "/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");
1638                 break;
1639         }
1640         // for poll times 9 seconds and above don't display a warning about the Image channel.
1641         if (9000 > cameraConfig.getPollTime() && cameraConfig.getUpdateImageWhen().contains("1")) {
1642             logger.warn(
1643                     "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.");
1644         }
1645         // ONVIF and Instar event handling need the server started before connecting.
1646         startStreamServer();
1647         tryConnecting();
1648     }
1649
1650     private void tryConnecting() {
1651         if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)
1652                 && !thing.getThingTypeUID().getId().equals(DOORBIRD_THING)) {
1653             onvifCamera = new OnvifConnection(this, cameraConfig.getIp() + ":" + cameraConfig.getOnvifPort(),
1654                     cameraConfig.getUser(), cameraConfig.getPassword());
1655             onvifCamera.setSelectedMediaProfile(cameraConfig.getOnvifMediaProfile());
1656             // Only use ONVIF events if it is not an API camera.
1657             onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1658         }
1659         cameraConnectionJob = threadPool.scheduleWithFixedDelay(this::pollingCameraConnection, 4, 8, TimeUnit.SECONDS);
1660     }
1661
1662     private void keepMjpegRunning() {
1663         CameraServlet localServlet = servlet;
1664         if (localServlet != null && !localServlet.openStreams.isEmpty()) {
1665             if (!mjpegUri.isEmpty() && !"ffmpeg".equals(mjpegUri)) {
1666                 localServlet.openStreams.queueFrame(("--" + localServlet.openStreams.boundary + "\r\n\r\n").getBytes());
1667             }
1668             localServlet.openStreams.queueFrame(getSnapshot());
1669         }
1670     }
1671
1672     // What the camera needs to re-connect if the initialize() is not called.
1673     private void resetAndRetryConnecting() {
1674         offline();
1675         tryConnecting();
1676     }
1677
1678     private void offline() {
1679         isOnline = false;
1680         snapshotPolling = false;
1681         Future<?> localFuture = pollCameraJob;
1682         if (localFuture != null) {
1683             localFuture.cancel(true);
1684             localFuture = null;
1685         }
1686         localFuture = snapshotJob;
1687         if (localFuture != null) {
1688             localFuture.cancel(true);
1689             localFuture = null;
1690         }
1691         localFuture = cameraConnectionJob;
1692         if (localFuture != null) {
1693             localFuture.cancel(true);
1694             localFuture = null;
1695         }
1696         Ffmpeg localFfmpeg = ffmpegHLS;
1697         if (localFfmpeg != null) {
1698             localFfmpeg.stopConverting();
1699             ffmpegHLS = null;
1700         }
1701         localFfmpeg = ffmpegRecord;
1702         if (localFfmpeg != null) {
1703             localFfmpeg.stopConverting();
1704             ffmpegRecord = null;
1705         }
1706         localFfmpeg = ffmpegGIF;
1707         if (localFfmpeg != null) {
1708             localFfmpeg.stopConverting();
1709             ffmpegGIF = null;
1710         }
1711         localFfmpeg = ffmpegRtspHelper;
1712         if (localFfmpeg != null) {
1713             localFfmpeg.stopConverting();
1714             ffmpegRtspHelper = null;
1715         }
1716         localFfmpeg = ffmpegMjpeg;
1717         if (localFfmpeg != null) {
1718             localFfmpeg.stopConverting();
1719             ffmpegMjpeg = null;
1720         }
1721         localFfmpeg = ffmpegSnapshot;
1722         if (localFfmpeg != null) {
1723             localFfmpeg.stopConverting();
1724             ffmpegSnapshot = null;
1725         }
1726         onvifCamera.disconnect();
1727         openChannels.close();
1728     }
1729
1730     @Override
1731     public void dispose() {
1732         offline();
1733         CameraServlet localServlet = servlet;
1734         if (localServlet != null) {
1735             localServlet.dispose();
1736             localServlet = null;
1737         }
1738         threadPool.shutdown();
1739         // inform all group handlers that this camera has gone offline
1740         groupTracker.listOfOnlineCameraHandlers.remove(this);
1741         groupTracker.listOfOnlineCameraUID.remove(getThing().getUID().getId());
1742         for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1743             handle.cameraOffline(this);
1744         }
1745         basicAuth = ""; // clear out stored Password hash
1746         useDigestAuth = false;
1747         mainEventLoopGroup.shutdownGracefully();
1748         mainBootstrap = null;
1749         channelTrackingMap.clear();
1750     }
1751
1752     public String getWhiteList() {
1753         return cameraConfig.getIpWhitelist();
1754     }
1755
1756     @Override
1757     public Collection<Class<? extends ThingHandlerService>> getServices() {
1758         return Collections.singleton(IpCameraActions.class);
1759     }
1760 }