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