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