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