]> git.basschouten.com Git - openhab-addons.git/blob
4994cfb3e725b04a0ea270eb757e8bc8b2d8a4c4
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13
14 package org.openhab.binding.ipcamera.internal.handler;
15
16 import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
17
18 import java.io.File;
19 import java.io.FileNotFoundException;
20 import java.io.FileOutputStream;
21 import java.io.IOException;
22 import java.io.OutputStream;
23 import java.net.InetSocketAddress;
24 import java.net.MalformedURLException;
25 import java.net.URL;
26 import java.nio.charset.StandardCharsets;
27 import java.util.ArrayList;
28 import java.util.Collection;
29 import java.util.Collections;
30 import java.util.LinkedList;
31 import java.util.List;
32 import java.util.Map;
33 import java.util.concurrent.ConcurrentHashMap;
34 import java.util.concurrent.Executors;
35 import java.util.concurrent.Future;
36 import java.util.concurrent.ScheduledExecutorService;
37 import java.util.concurrent.ScheduledFuture;
38 import java.util.concurrent.TimeUnit;
39 import java.util.concurrent.locks.ReentrantLock;
40
41 import org.eclipse.jdt.annotation.NonNullByDefault;
42 import org.eclipse.jdt.annotation.Nullable;
43 import org.openhab.binding.ipcamera.internal.AmcrestHandler;
44 import org.openhab.binding.ipcamera.internal.CameraConfig;
45 import org.openhab.binding.ipcamera.internal.ChannelTracking;
46 import org.openhab.binding.ipcamera.internal.DahuaHandler;
47 import org.openhab.binding.ipcamera.internal.DoorBirdHandler;
48 import org.openhab.binding.ipcamera.internal.Ffmpeg;
49 import org.openhab.binding.ipcamera.internal.FoscamHandler;
50 import org.openhab.binding.ipcamera.internal.GroupTracker;
51 import org.openhab.binding.ipcamera.internal.Helper;
52 import org.openhab.binding.ipcamera.internal.HikvisionHandler;
53 import org.openhab.binding.ipcamera.internal.HttpOnlyHandler;
54 import org.openhab.binding.ipcamera.internal.InstarHandler;
55 import org.openhab.binding.ipcamera.internal.IpCameraActions;
56 import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
57 import org.openhab.binding.ipcamera.internal.IpCameraDynamicStateDescriptionProvider;
58 import org.openhab.binding.ipcamera.internal.MyNettyAuthHandler;
59 import org.openhab.binding.ipcamera.internal.StreamServerHandler;
60 import org.openhab.binding.ipcamera.internal.onvif.OnvifConnection;
61 import org.openhab.core.library.types.DecimalType;
62 import org.openhab.core.library.types.IncreaseDecreaseType;
63 import org.openhab.core.library.types.OnOffType;
64 import org.openhab.core.library.types.PercentType;
65 import org.openhab.core.library.types.RawType;
66 import org.openhab.core.library.types.StringType;
67 import org.openhab.core.thing.ChannelUID;
68 import org.openhab.core.thing.Thing;
69 import org.openhab.core.thing.ThingStatus;
70 import org.openhab.core.thing.ThingStatusDetail;
71 import org.openhab.core.thing.binding.BaseThingHandler;
72 import org.openhab.core.thing.binding.ThingHandlerService;
73 import org.openhab.core.types.Command;
74 import org.openhab.core.types.RefreshType;
75 import org.openhab.core.types.State;
76 import org.slf4j.Logger;
77 import org.slf4j.LoggerFactory;
78
79 import io.netty.bootstrap.Bootstrap;
80 import io.netty.bootstrap.ServerBootstrap;
81 import io.netty.buffer.ByteBuf;
82 import io.netty.buffer.Unpooled;
83 import io.netty.channel.Channel;
84 import io.netty.channel.ChannelDuplexHandler;
85 import io.netty.channel.ChannelFuture;
86 import io.netty.channel.ChannelFutureListener;
87 import io.netty.channel.ChannelHandlerContext;
88 import io.netty.channel.ChannelInitializer;
89 import io.netty.channel.ChannelOption;
90 import io.netty.channel.EventLoopGroup;
91 import io.netty.channel.group.ChannelGroup;
92 import io.netty.channel.group.DefaultChannelGroup;
93 import io.netty.channel.nio.NioEventLoopGroup;
94 import io.netty.channel.socket.SocketChannel;
95 import io.netty.channel.socket.nio.NioServerSocketChannel;
96 import io.netty.channel.socket.nio.NioSocketChannel;
97 import io.netty.handler.codec.base64.Base64;
98 import io.netty.handler.codec.http.DefaultFullHttpRequest;
99 import io.netty.handler.codec.http.DefaultHttpResponse;
100 import io.netty.handler.codec.http.FullHttpRequest;
101 import io.netty.handler.codec.http.HttpClientCodec;
102 import io.netty.handler.codec.http.HttpContent;
103 import io.netty.handler.codec.http.HttpHeaderNames;
104 import io.netty.handler.codec.http.HttpHeaderValues;
105 import io.netty.handler.codec.http.HttpMessage;
106 import io.netty.handler.codec.http.HttpMethod;
107 import io.netty.handler.codec.http.HttpResponse;
108 import io.netty.handler.codec.http.HttpResponseStatus;
109 import io.netty.handler.codec.http.HttpServerCodec;
110 import io.netty.handler.codec.http.HttpVersion;
111 import io.netty.handler.codec.http.LastHttpContent;
112 import io.netty.handler.stream.ChunkedWriteHandler;
113 import io.netty.handler.timeout.IdleState;
114 import io.netty.handler.timeout.IdleStateEvent;
115 import io.netty.handler.timeout.IdleStateHandler;
116 import io.netty.util.CharsetUtil;
117 import io.netty.util.ReferenceCountUtil;
118 import io.netty.util.concurrent.GlobalEventExecutor;
119
120 /**
121  * The {@link IpCameraHandler} is responsible for handling commands, which are
122  * sent to one of the channels.
123  *
124  * @author Matthew Skinner - Initial contribution
125  */
126
127 @NonNullByDefault
128 public class IpCameraHandler extends BaseThingHandler {
129     public final Logger logger = LoggerFactory.getLogger(getClass());
130     public final IpCameraDynamicStateDescriptionProvider stateDescriptionProvider;
131     private ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(4);
132     private GroupTracker groupTracker;
133     public CameraConfig cameraConfig = new CameraConfig();
134
135     // ChannelGroup is thread safe
136     public final ChannelGroup mjpegChannelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
137     private final ChannelGroup snapshotMjpegChannelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
138     private final ChannelGroup autoSnapshotMjpegChannelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
139     public final ChannelGroup openChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
140     public @Nullable Ffmpeg ffmpegHLS = null;
141     public @Nullable Ffmpeg ffmpegRecord = null;
142     public @Nullable Ffmpeg ffmpegGIF = null;
143     public @Nullable Ffmpeg ffmpegRtspHelper = null;
144     public @Nullable Ffmpeg ffmpegMjpeg = null;
145     public @Nullable Ffmpeg ffmpegSnapshot = null;
146     public boolean streamingAutoFps = false;
147     public boolean motionDetected = false;
148
149     private @Nullable ScheduledFuture<?> cameraConnectionJob = null;
150     private @Nullable ScheduledFuture<?> pollCameraJob = null;
151     private @Nullable ScheduledFuture<?> snapshotJob = null;
152     private @Nullable Bootstrap mainBootstrap;
153     private @Nullable ServerBootstrap serverBootstrap;
154
155     private EventLoopGroup mainEventLoopGroup = new NioEventLoopGroup();
156     private EventLoopGroup serversLoopGroup = new NioEventLoopGroup();
157     private FullHttpRequest putRequestWithBody = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod("PUT"),
158             "");
159     private String gifFilename = "ipcamera";
160     private String gifHistory = "";
161     private String mp4History = "";
162     public int gifHistoryLength;
163     public int mp4HistoryLength;
164     private String mp4Filename = "ipcamera";
165     private int mp4RecordTime;
166     private int gifRecordTime = 5;
167     private LinkedList<byte[]> fifoSnapshotBuffer = new LinkedList<byte[]>();
168     private int snapCount;
169     private boolean updateImageChannel = false;
170     private boolean updateAutoFps = false;
171     private byte lowPriorityCounter = 0;
172     public String hostIp;
173     public Map<String, ChannelTracking> channelTrackingMap = new ConcurrentHashMap<>();
174     public List<String> lowPriorityRequests = new ArrayList<>(0);
175
176     // basicAuth MUST remain private as it holds the cameraConfig.getPassword()
177     private String basicAuth = "";
178     public boolean useBasicAuth = false;
179     public boolean useDigestAuth = false;
180     public String snapshotUri = "";
181     public String mjpegUri = "";
182     private @Nullable ChannelFuture serverFuture = null;
183     private Object firstStreamedMsg = new Object();
184     public byte[] currentSnapshot = new byte[] { (byte) 0x00 };
185     public ReentrantLock lockCurrentSnapshot = new ReentrantLock();
186     public String rtspUri = "";
187     public boolean audioAlarmUpdateSnapshot = false;
188     private boolean motionAlarmUpdateSnapshot = false;
189     private boolean isOnline = false; // Used so only 1 error is logged when a network issue occurs.
190     private boolean firstAudioAlarm = false;
191     private boolean firstMotionAlarm = false;
192     public Double motionThreshold = 0.0016;
193     public int audioThreshold = 35;
194     @SuppressWarnings("unused")
195     private @Nullable StreamServerHandler streamServerHandler;
196     private boolean streamingSnapshotMjpeg = false;
197     public boolean motionAlarmEnabled = false;
198     public boolean audioAlarmEnabled = false;
199     public boolean ffmpegSnapshotGeneration = false;
200     public boolean snapshotPolling = false;
201     public OnvifConnection onvifCamera = new OnvifConnection(this, "", "", "");
202
203     // These methods handle the response from all camera brands, nothing specific to 1 brand.
204     private class CommonCameraHandler extends ChannelDuplexHandler {
205         private int bytesToRecieve = 0;
206         private int bytesAlreadyRecieved = 0;
207         private byte[] incomingJpeg = new byte[0];
208         private String incomingMessage = "";
209         private String contentType = "empty";
210         private Object reply = new Object();
211         private String requestUrl = "";
212         private boolean closeConnection = true;
213         private boolean isChunked = false;
214
215         public void setURL(String url) {
216             requestUrl = url;
217         }
218
219         @Override
220         public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
221             if (msg == null || ctx == null) {
222                 return;
223             }
224             try {
225                 if (msg instanceof HttpResponse) {
226                     HttpResponse response = (HttpResponse) msg;
227                     if (response.status().code() != 401) {
228                         if (!response.headers().isEmpty()) {
229                             for (String name : response.headers().names()) {
230                                 // Some cameras use first letter uppercase and others dont.
231                                 switch (name.toLowerCase()) { // Possible localization issues doing this
232                                     case "content-type":
233                                         contentType = response.headers().getAsString(name);
234                                         break;
235                                     case "content-length":
236                                         bytesToRecieve = Integer.parseInt(response.headers().getAsString(name));
237                                         break;
238                                     case "connection":
239                                         if (response.headers().getAsString(name).contains("keep-alive")) {
240                                             closeConnection = false;
241                                         }
242                                         break;
243                                     case "transfer-encoding":
244                                         if (response.headers().getAsString(name).contains("chunked")) {
245                                             isChunked = true;
246                                         }
247                                         break;
248                                 }
249                             }
250                             if (contentType.contains("multipart")) {
251                                 closeConnection = false;
252                                 if (mjpegUri.contains(requestUrl)) {
253                                     if (msg instanceof HttpMessage) {
254                                         // very start of stream only
255                                         ReferenceCountUtil.retain(msg, 1);
256                                         firstStreamedMsg = msg;
257                                         streamToGroup(firstStreamedMsg, mjpegChannelGroup, true);
258                                     }
259                                 }
260                             } else if (contentType.contains("image/jp")) {
261                                 if (bytesToRecieve == 0) {
262                                     bytesToRecieve = 768000; // 0.768 Mbyte when no Content-Length is sent
263                                     logger.debug("Camera has no Content-Length header, we have to guess how much RAM.");
264                                 }
265                                 incomingJpeg = new byte[bytesToRecieve];
266                             }
267                         }
268                     }
269                 }
270                 if (msg instanceof HttpContent) {
271                     if (mjpegUri.contains(requestUrl)) {
272                         // multiple MJPEG stream packets come back as this.
273                         ReferenceCountUtil.retain(msg, 1);
274                         streamToGroup(msg, mjpegChannelGroup, true);
275                     } else {
276                         HttpContent content = (HttpContent) msg;
277                         // Found some cameras uses Content-Type: image/jpg instead of image/jpeg
278                         if (contentType.contains("image/jp")) {
279                             for (int i = 0; i < content.content().capacity(); i++) {
280                                 incomingJpeg[bytesAlreadyRecieved++] = content.content().getByte(i);
281                             }
282                             if (content instanceof LastHttpContent) {
283                                 processSnapshot(incomingJpeg);
284                                 // testing next line and if works need to do a full cleanup of this function.
285                                 closeConnection = true;
286                                 if (closeConnection) {
287                                     ctx.close();
288                                 } else {
289                                     bytesToRecieve = 0;
290                                     bytesAlreadyRecieved = 0;
291                                 }
292                             }
293                         } else { // incomingMessage that is not an IMAGE
294                             if (incomingMessage.isEmpty()) {
295                                 incomingMessage = content.content().toString(CharsetUtil.UTF_8);
296                             } else {
297                                 incomingMessage += content.content().toString(CharsetUtil.UTF_8);
298                             }
299                             bytesAlreadyRecieved = incomingMessage.length();
300                             if (content instanceof LastHttpContent) {
301                                 // If it is not an image send it on to the next handler//
302                                 if (bytesAlreadyRecieved != 0) {
303                                     reply = incomingMessage;
304                                     super.channelRead(ctx, reply);
305                                 }
306                             }
307                             // HIKVISION alertStream never has a LastHttpContent as it always stays open//
308                             if (contentType.contains("multipart")) {
309                                 if (bytesAlreadyRecieved != 0) {
310                                     reply = incomingMessage;
311                                     incomingMessage = "";
312                                     bytesToRecieve = 0;
313                                     bytesAlreadyRecieved = 0;
314                                     super.channelRead(ctx, reply);
315                                 }
316                             }
317                             // Foscam needs this as will other cameras with chunks//
318                             if (isChunked && bytesAlreadyRecieved != 0) {
319                                 reply = incomingMessage;
320                                 super.channelRead(ctx, reply);
321                             }
322                         }
323                     }
324                 } else { // msg is not HttpContent
325                     // Foscam and Amcrest 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 outputOptions = "-f null -";
986                 String filterOptions = "";
987                 if (!audioAlarmEnabled) {
988                     filterOptions = "-an";
989                 } else {
990                     filterOptions = "-af silencedetect=n=-" + audioThreshold + "dB:d=2";
991                 }
992                 if (!motionAlarmEnabled && !ffmpegSnapshotGeneration) {
993                     filterOptions = filterOptions.concat(" -vn");
994                 } else if (motionAlarmEnabled) {
995                     filterOptions = filterOptions
996                             .concat(" -vf select='gte(scene," + motionThreshold + ")',metadata=print");
997                 }
998                 if (!cameraConfig.getUser().isEmpty()) {
999                     filterOptions += " ";// add space as the Framework does not allow spaces at start of config.
1000                 }
1001                 ffmpegRtspHelper = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, input,
1002                         filterOptions + cameraConfig.getMotionOptions(), outputOptions, cameraConfig.getUser(),
1003                         cameraConfig.getPassword());
1004                 localAlarms = ffmpegRtspHelper;
1005                 if (localAlarms != null) {
1006                     localAlarms.startConverting();
1007                 }
1008                 break;
1009             case MJPEG:
1010                 if (ffmpegMjpeg == null) {
1011                     if (inputOptions.isEmpty()) {
1012                         inputOptions = "-hide_banner -loglevel warning";
1013                     } else {
1014                         inputOptions += " -hide_banner -loglevel warning";
1015                     }
1016                     ffmpegMjpeg = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
1017                             cameraConfig.getMjpegOptions(),
1018                             "http://127.0.0.1:" + cameraConfig.getServerPort() + "/ipcamera.jpg",
1019                             cameraConfig.getUser(), cameraConfig.getPassword());
1020                 }
1021                 Ffmpeg localMjpeg = ffmpegMjpeg;
1022                 if (localMjpeg != null) {
1023                     localMjpeg.startConverting();
1024                 }
1025                 break;
1026             case SNAPSHOT:
1027                 // if mjpeg stream you can use 'ffmpeg -i input -codec:v copy -bsf:v mjpeg2jpeg output.jpg'
1028                 if (ffmpegSnapshot == null) {
1029                     if (inputOptions.isEmpty()) {
1030                         // iFrames only
1031                         inputOptions = "-threads 1 -skip_frame nokey -hide_banner -loglevel warning";
1032                     } else {
1033                         inputOptions += " -threads 1 -skip_frame nokey -hide_banner -loglevel warning";
1034                     }
1035                     ffmpegSnapshot = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
1036                             cameraConfig.getSnapshotOptions(),
1037                             "http://127.0.0.1:" + cameraConfig.getServerPort() + "/snapshot.jpg",
1038                             cameraConfig.getUser(), cameraConfig.getPassword());
1039                 }
1040                 Ffmpeg localSnaps = ffmpegSnapshot;
1041                 if (localSnaps != null) {
1042                     localSnaps.startConverting();
1043                 }
1044                 break;
1045         }
1046     }
1047
1048     public void noMotionDetected(String thisAlarmsChannel) {
1049         setChannelState(thisAlarmsChannel, OnOffType.OFF);
1050         firstMotionAlarm = false;
1051         motionAlarmUpdateSnapshot = false;
1052         motionDetected = false;
1053         if (streamingAutoFps) {
1054             stopSnapshotPolling();
1055         } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1056             stopSnapshotPolling();
1057         }
1058     }
1059
1060     /**
1061      * The {@link changeAlarmState} To only be used to change alarms channels that are not counted as motion. This will
1062      * allow logic to be added here in the future. Example more than 1 type of alarm may indicate that someone is
1063      * tampering with the camera.
1064      */
1065     public void changeAlarmState(String thisAlarmsChannel, OnOffType state) {
1066         updateState(thisAlarmsChannel, state);
1067     }
1068
1069     public void motionDetected(String thisAlarmsChannel) {
1070         updateState(CHANNEL_LAST_MOTION_TYPE, new StringType(thisAlarmsChannel));
1071         updateState(thisAlarmsChannel, OnOffType.ON);
1072         motionDetected = true;
1073         if (streamingAutoFps) {
1074             startSnapshotPolling();
1075         }
1076         if (cameraConfig.getUpdateImageWhen().contains("2")) {
1077             if (!firstMotionAlarm) {
1078                 if (!snapshotUri.isEmpty()) {
1079                     sendHttpGET(snapshotUri);
1080                 }
1081                 firstMotionAlarm = true;// reset back to false when the jpg arrives.
1082             }
1083         } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1084             if (!snapshotPolling) {
1085                 startSnapshotPolling();
1086             }
1087             firstMotionAlarm = true;
1088             motionAlarmUpdateSnapshot = true;
1089         }
1090     }
1091
1092     public void audioDetected() {
1093         updateState(CHANNEL_AUDIO_ALARM, OnOffType.ON);
1094         if (cameraConfig.getUpdateImageWhen().contains("3")) {
1095             if (!firstAudioAlarm) {
1096                 if (!snapshotUri.isEmpty()) {
1097                     sendHttpGET(snapshotUri);
1098                 }
1099                 firstAudioAlarm = true;// reset back to false when the jpg arrives.
1100             }
1101         } else if (cameraConfig.getUpdateImageWhen().contains("5")) {// During audio alarms
1102             firstAudioAlarm = true;
1103             audioAlarmUpdateSnapshot = true;
1104         }
1105     }
1106
1107     public void noAudioDetected() {
1108         setChannelState(CHANNEL_AUDIO_ALARM, OnOffType.OFF);
1109         firstAudioAlarm = false;
1110         audioAlarmUpdateSnapshot = false;
1111     }
1112
1113     public void recordMp4(String filename, int seconds) {
1114         mp4Filename = filename;
1115         mp4RecordTime = seconds;
1116         setupFfmpegFormat(FFmpegFormat.RECORD);
1117         setChannelState(CHANNEL_RECORDING_MP4, DecimalType.valueOf(new String("" + seconds)));
1118     }
1119
1120     public void recordGif(String filename, int seconds) {
1121         gifFilename = filename;
1122         gifRecordTime = seconds;
1123         if (cameraConfig.getGifPreroll() > 0) {
1124             snapCount = seconds;
1125         } else {
1126             setupFfmpegFormat(FFmpegFormat.GIF);
1127         }
1128         setChannelState(CHANNEL_RECORDING_GIF, DecimalType.valueOf(new String("" + seconds)));
1129     }
1130
1131     public String returnValueFromString(String rawString, String searchedString) {
1132         String result = "";
1133         int index = rawString.indexOf(searchedString);
1134         if (index != -1) // -1 means "not found"
1135         {
1136             result = rawString.substring(index + searchedString.length(), rawString.length());
1137             index = result.indexOf("\r\n"); // find a carriage return to find the end of the value.
1138             if (index == -1) {
1139                 return result; // Did not find a carriage return.
1140             } else {
1141                 return result.substring(0, index);
1142             }
1143         }
1144         return ""; // Did not find the String we were searching for
1145     }
1146
1147     private void sendPTZRequest() {
1148         onvifCamera.sendPTZRequest(OnvifConnection.RequestType.AbsoluteMove);
1149     }
1150
1151     @Override
1152     public void handleCommand(ChannelUID channelUID, Command command) {
1153         if (command instanceof RefreshType) {
1154             switch (channelUID.getId()) {
1155                 case CHANNEL_PAN:
1156                     if (onvifCamera.supportsPTZ()) {
1157                         updateState(CHANNEL_PAN, new PercentType(Math.round(onvifCamera.getAbsolutePan())));
1158                     }
1159                     return;
1160                 case CHANNEL_TILT:
1161                     if (onvifCamera.supportsPTZ()) {
1162                         updateState(CHANNEL_TILT, new PercentType(Math.round(onvifCamera.getAbsoluteTilt())));
1163                     }
1164                     return;
1165                 case CHANNEL_ZOOM:
1166                     if (onvifCamera.supportsPTZ()) {
1167                         updateState(CHANNEL_ZOOM, new PercentType(Math.round(onvifCamera.getAbsoluteZoom())));
1168                     }
1169                     return;
1170                 case CHANNEL_GOTO_PRESET:
1171                     if (onvifCamera.supportsPTZ()) {
1172                         onvifCamera.sendPTZRequest(OnvifConnection.RequestType.GetPresets);
1173                     }
1174                     return;
1175             }
1176         } // caution "REFRESH" can still progress to brand Handlers below the else.
1177         else {
1178             switch (channelUID.getId()) {
1179                 case CHANNEL_MP4_HISTORY_LENGTH:
1180                     if (DecimalType.ZERO.equals(command)) {
1181                         mp4HistoryLength = 0;
1182                         mp4History = "";
1183                         setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
1184                     }
1185                     return;
1186                 case CHANNEL_GIF_HISTORY_LENGTH:
1187                     if (DecimalType.ZERO.equals(command)) {
1188                         gifHistoryLength = 0;
1189                         gifHistory = "";
1190                         setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
1191                     }
1192                     return;
1193                 case CHANNEL_FFMPEG_MOTION_CONTROL:
1194                     if (OnOffType.ON.equals(command)) {
1195                         motionAlarmEnabled = true;
1196                     } else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
1197                         motionAlarmEnabled = false;
1198                         noMotionDetected(CHANNEL_FFMPEG_MOTION_ALARM);
1199                     } else {
1200                         motionAlarmEnabled = true;
1201                         motionThreshold = Double.valueOf(command.toString());
1202                         motionThreshold = motionThreshold / 10000;
1203                     }
1204                     setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
1205                     return;
1206                 case CHANNEL_START_STREAM:
1207                     Ffmpeg localHLS;
1208                     if (OnOffType.ON.equals(command)) {
1209                         localHLS = ffmpegHLS;
1210                         if (localHLS == null) {
1211                             setupFfmpegFormat(FFmpegFormat.HLS);
1212                             localHLS = ffmpegHLS;
1213                         }
1214                         if (localHLS != null) {
1215                             localHLS.setKeepAlive(-1);// Now will run till manually stopped.
1216                             localHLS.startConverting();
1217                         }
1218                     } else {
1219                         localHLS = ffmpegHLS;
1220                         if (localHLS != null) {
1221                             // Still runs but will be able to auto stop when the HLS stream is no longer used.
1222                             localHLS.setKeepAlive(1);
1223                         }
1224                     }
1225                     return;
1226                 case CHANNEL_EXTERNAL_MOTION:
1227                     if (OnOffType.ON.equals(command)) {
1228                         motionDetected(CHANNEL_EXTERNAL_MOTION);
1229                     } else {
1230                         noMotionDetected(CHANNEL_EXTERNAL_MOTION);
1231                     }
1232                     return;
1233                 case CHANNEL_GOTO_PRESET:
1234                     if (onvifCamera.supportsPTZ()) {
1235                         onvifCamera.gotoPreset(Integer.valueOf(command.toString()));
1236                     }
1237                     return;
1238                 case CHANNEL_POLL_IMAGE:
1239                     if (OnOffType.ON.equals(command)) {
1240                         if (snapshotUri.isEmpty()) {
1241                             ffmpegSnapshotGeneration = true;
1242                             setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1243                             updateImageChannel = false;
1244                         } else {
1245                             updateImageChannel = true;
1246                             sendHttpGET(snapshotUri);// Allows this to change Image FPS on demand
1247                         }
1248                     } else {
1249                         Ffmpeg localSnaps = ffmpegSnapshot;
1250                         if (localSnaps != null) {
1251                             localSnaps.stopConverting();
1252                             ffmpegSnapshotGeneration = false;
1253                         }
1254                         updateImageChannel = false;
1255                     }
1256                     return;
1257                 case CHANNEL_PAN:
1258                     if (onvifCamera.supportsPTZ()) {
1259                         if (command instanceof IncreaseDecreaseType) {
1260                             if (command == IncreaseDecreaseType.INCREASE) {
1261                                 if (cameraConfig.getPtzContinuous()) {
1262                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveLeft);
1263                                 } else {
1264                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveLeft);
1265                                 }
1266                             } else {
1267                                 if (cameraConfig.getPtzContinuous()) {
1268                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveRight);
1269                                 } else {
1270                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveRight);
1271                                 }
1272                             }
1273                             return;
1274                         } else if (OnOffType.OFF.equals(command)) {
1275                             onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1276                             return;
1277                         }
1278                         onvifCamera.setAbsolutePan(Float.valueOf(command.toString()));
1279                         threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1280                     }
1281                     return;
1282                 case CHANNEL_TILT:
1283                     if (onvifCamera.supportsPTZ()) {
1284                         if (command instanceof IncreaseDecreaseType) {
1285                             if (IncreaseDecreaseType.INCREASE.equals(command)) {
1286                                 if (cameraConfig.getPtzContinuous()) {
1287                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveUp);
1288                                 } else {
1289                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveUp);
1290                                 }
1291                             } else {
1292                                 if (cameraConfig.getPtzContinuous()) {
1293                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveDown);
1294                                 } else {
1295                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveDown);
1296                                 }
1297                             }
1298                             return;
1299                         } else if (OnOffType.OFF.equals(command)) {
1300                             onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1301                             return;
1302                         }
1303                         onvifCamera.setAbsoluteTilt(Float.valueOf(command.toString()));
1304                         threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1305                     }
1306                     return;
1307                 case CHANNEL_ZOOM:
1308                     if (onvifCamera.supportsPTZ()) {
1309                         if (command instanceof IncreaseDecreaseType) {
1310                             if (IncreaseDecreaseType.INCREASE.equals(command)) {
1311                                 if (cameraConfig.getPtzContinuous()) {
1312                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveIn);
1313                                 } else {
1314                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveIn);
1315                                 }
1316                             } else {
1317                                 if (cameraConfig.getPtzContinuous()) {
1318                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveOut);
1319                                 } else {
1320                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveOut);
1321                                 }
1322                             }
1323                             return;
1324                         } else if (OnOffType.OFF.equals(command)) {
1325                             onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1326                             return;
1327                         }
1328                         onvifCamera.setAbsoluteZoom(Float.valueOf(command.toString()));
1329                         threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1330                     }
1331                     return;
1332             }
1333         }
1334         // commands and refresh now get passed to brand handlers
1335         switch (thing.getThingTypeUID().getId()) {
1336             case AMCREST_THING:
1337                 AmcrestHandler amcrestHandler = new AmcrestHandler(getHandle());
1338                 amcrestHandler.handleCommand(channelUID, command);
1339                 if (lowPriorityRequests.isEmpty()) {
1340                     lowPriorityRequests = amcrestHandler.getLowPriorityRequests();
1341                 }
1342                 break;
1343             case DAHUA_THING:
1344                 DahuaHandler dahuaHandler = new DahuaHandler(getHandle(), cameraConfig.getNvrChannel());
1345                 dahuaHandler.handleCommand(channelUID, command);
1346                 if (lowPriorityRequests.isEmpty()) {
1347                     lowPriorityRequests = dahuaHandler.getLowPriorityRequests();
1348                 }
1349                 break;
1350             case DOORBIRD_THING:
1351                 DoorBirdHandler doorBirdHandler = new DoorBirdHandler(getHandle());
1352                 doorBirdHandler.handleCommand(channelUID, command);
1353                 if (lowPriorityRequests.isEmpty()) {
1354                     lowPriorityRequests = doorBirdHandler.getLowPriorityRequests();
1355                 }
1356                 break;
1357             case HIKVISION_THING:
1358                 HikvisionHandler hikvisionHandler = new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel());
1359                 hikvisionHandler.handleCommand(channelUID, command);
1360                 if (lowPriorityRequests.isEmpty()) {
1361                     lowPriorityRequests = hikvisionHandler.getLowPriorityRequests();
1362                 }
1363                 break;
1364             case FOSCAM_THING:
1365                 FoscamHandler foscamHandler = new FoscamHandler(getHandle(), cameraConfig.getUser(),
1366                         cameraConfig.getPassword());
1367                 foscamHandler.handleCommand(channelUID, command);
1368                 if (lowPriorityRequests.isEmpty()) {
1369                     lowPriorityRequests = foscamHandler.getLowPriorityRequests();
1370                 }
1371                 break;
1372             case INSTAR_THING:
1373                 InstarHandler instarHandler = new InstarHandler(getHandle());
1374                 instarHandler.handleCommand(channelUID, command);
1375                 if (lowPriorityRequests.isEmpty()) {
1376                     lowPriorityRequests = instarHandler.getLowPriorityRequests();
1377                 }
1378                 break;
1379             default:
1380                 HttpOnlyHandler defaultHandler = new HttpOnlyHandler(getHandle());
1381                 defaultHandler.handleCommand(channelUID, command);
1382                 if (lowPriorityRequests.isEmpty()) {
1383                     lowPriorityRequests = defaultHandler.getLowPriorityRequests();
1384                 }
1385                 break;
1386         }
1387     }
1388
1389     public void setChannelState(String channelToUpdate, State valueOf) {
1390         updateState(channelToUpdate, valueOf);
1391     }
1392
1393     void bringCameraOnline() {
1394         isOnline = true;
1395         updateStatus(ThingStatus.ONLINE);
1396         groupTracker.listOfOnlineCameraHandlers.add(this);
1397         groupTracker.listOfOnlineCameraUID.add(getThing().getUID().getId());
1398         Future<?> localFuture = cameraConnectionJob;
1399         if (localFuture != null) {
1400             localFuture.cancel(false);
1401         }
1402
1403         if (cameraConfig.getGifPreroll() > 0 || cameraConfig.getUpdateImageWhen().contains("1")) {
1404             snapshotPolling = true;
1405             snapshotJob = threadPool.scheduleAtFixedRate(this::snapshotRunnable, 1000, cameraConfig.getPollTime(),
1406                     TimeUnit.MILLISECONDS);
1407         }
1408
1409         pollCameraJob = threadPool.scheduleWithFixedDelay(this::pollCameraRunnable, 1000, 8000, TimeUnit.MILLISECONDS);
1410
1411         if (!rtspUri.isEmpty()) {
1412             updateState(CHANNEL_RTSP_URL, new StringType(rtspUri));
1413         }
1414         if (updateImageChannel) {
1415             updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1416         } else {
1417             updateState(CHANNEL_POLL_IMAGE, OnOffType.OFF);
1418         }
1419         if (!groupTracker.listOfGroupHandlers.isEmpty()) {
1420             for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1421                 handle.cameraOnline(getThing().getUID().getId());
1422             }
1423         }
1424     }
1425
1426     void snapshotIsFfmpeg() {
1427         bringCameraOnline();
1428         snapshotUri = "";// ffmpeg is a valid option. Simplify further checks.
1429         logger.debug(
1430                 "Binding has no snapshot url. Will use your CPU and FFmpeg to create snapshots from the cameras RTSP.");
1431         if (!rtspUri.isEmpty()) {
1432             updateImageChannel = false;
1433             ffmpegSnapshotGeneration = true;
1434             setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1435             updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1436         } else {
1437             cameraConfigError("Binding can not find a RTSP url for this camera, please provide a FFmpeg Input URL.");
1438         }
1439     }
1440
1441     void pollingCameraConnection() {
1442         if (thing.getThingTypeUID().getId().equals(GENERIC_THING)) {
1443             if (rtspUri.isEmpty()) {
1444                 logger.warn("Binding has not been supplied with a FFmpeg Input URL, so some features will not work.");
1445             }
1446             if (snapshotUri.isEmpty() || snapshotUri.equals("ffmpeg")) {
1447                 snapshotIsFfmpeg();
1448             } else {
1449                 sendHttpRequest("GET", snapshotUri, null);
1450             }
1451             return;
1452         }
1453         if (!onvifCamera.isConnected()) {
1454             logger.debug("About to connect to the IP Camera using the ONVIF PORT at IP:{}:{}", cameraConfig.getIp(),
1455                     cameraConfig.getOnvifPort());
1456             onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1457         }
1458         if (snapshotUri.equals("ffmpeg")) {
1459             snapshotIsFfmpeg();
1460         } else if (!snapshotUri.isEmpty()) {
1461             sendHttpRequest("GET", snapshotUri, null);
1462         } else if (!rtspUri.isEmpty()) {
1463             snapshotIsFfmpeg();
1464         } else {
1465             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
1466                     "Camera failed to report a valid Snaphot and/or RTSP URL. See readme on how to use the SNAPSHOT_URL_OVERRIDE feature.");
1467         }
1468     }
1469
1470     public void cameraConfigError(String reason) {
1471         // wont try to reconnect again due to a config error being the cause.
1472         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, reason);
1473         dispose();
1474     }
1475
1476     public void cameraCommunicationError(String reason) {
1477         // will try to reconnect again as camera may be rebooting.
1478         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
1479         if (isOnline) {// if already offline dont try reconnecting in 6 seconds, we want 30sec wait.
1480             resetAndRetryConnecting();
1481         }
1482     }
1483
1484     boolean streamIsStopped(String url) {
1485         ChannelTracking channelTracking = channelTrackingMap.get(url);
1486         if (channelTracking != null) {
1487             if (channelTracking.getChannel().isOpen()) {
1488                 return false; // stream is running.
1489             }
1490         }
1491         return true; // Stream stopped or never started.
1492     }
1493
1494     void snapshotRunnable() {
1495         // Snapshot should be first to keep consistent time between shots
1496         sendHttpGET(snapshotUri);
1497         if (snapCount > 0) {
1498             if (--snapCount == 0) {
1499                 setupFfmpegFormat(FFmpegFormat.GIF);
1500             }
1501         }
1502     }
1503
1504     public void stopSnapshotPolling() {
1505         Future<?> localFuture;
1506         if (!streamingSnapshotMjpeg && cameraConfig.getGifPreroll() == 0
1507                 && !cameraConfig.getUpdateImageWhen().contains("1")) {
1508             snapshotPolling = false;
1509             localFuture = snapshotJob;
1510             if (localFuture != null) {
1511                 localFuture.cancel(true);
1512             }
1513         } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // only during Motion Alarms
1514             snapshotPolling = false;
1515             localFuture = snapshotJob;
1516             if (localFuture != null) {
1517                 localFuture.cancel(true);
1518             }
1519         }
1520     }
1521
1522     public void startSnapshotPolling() {
1523         if (snapshotPolling || ffmpegSnapshotGeneration) {
1524             return; // Already polling or creating with FFmpeg from RTSP
1525         }
1526         if (streamingSnapshotMjpeg || streamingAutoFps) {
1527             snapshotPolling = true;
1528             snapshotJob = threadPool.scheduleAtFixedRate(this::snapshotRunnable, 200, cameraConfig.getPollTime(),
1529                     TimeUnit.MILLISECONDS);
1530         } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1531             snapshotPolling = true;
1532             snapshotJob = threadPool.scheduleAtFixedRate(this::snapshotRunnable, 200, cameraConfig.getPollTime(),
1533                     TimeUnit.MILLISECONDS);
1534         }
1535     }
1536
1537     // runs every 8 seconds due to mjpeg streams not staying open unless they update this often.
1538     void pollCameraRunnable() {
1539         // Snapshot should be first to keep consistent time between shots
1540         if (!snapshotUri.isEmpty()) {
1541             if (updateImageChannel) {
1542                 sendHttpGET(snapshotUri);
1543             }
1544         }
1545         if (streamingAutoFps) {
1546             updateAutoFps = true;
1547             if (!snapshotPolling && !ffmpegSnapshotGeneration) {
1548                 // Dont need to poll if creating from RTSP stream with FFmpeg or we are polling at full rate already.
1549                 sendHttpGET(snapshotUri);
1550             }
1551         }
1552         // NOTE: Use lowPriorityRequests if get request is not needed every poll.
1553         if (!lowPriorityRequests.isEmpty()) {
1554             if (lowPriorityCounter >= lowPriorityRequests.size()) {
1555                 lowPriorityCounter = 0;
1556             }
1557             sendHttpGET(lowPriorityRequests.get(lowPriorityCounter++));
1558         }
1559         // what needs to be done every poll//
1560         switch (thing.getThingTypeUID().getId()) {
1561             case GENERIC_THING:
1562                 break;
1563             case ONVIF_THING:
1564                 if (!onvifCamera.isConnected()) {
1565                     onvifCamera.connect(true);
1566                 }
1567                 break;
1568             case INSTAR_THING:
1569                 noMotionDetected(CHANNEL_MOTION_ALARM);
1570                 noMotionDetected(CHANNEL_PIR_ALARM);
1571                 noAudioDetected();
1572                 break;
1573             case HIKVISION_THING:
1574                 if (streamIsStopped("/ISAPI/Event/notification/alertStream")) {
1575                     logger.info("The alarm stream was not running for camera {}, re-starting it now",
1576                             cameraConfig.getIp());
1577                     sendHttpGET("/ISAPI/Event/notification/alertStream");
1578                 }
1579                 break;
1580             case AMCREST_THING:
1581                 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=VideoMotion");
1582                 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=AudioMutation");
1583                 break;
1584             case DAHUA_THING:
1585                 // Check for alarms, channel for NVRs appears not to work at filtering.
1586                 if (streamIsStopped("/cgi-bin/eventManager.cgi?action=attach&codes=[All]")) {
1587                     logger.info("The alarm stream was not running for camera {}, re-starting it now",
1588                             cameraConfig.getIp());
1589                     sendHttpGET("/cgi-bin/eventManager.cgi?action=attach&codes=[All]");
1590                 }
1591                 break;
1592             case DOORBIRD_THING:
1593                 // Check for alarms, channel for NVRs appears not to work at filtering.
1594                 if (streamIsStopped("/bha-api/monitor.cgi?ring=doorbell,motionsensor")) {
1595                     logger.info("The alarm stream was not running for camera {}, re-starting it now",
1596                             cameraConfig.getIp());
1597                     sendHttpGET("/bha-api/monitor.cgi?ring=doorbell,motionsensor");
1598                 }
1599                 break;
1600         }
1601         Ffmpeg localHLS = ffmpegHLS;
1602         if (localHLS != null) {
1603             localHLS.checkKeepAlive();
1604         }
1605         if (openChannels.size() > 18) {
1606             logger.debug("There are {} open Channels being tracked.", openChannels.size());
1607             cleanChannels();
1608         }
1609     }
1610
1611     @Override
1612     public void initialize() {
1613         cameraConfig = getConfigAs(CameraConfig.class);
1614         snapshotUri = getCorrectUrlFormat(cameraConfig.getSnapshotUrl());
1615         mjpegUri = getCorrectUrlFormat(cameraConfig.getMjpegUrl());
1616         rtspUri = cameraConfig.getFfmpegInput();
1617
1618         if (cameraConfig.getServerPort() < 1) {
1619             logger.warn(
1620                     "The Server Port is not set to a valid number which disables a lot of binding features. See readme for more info.");
1621         } else if (cameraConfig.getServerPort() < 1025) {
1622             logger.warn("The Server Port is <= 1024 and may cause permission errors under Linux, try a higher number.");
1623         }
1624
1625         // Known cameras will connect quicker if we skip ONVIF questions.
1626         switch (thing.getThingTypeUID().getId()) {
1627             case AMCREST_THING:
1628             case DAHUA_THING:
1629                 if (mjpegUri.isEmpty()) {
1630                     mjpegUri = "/cgi-bin/mjpg/video.cgi?channel=" + cameraConfig.getNvrChannel() + "&subtype=1";
1631                 }
1632                 if (snapshotUri.isEmpty()) {
1633                     snapshotUri = "/cgi-bin/snapshot.cgi?channel=" + cameraConfig.getNvrChannel();
1634                 }
1635                 break;
1636             case DOORBIRD_THING:
1637                 if (mjpegUri.isEmpty()) {
1638                     mjpegUri = "/bha-api/video.cgi";
1639                 }
1640                 if (snapshotUri.isEmpty()) {
1641                     snapshotUri = "/bha-api/image.cgi";
1642                 }
1643                 break;
1644             case FOSCAM_THING:
1645                 // Foscam needs any special char like spaces (%20) to be encoded for URLs.
1646                 cameraConfig.setUser(Helper.encodeSpecialChars(cameraConfig.getUser()));
1647                 cameraConfig.setPassword(Helper.encodeSpecialChars(cameraConfig.getPassword()));
1648                 if (mjpegUri.isEmpty()) {
1649                     mjpegUri = "/cgi-bin/CGIStream.cgi?cmd=GetMJStream&usr=" + cameraConfig.getUser() + "&pwd="
1650                             + cameraConfig.getPassword();
1651                 }
1652                 if (snapshotUri.isEmpty()) {
1653                     snapshotUri = "/cgi-bin/CGIProxy.fcgi?usr=" + cameraConfig.getUser() + "&pwd="
1654                             + cameraConfig.getPassword() + "&cmd=snapPicture2";
1655                 }
1656                 break;
1657             case HIKVISION_THING:// The 02 gives you the first sub stream which needs to be set to MJPEG
1658                 if (mjpegUri.isEmpty()) {
1659                     mjpegUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "02" + "/httppreview";
1660                 }
1661                 if (snapshotUri.isEmpty()) {
1662                     snapshotUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "01/picture";
1663                 }
1664                 break;
1665             case INSTAR_THING:
1666                 if (snapshotUri.isEmpty()) {
1667                     snapshotUri = "/tmpfs/snap.jpg";
1668                 }
1669                 if (mjpegUri.isEmpty()) {
1670                     mjpegUri = "/mjpegstream.cgi?-chn=12";
1671                 }
1672                 break;
1673         }
1674
1675         // Onvif and Instar event handling needs the host IP and the server started.
1676         if (cameraConfig.getServerPort() > 0) {
1677             startStreamServer();
1678         }
1679
1680         if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)) {
1681             onvifCamera = new OnvifConnection(this, cameraConfig.getIp() + ":" + cameraConfig.getOnvifPort(),
1682                     cameraConfig.getUser(), cameraConfig.getPassword());
1683             onvifCamera.setSelectedMediaProfile(cameraConfig.getOnvifMediaProfile());
1684             // Only use ONVIF events if it is not an API camera.
1685             onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1686         }
1687
1688         // for poll times above 9 seconds don't display a warning about the Image channel.
1689         if (9000 <= cameraConfig.getPollTime() && cameraConfig.getUpdateImageWhen().contains("1")) {
1690             logger.warn(
1691                     "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.");
1692         }
1693         // Waiting 3 seconds for ONVIF to discover the urls before running.
1694         cameraConnectionJob = threadPool.scheduleWithFixedDelay(this::pollingCameraConnection, 4, 30, TimeUnit.SECONDS);
1695     }
1696
1697     // What the camera needs to re-connect if the initialize() is not called.
1698     private void resetAndRetryConnecting() {
1699         dispose();
1700         initialize();
1701     }
1702
1703     @Override
1704     public void dispose() {
1705         isOnline = false;
1706         snapshotPolling = false;
1707         onvifCamera.disconnect();
1708         Future<?> localFuture = pollCameraJob;
1709         if (localFuture != null) {
1710             localFuture.cancel(true);
1711         }
1712         localFuture = snapshotJob;
1713         if (localFuture != null) {
1714             localFuture.cancel(true);
1715         }
1716         localFuture = cameraConnectionJob;
1717         if (localFuture != null) {
1718             localFuture.cancel(true);
1719         }
1720         threadPool.shutdown();
1721         threadPool = Executors.newScheduledThreadPool(4);
1722
1723         groupTracker.listOfOnlineCameraHandlers.remove(this);
1724         groupTracker.listOfOnlineCameraUID.remove(getThing().getUID().getId());
1725         // inform all group handlers that this camera has gone offline
1726         for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1727             handle.cameraOffline(this);
1728         }
1729         basicAuth = ""; // clear out stored Password hash
1730         useDigestAuth = false;
1731         stopStreamServer();
1732         openChannels.close();
1733
1734         Ffmpeg localFfmpeg = ffmpegHLS;
1735         if (localFfmpeg != null) {
1736             localFfmpeg.stopConverting();
1737         }
1738         localFfmpeg = ffmpegRecord;
1739         if (localFfmpeg != null) {
1740             localFfmpeg.stopConverting();
1741         }
1742         localFfmpeg = ffmpegGIF;
1743         if (localFfmpeg != null) {
1744             localFfmpeg.stopConverting();
1745         }
1746         localFfmpeg = ffmpegRtspHelper;
1747         if (localFfmpeg != null) {
1748             localFfmpeg.stopConverting();
1749         }
1750         localFfmpeg = ffmpegMjpeg;
1751         if (localFfmpeg != null) {
1752             localFfmpeg.stopConverting();
1753         }
1754         localFfmpeg = ffmpegSnapshot;
1755         if (localFfmpeg != null) {
1756             localFfmpeg.stopConverting();
1757         }
1758         channelTrackingMap.clear();
1759     }
1760
1761     public void setStreamServerHandler(StreamServerHandler streamServerHandler2) {
1762         streamServerHandler = streamServerHandler2;
1763     }
1764
1765     public String getWhiteList() {
1766         return cameraConfig.getIpWhitelist();
1767     }
1768
1769     @Override
1770     public Collection<Class<? extends ThingHandlerService>> getServices() {
1771         return Collections.singleton(IpCameraActions.class);
1772     }
1773 }