]> git.basschouten.com Git - openhab-addons.git/blob
ea43387e15c43c1a551f2d0a01e1d53308d00b61
[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             if (thing.getThingTypeUID().getId().equals(INSTAR_THING)) {
696                 logger.debug("Setting up the Alarm Server settings in the camera now");
697                 sendHttpGET(
698                         "/param.cgi?cmd=setmdalarm&-aname=server2&-switch=on&-interval=1&cmd=setalarmserverattr&-as_index=3&-as_server="
699                                 + hostIp + "&-as_port=" + cameraConfig.getServerPort()
700                                 + "&-as_path=/instar&-as_queryattr1=&-as_queryval1=&-as_queryattr2=&-as_queryval2=&-as_queryattr3=&-as_queryval3=&-as_activequery=1&-as_auth=0&-as_query1=0&-as_query2=0&-as_query3=0");
701             }
702         }
703     }
704
705     public void setupSnapshotStreaming(boolean stream, ChannelHandlerContext ctx, boolean auto) {
706         if (stream) {
707             sendMjpegFirstPacket(ctx);
708             if (auto) {
709                 autoSnapshotMjpegChannelGroup.add(ctx.channel());
710                 lockCurrentSnapshot.lock();
711                 try {
712                     sendMjpegFrame(currentSnapshot, autoSnapshotMjpegChannelGroup);
713                     // iOS uses a FIFO? and needs two frames to display a pic
714                     sendMjpegFrame(currentSnapshot, autoSnapshotMjpegChannelGroup);
715                 } finally {
716                     lockCurrentSnapshot.unlock();
717                 }
718                 streamingAutoFps = true;
719             } else {
720                 snapshotMjpegChannelGroup.add(ctx.channel());
721                 lockCurrentSnapshot.lock();
722                 try {
723                     sendMjpegFrame(currentSnapshot, snapshotMjpegChannelGroup);
724                 } finally {
725                     lockCurrentSnapshot.unlock();
726                 }
727                 streamingSnapshotMjpeg = true;
728                 startSnapshotPolling();
729             }
730         } else {
731             snapshotMjpegChannelGroup.remove(ctx.channel());
732             autoSnapshotMjpegChannelGroup.remove(ctx.channel());
733             if (streamingSnapshotMjpeg && snapshotMjpegChannelGroup.isEmpty()) {
734                 streamingSnapshotMjpeg = false;
735                 stopSnapshotPolling();
736                 logger.debug("All snapshots.mjpeg streams have stopped.");
737             } else if (streamingAutoFps && autoSnapshotMjpegChannelGroup.isEmpty()) {
738                 streamingAutoFps = false;
739                 stopSnapshotPolling();
740                 logger.debug("All autofps.mjpeg streams have stopped.");
741             }
742         }
743     }
744
745     // If start is true the CTX is added to the list to stream video to, false stops
746     // the stream.
747     public void setupMjpegStreaming(boolean start, ChannelHandlerContext ctx) {
748         if (start) {
749             if (mjpegChannelGroup.isEmpty()) {// first stream being requested.
750                 mjpegChannelGroup.add(ctx.channel());
751                 if (mjpegUri.isEmpty() || mjpegUri.equals("ffmpeg")) {
752                     sendMjpegFirstPacket(ctx);
753                     setupFfmpegFormat(FFmpegFormat.MJPEG);
754                 } else {
755                     try {
756                         // fix Dahua reboots when refreshing a mjpeg stream.
757                         TimeUnit.MILLISECONDS.sleep(500);
758                     } catch (InterruptedException e) {
759                     }
760                     sendHttpGET(mjpegUri);
761                 }
762             } else if (ffmpegMjpeg != null) {// not first stream and we will use ffmpeg
763                 sendMjpegFirstPacket(ctx);
764                 mjpegChannelGroup.add(ctx.channel());
765             } else {// not first stream and camera supplies the mjpeg source.
766                 ctx.channel().writeAndFlush(firstStreamedMsg);
767                 mjpegChannelGroup.add(ctx.channel());
768             }
769         } else {
770             mjpegChannelGroup.remove(ctx.channel());
771             if (mjpegChannelGroup.isEmpty()) {
772                 logger.debug("All ipcamera.mjpeg streams have stopped.");
773                 if (mjpegUri.equals("ffmpeg") || mjpegUri.isEmpty()) {
774                     Ffmpeg localMjpeg = ffmpegMjpeg;
775                     if (localMjpeg != null) {
776                         localMjpeg.stopConverting();
777                     }
778                 } else {
779                     closeChannel(getTinyUrl(mjpegUri));
780                 }
781             }
782         }
783     }
784
785     void closeChannel(String url) {
786         ChannelTracking channelTracking = channelTrackingMap.get(url);
787         if (channelTracking != null) {
788             if (channelTracking.getChannel().isOpen()) {
789                 channelTracking.getChannel().close();
790                 return;
791             }
792         }
793     }
794
795     /**
796      * This method should never run under normal use, if there is a bug in a camera or binding it may be possible to
797      * open large amounts of channels. This may help to keep it under control and WARN the user every 8 seconds this is
798      * still occurring.
799      */
800     void cleanChannels() {
801         for (Channel channel : openChannels) {
802             boolean oldChannel = true;
803             for (ChannelTracking channelTracking : channelTrackingMap.values()) {
804                 if (!channelTracking.getChannel().isOpen() && channelTracking.getReply().isEmpty()) {
805                     channelTrackingMap.remove(channelTracking.getRequestUrl());
806                 }
807                 if (channelTracking.getChannel() == channel) {
808                     logger.trace("Open channel to camera is used for URL:{}", channelTracking.getRequestUrl());
809                     oldChannel = false;
810                 }
811             }
812             if (oldChannel) {
813                 channel.close();
814             }
815         }
816     }
817
818     public void storeHttpReply(String url, String content) {
819         ChannelTracking channelTracking = channelTrackingMap.get(url);
820         if (channelTracking != null) {
821             channelTracking.setReply(content);
822         }
823     }
824
825     // sends direct to ctx so can be either snapshots.mjpeg or normal mjpeg stream
826     public void sendMjpegFirstPacket(ChannelHandlerContext ctx) {
827         final String boundary = "thisMjpegStream";
828         String contentType = "multipart/x-mixed-replace; boundary=" + boundary;
829         HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
830         response.headers().add(HttpHeaderNames.CONTENT_TYPE, contentType);
831         response.headers().set(HttpHeaderNames.CACHE_CONTROL, HttpHeaderValues.NO_CACHE);
832         response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
833         response.headers().add("Access-Control-Allow-Origin", "*");
834         response.headers().add("Access-Control-Expose-Headers", "*");
835         ctx.channel().writeAndFlush(response);
836     }
837
838     public void sendMjpegFrame(byte[] jpg, ChannelGroup channelGroup) {
839         final String boundary = "thisMjpegStream";
840         ByteBuf imageByteBuf = Unpooled.copiedBuffer(jpg);
841         int length = imageByteBuf.readableBytes();
842         String header = "--" + boundary + "\r\n" + "content-type: image/jpeg" + "\r\n" + "content-length: " + length
843                 + "\r\n\r\n";
844         ByteBuf headerBbuf = Unpooled.copiedBuffer(header, 0, header.length(), StandardCharsets.UTF_8);
845         ByteBuf footerBbuf = Unpooled.copiedBuffer("\r\n", 0, 2, StandardCharsets.UTF_8);
846         streamToGroup(headerBbuf, channelGroup, false);
847         streamToGroup(imageByteBuf, channelGroup, false);
848         streamToGroup(footerBbuf, channelGroup, true);
849     }
850
851     public void streamToGroup(Object msg, ChannelGroup channelGroup, boolean flush) {
852         channelGroup.write(msg);
853         if (flush) {
854             channelGroup.flush();
855         }
856     }
857
858     private void storeSnapshots() {
859         int count = 0;
860         // Need to lock as fifoSnapshotBuffer is not thread safe and new snapshots can be incoming.
861         lockCurrentSnapshot.lock();
862         try {
863             for (byte[] foo : fifoSnapshotBuffer) {
864                 File file = new File(cameraConfig.getFfmpegOutput() + "snapshot" + count + ".jpg");
865                 count++;
866                 try {
867                     OutputStream fos = new FileOutputStream(file);
868                     fos.write(foo);
869                     fos.close();
870                 } catch (FileNotFoundException e) {
871                     logger.warn("FileNotFoundException {}", e.getMessage());
872                 } catch (IOException e) {
873                     logger.warn("IOException {}", e.getMessage());
874                 }
875             }
876         } finally {
877             lockCurrentSnapshot.unlock();
878         }
879     }
880
881     public void setupFfmpegFormat(FFmpegFormat format) {
882         String inputOptions = cameraConfig.getFfmpegInputOptions();
883         if (cameraConfig.getFfmpegOutput().isEmpty()) {
884             logger.warn("The camera tried to use a FFmpeg feature when the output folder is not set.");
885             return;
886         }
887         if (rtspUri.isEmpty()) {
888             logger.warn("The camera tried to use a FFmpeg feature when no valid input for FFmpeg is provided.");
889             return;
890         }
891         if (cameraConfig.getFfmpegLocation().isEmpty()) {
892             logger.warn("The camera tried to use a FFmpeg feature when the location for FFmpeg is not known.");
893             return;
894         }
895         if (rtspUri.toLowerCase().contains("rtsp")) {
896             if (inputOptions.isEmpty()) {
897                 inputOptions = "-rtsp_transport tcp";
898             }
899         }
900
901         // Make sure the folder exists, if not create it.
902         new File(cameraConfig.getFfmpegOutput()).mkdirs();
903         switch (format) {
904             case HLS:
905                 if (ffmpegHLS == null) {
906                     if (!inputOptions.isEmpty()) {
907                         ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
908                                 "-hide_banner -loglevel warning " + inputOptions, rtspUri,
909                                 cameraConfig.getHlsOutOptions(), cameraConfig.getFfmpegOutput() + "ipcamera.m3u8",
910                                 cameraConfig.getUser(), cameraConfig.getPassword());
911                     } else {
912                         ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
913                                 "-hide_banner -loglevel warning", rtspUri, cameraConfig.getHlsOutOptions(),
914                                 cameraConfig.getFfmpegOutput() + "ipcamera.m3u8", cameraConfig.getUser(),
915                                 cameraConfig.getPassword());
916                     }
917                 }
918                 Ffmpeg localHLS = ffmpegHLS;
919                 if (localHLS != null) {
920                     localHLS.startConverting();
921                 }
922                 break;
923             case GIF:
924                 if (cameraConfig.getGifPreroll() > 0) {
925                     ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
926                             "-y -r 1 -hide_banner -loglevel warning", cameraConfig.getFfmpegOutput() + "snapshot%d.jpg",
927                             "-frames:v " + (cameraConfig.getGifPreroll() + gifRecordTime) + " "
928                                     + cameraConfig.getGifOutOptions(),
929                             cameraConfig.getFfmpegOutput() + gifFilename + ".gif", cameraConfig.getUser(),
930                             cameraConfig.getPassword());
931                 } else {
932                     if (!inputOptions.isEmpty()) {
933                         inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning " + inputOptions;
934                     } else {
935                         inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning";
936                     }
937                     ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
938                             cameraConfig.getGifOutOptions(), cameraConfig.getFfmpegOutput() + gifFilename + ".gif",
939                             cameraConfig.getUser(), cameraConfig.getPassword());
940                 }
941                 if (cameraConfig.getGifPreroll() > 0) {
942                     storeSnapshots();
943                 }
944                 Ffmpeg localGIF = ffmpegGIF;
945                 if (localGIF != null) {
946                     localGIF.startConverting();
947                     if (gifHistory.isEmpty()) {
948                         gifHistory = gifFilename;
949                     } else if (!gifFilename.equals("ipcamera")) {
950                         gifHistory = gifFilename + "," + gifHistory;
951                         if (gifHistoryLength > 49) {
952                             int endIndex = gifHistory.lastIndexOf(",");
953                             gifHistory = gifHistory.substring(0, endIndex);
954                         }
955                     }
956                     setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
957                 }
958                 break;
959             case RECORD:
960                 if (!inputOptions.isEmpty()) {
961                     inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning " + inputOptions;
962                 } else {
963                     inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning";
964                 }
965                 ffmpegRecord = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
966                         cameraConfig.getMp4OutOptions(), cameraConfig.getFfmpegOutput() + mp4Filename + ".mp4",
967                         cameraConfig.getUser(), cameraConfig.getPassword());
968                 Ffmpeg localRecord = ffmpegRecord;
969                 if (localRecord != null) {
970                     localRecord.startConverting();
971                     if (mp4History.isEmpty()) {
972                         mp4History = mp4Filename;
973                     } else if (!mp4Filename.equals("ipcamera")) {
974                         mp4History = mp4Filename + "," + mp4History;
975                         if (mp4HistoryLength > 49) {
976                             int endIndex = mp4History.lastIndexOf(",");
977                             mp4History = mp4History.substring(0, endIndex);
978                         }
979                     }
980                 }
981                 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
982                 break;
983             case RTSP_ALARMS:
984                 Ffmpeg localAlarms = ffmpegRtspHelper;
985                 if (localAlarms != null) {
986                     localAlarms.stopConverting();
987                     if (!audioAlarmEnabled && !motionAlarmEnabled) {
988                         return;
989                     }
990                 }
991                 String input = (cameraConfig.getAlarmInputUrl().isEmpty()) ? rtspUri : cameraConfig.getAlarmInputUrl();
992                 String filterOptions = "";
993                 if (!audioAlarmEnabled) {
994                     filterOptions = "-an";
995                 } else {
996                     filterOptions = "-af silencedetect=n=-" + audioThreshold + "dB:d=2";
997                 }
998                 if (!motionAlarmEnabled && !ffmpegSnapshotGeneration) {
999                     filterOptions = filterOptions.concat(" -vn");
1000                 } else if (motionAlarmEnabled && !cameraConfig.getMotionOptions().isEmpty()) {
1001                     String usersMotionOptions = cameraConfig.getMotionOptions();
1002                     if (usersMotionOptions.startsWith("-")) {
1003                         // Need to put the users custom options first in the chain before the motion is detected
1004                         filterOptions += " " + usersMotionOptions + ",select='gte(scene," + motionThreshold
1005                                 + ")',metadata=print";
1006                     } else {
1007                         filterOptions = filterOptions + " " + usersMotionOptions + " -vf select='gte(scene,"
1008                                 + motionThreshold + ")',metadata=print";
1009                     }
1010                 } else if (motionAlarmEnabled) {
1011                     filterOptions = filterOptions
1012                             .concat(" -vf select='gte(scene," + motionThreshold + ")',metadata=print");
1013                 }
1014                 ffmpegRtspHelper = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, input,
1015                         filterOptions, "-f null -", cameraConfig.getUser(), cameraConfig.getPassword());
1016                 localAlarms = ffmpegRtspHelper;
1017                 if (localAlarms != null) {
1018                     localAlarms.startConverting();
1019                 }
1020                 break;
1021             case MJPEG:
1022                 if (ffmpegMjpeg == null) {
1023                     if (inputOptions.isEmpty()) {
1024                         inputOptions = "-hide_banner -loglevel warning";
1025                     } else {
1026                         inputOptions += " -hide_banner -loglevel warning";
1027                     }
1028                     ffmpegMjpeg = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
1029                             cameraConfig.getMjpegOptions(),
1030                             "http://127.0.0.1:" + cameraConfig.getServerPort() + "/ipcamera.jpg",
1031                             cameraConfig.getUser(), cameraConfig.getPassword());
1032                 }
1033                 Ffmpeg localMjpeg = ffmpegMjpeg;
1034                 if (localMjpeg != null) {
1035                     localMjpeg.startConverting();
1036                 }
1037                 break;
1038             case SNAPSHOT:
1039                 // if mjpeg stream you can use 'ffmpeg -i input -codec:v copy -bsf:v mjpeg2jpeg output.jpg'
1040                 if (ffmpegSnapshot == null) {
1041                     if (inputOptions.isEmpty()) {
1042                         // iFrames only
1043                         inputOptions = "-threads 1 -skip_frame nokey -hide_banner -loglevel warning";
1044                     } else {
1045                         inputOptions += " -threads 1 -skip_frame nokey -hide_banner -loglevel warning";
1046                     }
1047                     ffmpegSnapshot = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
1048                             cameraConfig.getSnapshotOptions(),
1049                             "http://127.0.0.1:" + cameraConfig.getServerPort() + "/snapshot.jpg",
1050                             cameraConfig.getUser(), cameraConfig.getPassword());
1051                 }
1052                 Ffmpeg localSnaps = ffmpegSnapshot;
1053                 if (localSnaps != null) {
1054                     localSnaps.startConverting();
1055                 }
1056                 break;
1057         }
1058     }
1059
1060     public void noMotionDetected(String thisAlarmsChannel) {
1061         setChannelState(thisAlarmsChannel, OnOffType.OFF);
1062         firstMotionAlarm = false;
1063         motionAlarmUpdateSnapshot = false;
1064         motionDetected = false;
1065         if (streamingAutoFps) {
1066             stopSnapshotPolling();
1067         } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1068             stopSnapshotPolling();
1069         }
1070     }
1071
1072     /**
1073      * The {@link changeAlarmState} To only be used to change alarms channels that are not counted as motion. This will
1074      * allow logic to be added here in the future. Example more than 1 type of alarm may indicate that someone is
1075      * tampering with the camera.
1076      */
1077     public void changeAlarmState(String thisAlarmsChannel, OnOffType state) {
1078         updateState(thisAlarmsChannel, state);
1079     }
1080
1081     public void motionDetected(String thisAlarmsChannel) {
1082         updateState(CHANNEL_LAST_MOTION_TYPE, new StringType(thisAlarmsChannel));
1083         updateState(thisAlarmsChannel, OnOffType.ON);
1084         motionDetected = true;
1085         if (streamingAutoFps) {
1086             startSnapshotPolling();
1087         }
1088         if (cameraConfig.getUpdateImageWhen().contains("2")) {
1089             if (!firstMotionAlarm) {
1090                 if (!snapshotUri.isEmpty()) {
1091                     sendHttpGET(snapshotUri);
1092                 }
1093                 firstMotionAlarm = true;// reset back to false when the jpg arrives.
1094             }
1095         } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1096             if (!snapshotPolling) {
1097                 startSnapshotPolling();
1098             }
1099             firstMotionAlarm = true;
1100             motionAlarmUpdateSnapshot = true;
1101         }
1102     }
1103
1104     public void audioDetected() {
1105         updateState(CHANNEL_AUDIO_ALARM, OnOffType.ON);
1106         if (cameraConfig.getUpdateImageWhen().contains("3")) {
1107             if (!firstAudioAlarm) {
1108                 if (!snapshotUri.isEmpty()) {
1109                     sendHttpGET(snapshotUri);
1110                 }
1111                 firstAudioAlarm = true;// reset back to false when the jpg arrives.
1112             }
1113         } else if (cameraConfig.getUpdateImageWhen().contains("5")) {// During audio alarms
1114             firstAudioAlarm = true;
1115             audioAlarmUpdateSnapshot = true;
1116         }
1117     }
1118
1119     public void noAudioDetected() {
1120         setChannelState(CHANNEL_AUDIO_ALARM, OnOffType.OFF);
1121         firstAudioAlarm = false;
1122         audioAlarmUpdateSnapshot = false;
1123     }
1124
1125     public void recordMp4(String filename, int seconds) {
1126         mp4Filename = filename;
1127         mp4RecordTime = seconds;
1128         setupFfmpegFormat(FFmpegFormat.RECORD);
1129         setChannelState(CHANNEL_RECORDING_MP4, DecimalType.valueOf(new String("" + seconds)));
1130     }
1131
1132     public void recordGif(String filename, int seconds) {
1133         gifFilename = filename;
1134         gifRecordTime = seconds;
1135         if (cameraConfig.getGifPreroll() > 0) {
1136             snapCount = seconds;
1137         } else {
1138             setupFfmpegFormat(FFmpegFormat.GIF);
1139         }
1140         setChannelState(CHANNEL_RECORDING_GIF, DecimalType.valueOf(new String("" + seconds)));
1141     }
1142
1143     public String returnValueFromString(String rawString, String searchedString) {
1144         String result = "";
1145         int index = rawString.indexOf(searchedString);
1146         if (index != -1) // -1 means "not found"
1147         {
1148             result = rawString.substring(index + searchedString.length(), rawString.length());
1149             index = result.indexOf("\r\n"); // find a carriage return to find the end of the value.
1150             if (index == -1) {
1151                 return result; // Did not find a carriage return.
1152             } else {
1153                 return result.substring(0, index);
1154             }
1155         }
1156         return ""; // Did not find the String we were searching for
1157     }
1158
1159     private void sendPTZRequest() {
1160         onvifCamera.sendPTZRequest(OnvifConnection.RequestType.AbsoluteMove);
1161     }
1162
1163     @Override
1164     public void handleCommand(ChannelUID channelUID, Command command) {
1165         if (command instanceof RefreshType) {
1166             switch (channelUID.getId()) {
1167                 case CHANNEL_PAN:
1168                     if (onvifCamera.supportsPTZ()) {
1169                         updateState(CHANNEL_PAN, new PercentType(Math.round(onvifCamera.getAbsolutePan())));
1170                     }
1171                     return;
1172                 case CHANNEL_TILT:
1173                     if (onvifCamera.supportsPTZ()) {
1174                         updateState(CHANNEL_TILT, new PercentType(Math.round(onvifCamera.getAbsoluteTilt())));
1175                     }
1176                     return;
1177                 case CHANNEL_ZOOM:
1178                     if (onvifCamera.supportsPTZ()) {
1179                         updateState(CHANNEL_ZOOM, new PercentType(Math.round(onvifCamera.getAbsoluteZoom())));
1180                     }
1181                     return;
1182                 case CHANNEL_GOTO_PRESET:
1183                     if (onvifCamera.supportsPTZ()) {
1184                         onvifCamera.sendPTZRequest(OnvifConnection.RequestType.GetPresets);
1185                     }
1186                     return;
1187             }
1188         } // caution "REFRESH" can still progress to brand Handlers below the else.
1189         else {
1190             switch (channelUID.getId()) {
1191                 case CHANNEL_MP4_HISTORY_LENGTH:
1192                     if (DecimalType.ZERO.equals(command)) {
1193                         mp4HistoryLength = 0;
1194                         mp4History = "";
1195                         setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
1196                     }
1197                     return;
1198                 case CHANNEL_GIF_HISTORY_LENGTH:
1199                     if (DecimalType.ZERO.equals(command)) {
1200                         gifHistoryLength = 0;
1201                         gifHistory = "";
1202                         setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
1203                     }
1204                     return;
1205                 case CHANNEL_FFMPEG_MOTION_CONTROL:
1206                     if (OnOffType.ON.equals(command)) {
1207                         motionAlarmEnabled = true;
1208                     } else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
1209                         motionAlarmEnabled = false;
1210                         noMotionDetected(CHANNEL_FFMPEG_MOTION_ALARM);
1211                     } else {
1212                         motionAlarmEnabled = true;
1213                         motionThreshold = Double.valueOf(command.toString());
1214                         motionThreshold = motionThreshold / 10000;
1215                     }
1216                     setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
1217                     return;
1218                 case CHANNEL_START_STREAM:
1219                     Ffmpeg localHLS;
1220                     if (OnOffType.ON.equals(command)) {
1221                         localHLS = ffmpegHLS;
1222                         if (localHLS == null) {
1223                             setupFfmpegFormat(FFmpegFormat.HLS);
1224                             localHLS = ffmpegHLS;
1225                         }
1226                         if (localHLS != null) {
1227                             localHLS.setKeepAlive(-1);// Now will run till manually stopped.
1228                             localHLS.startConverting();
1229                         }
1230                     } else {
1231                         localHLS = ffmpegHLS;
1232                         if (localHLS != null) {
1233                             // Still runs but will be able to auto stop when the HLS stream is no longer used.
1234                             localHLS.setKeepAlive(1);
1235                         }
1236                     }
1237                     return;
1238                 case CHANNEL_EXTERNAL_MOTION:
1239                     if (OnOffType.ON.equals(command)) {
1240                         motionDetected(CHANNEL_EXTERNAL_MOTION);
1241                     } else {
1242                         noMotionDetected(CHANNEL_EXTERNAL_MOTION);
1243                     }
1244                     return;
1245                 case CHANNEL_GOTO_PRESET:
1246                     if (onvifCamera.supportsPTZ()) {
1247                         onvifCamera.gotoPreset(Integer.valueOf(command.toString()));
1248                     }
1249                     return;
1250                 case CHANNEL_POLL_IMAGE:
1251                     if (OnOffType.ON.equals(command)) {
1252                         if (snapshotUri.isEmpty()) {
1253                             ffmpegSnapshotGeneration = true;
1254                             setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1255                             updateImageChannel = false;
1256                         } else {
1257                             updateImageChannel = true;
1258                             sendHttpGET(snapshotUri);// Allows this to change Image FPS on demand
1259                         }
1260                     } else {
1261                         Ffmpeg localSnaps = ffmpegSnapshot;
1262                         if (localSnaps != null) {
1263                             localSnaps.stopConverting();
1264                             ffmpegSnapshotGeneration = false;
1265                         }
1266                         updateImageChannel = false;
1267                     }
1268                     return;
1269                 case CHANNEL_PAN:
1270                     if (onvifCamera.supportsPTZ()) {
1271                         if (command instanceof IncreaseDecreaseType) {
1272                             if (command == IncreaseDecreaseType.INCREASE) {
1273                                 if (cameraConfig.getPtzContinuous()) {
1274                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveLeft);
1275                                 } else {
1276                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveLeft);
1277                                 }
1278                             } else {
1279                                 if (cameraConfig.getPtzContinuous()) {
1280                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveRight);
1281                                 } else {
1282                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveRight);
1283                                 }
1284                             }
1285                             return;
1286                         } else if (OnOffType.OFF.equals(command)) {
1287                             onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1288                             return;
1289                         }
1290                         onvifCamera.setAbsolutePan(Float.valueOf(command.toString()));
1291                         threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1292                     }
1293                     return;
1294                 case CHANNEL_TILT:
1295                     if (onvifCamera.supportsPTZ()) {
1296                         if (command instanceof IncreaseDecreaseType) {
1297                             if (IncreaseDecreaseType.INCREASE.equals(command)) {
1298                                 if (cameraConfig.getPtzContinuous()) {
1299                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveUp);
1300                                 } else {
1301                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveUp);
1302                                 }
1303                             } else {
1304                                 if (cameraConfig.getPtzContinuous()) {
1305                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveDown);
1306                                 } else {
1307                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveDown);
1308                                 }
1309                             }
1310                             return;
1311                         } else if (OnOffType.OFF.equals(command)) {
1312                             onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1313                             return;
1314                         }
1315                         onvifCamera.setAbsoluteTilt(Float.valueOf(command.toString()));
1316                         threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1317                     }
1318                     return;
1319                 case CHANNEL_ZOOM:
1320                     if (onvifCamera.supportsPTZ()) {
1321                         if (command instanceof IncreaseDecreaseType) {
1322                             if (IncreaseDecreaseType.INCREASE.equals(command)) {
1323                                 if (cameraConfig.getPtzContinuous()) {
1324                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveIn);
1325                                 } else {
1326                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveIn);
1327                                 }
1328                             } else {
1329                                 if (cameraConfig.getPtzContinuous()) {
1330                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveOut);
1331                                 } else {
1332                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveOut);
1333                                 }
1334                             }
1335                             return;
1336                         } else if (OnOffType.OFF.equals(command)) {
1337                             onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1338                             return;
1339                         }
1340                         onvifCamera.setAbsoluteZoom(Float.valueOf(command.toString()));
1341                         threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1342                     }
1343                     return;
1344             }
1345         }
1346         // commands and refresh now get passed to brand handlers
1347         switch (thing.getThingTypeUID().getId()) {
1348             case AMCREST_THING:
1349                 AmcrestHandler amcrestHandler = new AmcrestHandler(getHandle());
1350                 amcrestHandler.handleCommand(channelUID, command);
1351                 if (lowPriorityRequests.isEmpty()) {
1352                     lowPriorityRequests = amcrestHandler.getLowPriorityRequests();
1353                 }
1354                 break;
1355             case DAHUA_THING:
1356                 DahuaHandler dahuaHandler = new DahuaHandler(getHandle(), cameraConfig.getNvrChannel());
1357                 dahuaHandler.handleCommand(channelUID, command);
1358                 if (lowPriorityRequests.isEmpty()) {
1359                     lowPriorityRequests = dahuaHandler.getLowPriorityRequests();
1360                 }
1361                 break;
1362             case DOORBIRD_THING:
1363                 DoorBirdHandler doorBirdHandler = new DoorBirdHandler(getHandle());
1364                 doorBirdHandler.handleCommand(channelUID, command);
1365                 if (lowPriorityRequests.isEmpty()) {
1366                     lowPriorityRequests = doorBirdHandler.getLowPriorityRequests();
1367                 }
1368                 break;
1369             case HIKVISION_THING:
1370                 HikvisionHandler hikvisionHandler = new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel());
1371                 hikvisionHandler.handleCommand(channelUID, command);
1372                 if (lowPriorityRequests.isEmpty()) {
1373                     lowPriorityRequests = hikvisionHandler.getLowPriorityRequests();
1374                 }
1375                 break;
1376             case FOSCAM_THING:
1377                 FoscamHandler foscamHandler = new FoscamHandler(getHandle(), cameraConfig.getUser(),
1378                         cameraConfig.getPassword());
1379                 foscamHandler.handleCommand(channelUID, command);
1380                 if (lowPriorityRequests.isEmpty()) {
1381                     lowPriorityRequests = foscamHandler.getLowPriorityRequests();
1382                 }
1383                 break;
1384             case INSTAR_THING:
1385                 InstarHandler instarHandler = new InstarHandler(getHandle());
1386                 instarHandler.handleCommand(channelUID, command);
1387                 if (lowPriorityRequests.isEmpty()) {
1388                     lowPriorityRequests = instarHandler.getLowPriorityRequests();
1389                 }
1390                 break;
1391             default:
1392                 HttpOnlyHandler defaultHandler = new HttpOnlyHandler(getHandle());
1393                 defaultHandler.handleCommand(channelUID, command);
1394                 if (lowPriorityRequests.isEmpty()) {
1395                     lowPriorityRequests = defaultHandler.getLowPriorityRequests();
1396                 }
1397                 break;
1398         }
1399     }
1400
1401     public void setChannelState(String channelToUpdate, State valueOf) {
1402         updateState(channelToUpdate, valueOf);
1403     }
1404
1405     void bringCameraOnline() {
1406         isOnline = true;
1407         updateStatus(ThingStatus.ONLINE);
1408         groupTracker.listOfOnlineCameraHandlers.add(this);
1409         groupTracker.listOfOnlineCameraUID.add(getThing().getUID().getId());
1410         Future<?> localFuture = cameraConnectionJob;
1411         if (localFuture != null) {
1412             localFuture.cancel(false);
1413         }
1414
1415         if (cameraConfig.getGifPreroll() > 0 || cameraConfig.getUpdateImageWhen().contains("1")) {
1416             snapshotPolling = true;
1417             snapshotJob = threadPool.scheduleAtFixedRate(this::snapshotRunnable, 1000, cameraConfig.getPollTime(),
1418                     TimeUnit.MILLISECONDS);
1419         }
1420
1421         pollCameraJob = threadPool.scheduleWithFixedDelay(this::pollCameraRunnable, 1000, 8000, TimeUnit.MILLISECONDS);
1422
1423         if (!rtspUri.isEmpty()) {
1424             updateState(CHANNEL_RTSP_URL, new StringType(rtspUri));
1425         }
1426         if (updateImageChannel) {
1427             updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1428         } else {
1429             updateState(CHANNEL_POLL_IMAGE, OnOffType.OFF);
1430         }
1431         if (!groupTracker.listOfGroupHandlers.isEmpty()) {
1432             for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1433                 handle.cameraOnline(getThing().getUID().getId());
1434             }
1435         }
1436     }
1437
1438     void snapshotIsFfmpeg() {
1439         bringCameraOnline();
1440         snapshotUri = "";// ffmpeg is a valid option. Simplify further checks.
1441         logger.debug(
1442                 "Binding has no snapshot url. Will use your CPU and FFmpeg to create snapshots from the cameras RTSP.");
1443         if (!rtspUri.isEmpty()) {
1444             updateImageChannel = false;
1445             ffmpegSnapshotGeneration = true;
1446             setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1447             updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1448         } else {
1449             cameraConfigError("Binding can not find a RTSP url for this camera, please provide a FFmpeg Input URL.");
1450         }
1451     }
1452
1453     void pollingCameraConnection() {
1454         if (thing.getThingTypeUID().getId().equals(GENERIC_THING)) {
1455             if (rtspUri.isEmpty()) {
1456                 logger.warn("Binding has not been supplied with a FFmpeg Input URL, so some features will not work.");
1457             }
1458             if (snapshotUri.isEmpty() || snapshotUri.equals("ffmpeg")) {
1459                 snapshotIsFfmpeg();
1460             } else {
1461                 sendHttpRequest("GET", snapshotUri, null);
1462             }
1463             return;
1464         }
1465         if (!onvifCamera.isConnected()) {
1466             logger.debug("About to connect to the IP Camera using the ONVIF PORT at IP:{}:{}", cameraConfig.getIp(),
1467                     cameraConfig.getOnvifPort());
1468             onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1469         }
1470         if (snapshotUri.equals("ffmpeg")) {
1471             snapshotIsFfmpeg();
1472         } else if (!snapshotUri.isEmpty()) {
1473             sendHttpRequest("GET", snapshotUri, null);
1474         } else if (!rtspUri.isEmpty()) {
1475             snapshotIsFfmpeg();
1476         } else {
1477             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
1478                     "Camera failed to report a valid Snaphot and/or RTSP URL. See readme on how to use the SNAPSHOT_URL_OVERRIDE feature.");
1479         }
1480     }
1481
1482     public void cameraConfigError(String reason) {
1483         // wont try to reconnect again due to a config error being the cause.
1484         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, reason);
1485         dispose();
1486     }
1487
1488     public void cameraCommunicationError(String reason) {
1489         // will try to reconnect again as camera may be rebooting.
1490         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
1491         if (isOnline) {// if already offline dont try reconnecting in 6 seconds, we want 30sec wait.
1492             resetAndRetryConnecting();
1493         }
1494     }
1495
1496     boolean streamIsStopped(String url) {
1497         ChannelTracking channelTracking = channelTrackingMap.get(url);
1498         if (channelTracking != null) {
1499             if (channelTracking.getChannel().isActive()) {
1500                 return false; // stream is running.
1501             }
1502         }
1503         return true; // Stream stopped or never started.
1504     }
1505
1506     void snapshotRunnable() {
1507         // Snapshot should be first to keep consistent time between shots
1508         sendHttpGET(snapshotUri);
1509         if (snapCount > 0) {
1510             if (--snapCount == 0) {
1511                 setupFfmpegFormat(FFmpegFormat.GIF);
1512             }
1513         }
1514     }
1515
1516     public void stopSnapshotPolling() {
1517         Future<?> localFuture;
1518         if (!streamingSnapshotMjpeg && cameraConfig.getGifPreroll() == 0
1519                 && !cameraConfig.getUpdateImageWhen().contains("1")) {
1520             snapshotPolling = false;
1521             localFuture = snapshotJob;
1522             if (localFuture != null) {
1523                 localFuture.cancel(true);
1524             }
1525         } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // only during Motion Alarms
1526             snapshotPolling = false;
1527             localFuture = snapshotJob;
1528             if (localFuture != null) {
1529                 localFuture.cancel(true);
1530             }
1531         }
1532     }
1533
1534     public void startSnapshotPolling() {
1535         if (snapshotPolling || ffmpegSnapshotGeneration) {
1536             return; // Already polling or creating with FFmpeg from RTSP
1537         }
1538         if (streamingSnapshotMjpeg || streamingAutoFps) {
1539             snapshotPolling = true;
1540             snapshotJob = threadPool.scheduleAtFixedRate(this::snapshotRunnable, 200, cameraConfig.getPollTime(),
1541                     TimeUnit.MILLISECONDS);
1542         } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1543             snapshotPolling = true;
1544             snapshotJob = threadPool.scheduleAtFixedRate(this::snapshotRunnable, 200, cameraConfig.getPollTime(),
1545                     TimeUnit.MILLISECONDS);
1546         }
1547     }
1548
1549     /**
1550      * {@link pollCameraRunnable} Polls every 8 seconds, to check camera is still ONLINE and keep mjpeg and alarm
1551      * streams open and more.
1552      *
1553      */
1554     void pollCameraRunnable() {
1555         // Snapshot should be first to keep consistent time between shots
1556         if (streamingAutoFps) {
1557             updateAutoFps = true;
1558             if (!snapshotPolling && !ffmpegSnapshotGeneration) {
1559                 // Dont need to poll if creating from RTSP stream with FFmpeg or we are polling at full rate already.
1560                 sendHttpGET(snapshotUri);
1561             }
1562         } else if (!snapshotUri.isEmpty() && !snapshotPolling) {// we need to check camera is still online.
1563             sendHttpGET(snapshotUri);
1564         }
1565         // NOTE: Use lowPriorityRequests if get request is not needed every poll.
1566         if (!lowPriorityRequests.isEmpty()) {
1567             if (lowPriorityCounter >= lowPriorityRequests.size()) {
1568                 lowPriorityCounter = 0;
1569             }
1570             sendHttpGET(lowPriorityRequests.get(lowPriorityCounter++));
1571         }
1572         // what needs to be done every poll//
1573         switch (thing.getThingTypeUID().getId()) {
1574             case GENERIC_THING:
1575                 break;
1576             case ONVIF_THING:
1577                 if (!onvifCamera.isConnected()) {
1578                     onvifCamera.connect(true);
1579                 }
1580                 break;
1581             case INSTAR_THING:
1582                 noMotionDetected(CHANNEL_MOTION_ALARM);
1583                 noMotionDetected(CHANNEL_PIR_ALARM);
1584                 noAudioDetected();
1585                 break;
1586             case HIKVISION_THING:
1587                 if (streamIsStopped("/ISAPI/Event/notification/alertStream")) {
1588                     logger.info("The alarm stream was not running for camera {}, re-starting it now",
1589                             cameraConfig.getIp());
1590                     sendHttpGET("/ISAPI/Event/notification/alertStream");
1591                 }
1592                 break;
1593             case AMCREST_THING:
1594                 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=VideoMotion");
1595                 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=AudioMutation");
1596                 break;
1597             case DAHUA_THING:
1598                 // Check for alarms, channel for NVRs appears not to work at filtering.
1599                 if (streamIsStopped("/cgi-bin/eventManager.cgi?action=attach&codes=[All]")) {
1600                     logger.info("The alarm stream was not running for camera {}, re-starting it now",
1601                             cameraConfig.getIp());
1602                     sendHttpGET("/cgi-bin/eventManager.cgi?action=attach&codes=[All]");
1603                 }
1604                 break;
1605             case DOORBIRD_THING:
1606                 // Check for alarms, channel for NVRs appears not to work at filtering.
1607                 if (streamIsStopped("/bha-api/monitor.cgi?ring=doorbell,motionsensor")) {
1608                     logger.info("The alarm stream was not running for camera {}, re-starting it now",
1609                             cameraConfig.getIp());
1610                     sendHttpGET("/bha-api/monitor.cgi?ring=doorbell,motionsensor");
1611                 }
1612                 break;
1613         }
1614         Ffmpeg localHLS = ffmpegHLS;
1615         if (localHLS != null) {
1616             localHLS.checkKeepAlive();
1617         }
1618         if (openChannels.size() > 18) {
1619             logger.debug("There are {} open Channels being tracked.", openChannels.size());
1620             cleanChannels();
1621         }
1622     }
1623
1624     @Override
1625     public void initialize() {
1626         cameraConfig = getConfigAs(CameraConfig.class);
1627         snapshotUri = getCorrectUrlFormat(cameraConfig.getSnapshotUrl());
1628         mjpegUri = getCorrectUrlFormat(cameraConfig.getMjpegUrl());
1629         rtspUri = cameraConfig.getFfmpegInput();
1630
1631         if (cameraConfig.getServerPort() < 1) {
1632             logger.warn(
1633                     "The Server Port is not set to a valid number which disables a lot of binding features. See readme for more info.");
1634         } else if (cameraConfig.getServerPort() < 1025) {
1635             logger.warn("The Server Port is <= 1024 and may cause permission errors under Linux, try a higher number.");
1636         }
1637
1638         // Known cameras will connect quicker if we skip ONVIF questions.
1639         switch (thing.getThingTypeUID().getId()) {
1640             case AMCREST_THING:
1641             case DAHUA_THING:
1642                 if (mjpegUri.isEmpty()) {
1643                     mjpegUri = "/cgi-bin/mjpg/video.cgi?channel=" + cameraConfig.getNvrChannel() + "&subtype=1";
1644                 }
1645                 if (snapshotUri.isEmpty()) {
1646                     snapshotUri = "/cgi-bin/snapshot.cgi?channel=" + cameraConfig.getNvrChannel();
1647                 }
1648                 break;
1649             case DOORBIRD_THING:
1650                 if (mjpegUri.isEmpty()) {
1651                     mjpegUri = "/bha-api/video.cgi";
1652                 }
1653                 if (snapshotUri.isEmpty()) {
1654                     snapshotUri = "/bha-api/image.cgi";
1655                 }
1656                 break;
1657             case FOSCAM_THING:
1658                 // Foscam needs any special char like spaces (%20) to be encoded for URLs.
1659                 cameraConfig.setUser(Helper.encodeSpecialChars(cameraConfig.getUser()));
1660                 cameraConfig.setPassword(Helper.encodeSpecialChars(cameraConfig.getPassword()));
1661                 if (mjpegUri.isEmpty()) {
1662                     mjpegUri = "/cgi-bin/CGIStream.cgi?cmd=GetMJStream&usr=" + cameraConfig.getUser() + "&pwd="
1663                             + cameraConfig.getPassword();
1664                 }
1665                 if (snapshotUri.isEmpty()) {
1666                     snapshotUri = "/cgi-bin/CGIProxy.fcgi?usr=" + cameraConfig.getUser() + "&pwd="
1667                             + cameraConfig.getPassword() + "&cmd=snapPicture2";
1668                 }
1669                 break;
1670             case HIKVISION_THING:// The 02 gives you the first sub stream which needs to be set to MJPEG
1671                 if (mjpegUri.isEmpty()) {
1672                     mjpegUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "02" + "/httppreview";
1673                 }
1674                 if (snapshotUri.isEmpty()) {
1675                     snapshotUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "01/picture";
1676                 }
1677                 break;
1678             case INSTAR_THING:
1679                 if (snapshotUri.isEmpty()) {
1680                     snapshotUri = "/tmpfs/snap.jpg";
1681                 }
1682                 if (mjpegUri.isEmpty()) {
1683                     mjpegUri = "/mjpegstream.cgi?-chn=12";
1684                 }
1685                 break;
1686         }
1687
1688         // Onvif and Instar event handling needs the host IP and the server started.
1689         if (cameraConfig.getServerPort() > 0) {
1690             startStreamServer();
1691         }
1692
1693         if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)) {
1694             onvifCamera = new OnvifConnection(this, cameraConfig.getIp() + ":" + cameraConfig.getOnvifPort(),
1695                     cameraConfig.getUser(), cameraConfig.getPassword());
1696             onvifCamera.setSelectedMediaProfile(cameraConfig.getOnvifMediaProfile());
1697             // Only use ONVIF events if it is not an API camera.
1698             onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1699         }
1700
1701         // for poll times above 9 seconds don't display a warning about the Image channel.
1702         if (9000 <= cameraConfig.getPollTime() && cameraConfig.getUpdateImageWhen().contains("1")) {
1703             logger.warn(
1704                     "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.");
1705         }
1706         // Waiting 3 seconds for ONVIF to discover the urls before running.
1707         cameraConnectionJob = threadPool.scheduleWithFixedDelay(this::pollingCameraConnection, 4, 30, TimeUnit.SECONDS);
1708     }
1709
1710     // What the camera needs to re-connect if the initialize() is not called.
1711     private void resetAndRetryConnecting() {
1712         dispose();
1713         initialize();
1714     }
1715
1716     @Override
1717     public void dispose() {
1718         isOnline = false;
1719         snapshotPolling = false;
1720         onvifCamera.disconnect();
1721         Future<?> localFuture = pollCameraJob;
1722         if (localFuture != null) {
1723             localFuture.cancel(true);
1724         }
1725         localFuture = snapshotJob;
1726         if (localFuture != null) {
1727             localFuture.cancel(true);
1728         }
1729         localFuture = cameraConnectionJob;
1730         if (localFuture != null) {
1731             localFuture.cancel(true);
1732         }
1733         threadPool.shutdown();
1734         threadPool = Executors.newScheduledThreadPool(4);
1735
1736         groupTracker.listOfOnlineCameraHandlers.remove(this);
1737         groupTracker.listOfOnlineCameraUID.remove(getThing().getUID().getId());
1738         // inform all group handlers that this camera has gone offline
1739         for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1740             handle.cameraOffline(this);
1741         }
1742         basicAuth = ""; // clear out stored Password hash
1743         useDigestAuth = false;
1744         stopStreamServer();
1745         openChannels.close();
1746
1747         Ffmpeg localFfmpeg = ffmpegHLS;
1748         if (localFfmpeg != null) {
1749             localFfmpeg.stopConverting();
1750         }
1751         localFfmpeg = ffmpegRecord;
1752         if (localFfmpeg != null) {
1753             localFfmpeg.stopConverting();
1754         }
1755         localFfmpeg = ffmpegGIF;
1756         if (localFfmpeg != null) {
1757             localFfmpeg.stopConverting();
1758         }
1759         localFfmpeg = ffmpegRtspHelper;
1760         if (localFfmpeg != null) {
1761             localFfmpeg.stopConverting();
1762         }
1763         localFfmpeg = ffmpegMjpeg;
1764         if (localFfmpeg != null) {
1765             localFfmpeg.stopConverting();
1766         }
1767         localFfmpeg = ffmpegSnapshot;
1768         if (localFfmpeg != null) {
1769             localFfmpeg.stopConverting();
1770         }
1771         channelTrackingMap.clear();
1772     }
1773
1774     public void setStreamServerHandler(StreamServerHandler streamServerHandler2) {
1775         streamServerHandler = streamServerHandler2;
1776     }
1777
1778     public String getWhiteList() {
1779         return cameraConfig.getIpWhitelist();
1780     }
1781
1782     @Override
1783     public Collection<Class<? extends ThingHandlerService>> getServices() {
1784         return Collections.singleton(IpCameraActions.class);
1785     }
1786 }