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