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