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