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