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