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