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