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