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