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