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