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