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