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