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