]> git.basschouten.com Git - openhab-addons.git/blob
5dd9481ecdc82753fa8146473d2e0c532e3eaa8d
[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.net.InetSocketAddress;
23 import java.net.MalformedURLException;
24 import java.net.URL;
25 import java.nio.charset.StandardCharsets;
26 import java.util.ArrayList;
27 import java.util.Collection;
28 import java.util.Collections;
29 import java.util.LinkedList;
30 import java.util.List;
31 import java.util.Map;
32 import java.util.concurrent.ConcurrentHashMap;
33 import java.util.concurrent.Executors;
34 import java.util.concurrent.Future;
35 import java.util.concurrent.ScheduledExecutorService;
36 import java.util.concurrent.ScheduledFuture;
37 import java.util.concurrent.TimeUnit;
38 import java.util.concurrent.locks.ReentrantLock;
39
40 import org.eclipse.jdt.annotation.NonNullByDefault;
41 import org.eclipse.jdt.annotation.Nullable;
42 import org.openhab.binding.ipcamera.internal.AmcrestHandler;
43 import org.openhab.binding.ipcamera.internal.CameraConfig;
44 import org.openhab.binding.ipcamera.internal.ChannelTracking;
45 import org.openhab.binding.ipcamera.internal.DahuaHandler;
46 import org.openhab.binding.ipcamera.internal.DoorBirdHandler;
47 import org.openhab.binding.ipcamera.internal.Ffmpeg;
48 import org.openhab.binding.ipcamera.internal.FoscamHandler;
49 import org.openhab.binding.ipcamera.internal.GroupTracker;
50 import org.openhab.binding.ipcamera.internal.Helper;
51 import org.openhab.binding.ipcamera.internal.HikvisionHandler;
52 import org.openhab.binding.ipcamera.internal.HttpOnlyHandler;
53 import org.openhab.binding.ipcamera.internal.InstarHandler;
54 import org.openhab.binding.ipcamera.internal.IpCameraActions;
55 import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
56 import org.openhab.binding.ipcamera.internal.IpCameraDynamicStateDescriptionProvider;
57 import org.openhab.binding.ipcamera.internal.MyNettyAuthHandler;
58 import org.openhab.binding.ipcamera.internal.StreamServerHandler;
59 import org.openhab.binding.ipcamera.internal.onvif.OnvifConnection;
60 import org.openhab.core.OpenHAB;
61 import org.openhab.core.library.types.DecimalType;
62 import org.openhab.core.library.types.IncreaseDecreaseType;
63 import org.openhab.core.library.types.OnOffType;
64 import org.openhab.core.library.types.PercentType;
65 import org.openhab.core.library.types.RawType;
66 import org.openhab.core.library.types.StringType;
67 import org.openhab.core.thing.ChannelUID;
68 import org.openhab.core.thing.Thing;
69 import org.openhab.core.thing.ThingStatus;
70 import org.openhab.core.thing.ThingStatusDetail;
71 import org.openhab.core.thing.binding.BaseThingHandler;
72 import org.openhab.core.thing.binding.ThingHandlerService;
73 import org.openhab.core.types.Command;
74 import org.openhab.core.types.RefreshType;
75 import org.openhab.core.types.State;
76 import org.slf4j.Logger;
77 import org.slf4j.LoggerFactory;
78
79 import io.netty.bootstrap.Bootstrap;
80 import io.netty.bootstrap.ServerBootstrap;
81 import io.netty.buffer.ByteBuf;
82 import io.netty.buffer.Unpooled;
83 import io.netty.channel.Channel;
84 import io.netty.channel.ChannelDuplexHandler;
85 import io.netty.channel.ChannelFuture;
86 import io.netty.channel.ChannelFutureListener;
87 import io.netty.channel.ChannelHandlerContext;
88 import io.netty.channel.ChannelInitializer;
89 import io.netty.channel.ChannelOption;
90 import io.netty.channel.EventLoopGroup;
91 import io.netty.channel.group.ChannelGroup;
92 import io.netty.channel.group.DefaultChannelGroup;
93 import io.netty.channel.nio.NioEventLoopGroup;
94 import io.netty.channel.socket.SocketChannel;
95 import io.netty.channel.socket.nio.NioServerSocketChannel;
96 import io.netty.channel.socket.nio.NioSocketChannel;
97 import io.netty.handler.codec.base64.Base64;
98 import io.netty.handler.codec.http.DefaultFullHttpRequest;
99 import io.netty.handler.codec.http.DefaultHttpResponse;
100 import io.netty.handler.codec.http.FullHttpRequest;
101 import io.netty.handler.codec.http.HttpClientCodec;
102 import io.netty.handler.codec.http.HttpContent;
103 import io.netty.handler.codec.http.HttpHeaderNames;
104 import io.netty.handler.codec.http.HttpHeaderValues;
105 import io.netty.handler.codec.http.HttpMessage;
106 import io.netty.handler.codec.http.HttpMethod;
107 import io.netty.handler.codec.http.HttpResponse;
108 import io.netty.handler.codec.http.HttpResponseStatus;
109 import io.netty.handler.codec.http.HttpServerCodec;
110 import io.netty.handler.codec.http.HttpVersion;
111 import io.netty.handler.codec.http.LastHttpContent;
112 import io.netty.handler.stream.ChunkedWriteHandler;
113 import io.netty.handler.timeout.IdleState;
114 import io.netty.handler.timeout.IdleStateEvent;
115 import io.netty.handler.timeout.IdleStateHandler;
116 import io.netty.util.CharsetUtil;
117 import io.netty.util.ReferenceCountUtil;
118 import io.netty.util.concurrent.GlobalEventExecutor;
119
120 /**
121  * The {@link IpCameraHandler} is responsible for handling commands, which are
122  * sent to one of the channels.
123  *
124  * @author Matthew Skinner - Initial contribution
125  */
126
127 @NonNullByDefault
128 public class IpCameraHandler extends BaseThingHandler {
129     public final Logger logger = LoggerFactory.getLogger(getClass());
130     public final IpCameraDynamicStateDescriptionProvider stateDescriptionProvider;
131     private ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(4);
132     private GroupTracker groupTracker;
133     public CameraConfig cameraConfig = new CameraConfig();
134
135     // ChannelGroup is thread safe
136     public final ChannelGroup mjpegChannelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
137     private final ChannelGroup snapshotMjpegChannelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
138     private final ChannelGroup autoSnapshotMjpegChannelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
139     public final ChannelGroup openChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
140     public @Nullable Ffmpeg ffmpegHLS = null;
141     public @Nullable Ffmpeg ffmpegRecord = null;
142     public @Nullable Ffmpeg ffmpegGIF = null;
143     public @Nullable Ffmpeg ffmpegRtspHelper = null;
144     public @Nullable Ffmpeg ffmpegMjpeg = null;
145     public @Nullable Ffmpeg ffmpegSnapshot = null;
146     public boolean streamingAutoFps = false;
147     public boolean motionDetected = false;
148
149     private @Nullable ScheduledFuture<?> cameraConnectionJob = null;
150     private @Nullable ScheduledFuture<?> pollCameraJob = null;
151     private @Nullable ScheduledFuture<?> snapshotJob = null;
152     private @Nullable Bootstrap mainBootstrap;
153     private @Nullable ServerBootstrap serverBootstrap;
154
155     private EventLoopGroup mainEventLoopGroup = new NioEventLoopGroup();
156     private EventLoopGroup serversLoopGroup = new NioEventLoopGroup();
157     private FullHttpRequest putRequestWithBody = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod("PUT"),
158             "");
159     private String gifFilename = "ipcamera";
160     private String gifHistory = "";
161     private String mp4History = "";
162     public int gifHistoryLength;
163     public int mp4HistoryLength;
164     private String mp4Filename = "ipcamera";
165     private int mp4RecordTime;
166     private int gifRecordTime = 5;
167     private LinkedList<byte[]> fifoSnapshotBuffer = new LinkedList<byte[]>();
168     private int snapCount;
169     private boolean updateImageChannel = false;
170     private boolean updateAutoFps = false;
171     private byte lowPriorityCounter = 0;
172     public String hostIp;
173     public Map<String, ChannelTracking> channelTrackingMap = new ConcurrentHashMap<>();
174     public List<String> lowPriorityRequests = new ArrayList<>(0);
175
176     // basicAuth MUST remain private as it holds the cameraConfig.getPassword()
177     private String basicAuth = "";
178     public boolean useBasicAuth = false;
179     public boolean useDigestAuth = false;
180     public String snapshotUri = "";
181     public String mjpegUri = "";
182     private @Nullable ChannelFuture serverFuture = null;
183     private Object firstStreamedMsg = new Object();
184     public byte[] currentSnapshot = new byte[] { (byte) 0x00 };
185     public ReentrantLock lockCurrentSnapshot = new ReentrantLock();
186     public String rtspUri = "";
187     public boolean audioAlarmUpdateSnapshot = false;
188     private boolean motionAlarmUpdateSnapshot = false;
189     private boolean isOnline = false; // Used so only 1 error is logged when a network issue occurs.
190     private boolean firstAudioAlarm = false;
191     private boolean firstMotionAlarm = false;
192     public Double motionThreshold = 0.0016;
193     public int audioThreshold = 35;
194     @SuppressWarnings("unused")
195     private @Nullable StreamServerHandler streamServerHandler;
196     private boolean streamingSnapshotMjpeg = false;
197     public boolean motionAlarmEnabled = false;
198     public boolean audioAlarmEnabled = false;
199     public boolean ffmpegSnapshotGeneration = false;
200     public boolean snapshotPolling = false;
201     public OnvifConnection onvifCamera = new OnvifConnection(this, "", "", "");
202
203     // These methods handle the response from all camera brands, nothing specific to 1 brand.
204     private class CommonCameraHandler extends ChannelDuplexHandler {
205         private int bytesToRecieve = 0;
206         private int bytesAlreadyRecieved = 0;
207         private byte[] incomingJpeg = new byte[0];
208         private String incomingMessage = "";
209         private String contentType = "empty";
210         private String boundary = "";
211         private Object reply = new Object();
212         private String requestUrl = "";
213         private boolean closeConnection = true;
214         private boolean isChunked = false;
215
216         public void setURL(String url) {
217             requestUrl = url;
218         }
219
220         @Override
221         public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
222             if (msg == null || ctx == null) {
223                 return;
224             }
225             try {
226                 if (msg instanceof HttpResponse) {
227                     HttpResponse response = (HttpResponse) msg;
228                     if (response.status().code() != 401) {
229                         if (!response.headers().isEmpty()) {
230                             for (String name : response.headers().names()) {
231                                 // Some cameras use first letter uppercase and others dont.
232                                 switch (name.toLowerCase()) { // Possible localization issues doing this
233                                     case "content-type":
234                                         contentType = response.headers().getAsString(name);
235                                         break;
236                                     case "content-length":
237                                         bytesToRecieve = Integer.parseInt(response.headers().getAsString(name));
238                                         break;
239                                     case "connection":
240                                         if (response.headers().getAsString(name).contains("keep-alive")) {
241                                             closeConnection = false;
242                                         }
243                                         break;
244                                     case "transfer-encoding":
245                                         if (response.headers().getAsString(name).contains("chunked")) {
246                                             isChunked = true;
247                                         }
248                                         break;
249                                 }
250                             }
251                             if (contentType.contains("multipart")) {
252                                 closeConnection = false;
253                                 if (mjpegUri.equals(requestUrl)) {
254                                     if (msg instanceof HttpMessage) {
255                                         // very start of stream only
256                                         ReferenceCountUtil.retain(msg, 1);
257                                         firstStreamedMsg = msg;
258                                         streamToGroup(firstStreamedMsg, mjpegChannelGroup, true);
259                                     }
260                                 } else {
261                                     boundary = Helper.searchString(contentType, "boundary=");
262                                 }
263                             } else if (contentType.contains("image/jp")) {
264                                 if (bytesToRecieve == 0) {
265                                     bytesToRecieve = 768000; // 0.768 Mbyte when no Content-Length is sent
266                                     logger.debug("Camera has no Content-Length header, we have to guess how much RAM.");
267                                 }
268                                 incomingJpeg = new byte[bytesToRecieve];
269                             }
270                         }
271                     }
272                 }
273                 if (msg instanceof HttpContent) {
274                     if (mjpegUri.equals(requestUrl)) {
275                         // multiple MJPEG stream packets come back as this.
276                         ReferenceCountUtil.retain(msg, 1);
277                         streamToGroup(msg, mjpegChannelGroup, true);
278                     } else {
279                         HttpContent content = (HttpContent) msg;
280                         // Found some cameras use Content-Type: image/jpg instead of image/jpeg
281                         if (contentType.contains("image/jp")) {
282                             for (int i = 0; i < content.content().capacity(); i++) {
283                                 incomingJpeg[bytesAlreadyRecieved++] = content.content().getByte(i);
284                             }
285                             if (content instanceof LastHttpContent) {
286                                 processSnapshot(incomingJpeg);
287                                 // testing next line and if works need to do a full cleanup of this function.
288                                 closeConnection = true;
289                                 if (closeConnection) {
290                                     ctx.close();
291                                 } else {
292                                     bytesToRecieve = 0;
293                                     bytesAlreadyRecieved = 0;
294                                 }
295                             }
296                         } else { // incomingMessage that is not an IMAGE
297                             if (incomingMessage.isEmpty()) {
298                                 incomingMessage = content.content().toString(CharsetUtil.UTF_8);
299                             } else {
300                                 incomingMessage += content.content().toString(CharsetUtil.UTF_8);
301                             }
302                             bytesAlreadyRecieved = incomingMessage.length();
303                             if (content instanceof LastHttpContent) {
304                                 // If it is not an image send it on to the next handler//
305                                 if (bytesAlreadyRecieved != 0) {
306                                     reply = incomingMessage;
307                                     super.channelRead(ctx, reply);
308                                 }
309                             }
310                             // Alarm Streams never have a LastHttpContent as they always stay open//
311                             else if (contentType.contains("multipart")) {
312                                 int beginIndex, endIndex;
313                                 if (bytesToRecieve == 0) {
314                                     beginIndex = incomingMessage.indexOf("Content-Length:");
315                                     if (beginIndex != -1) {
316                                         endIndex = incomingMessage.indexOf("\r\n", beginIndex);
317                                         if (endIndex != -1) {
318                                             bytesToRecieve = Integer.parseInt(
319                                                     incomingMessage.substring(beginIndex + 15, endIndex).strip());
320                                         }
321                                     }
322                                 }
323                                 // --boundary and headers are not included in the Content-Length value
324                                 if (bytesAlreadyRecieved > bytesToRecieve) {
325                                     // Check if message has a second --boundary
326                                     endIndex = incomingMessage.indexOf("--" + boundary, bytesToRecieve);
327                                     if (endIndex == -1) {
328                                         reply = incomingMessage;
329                                         incomingMessage = "";
330                                         bytesToRecieve = 0;
331                                         bytesAlreadyRecieved = 0;
332                                     } else {
333                                         reply = incomingMessage.substring(0, endIndex);
334                                         incomingMessage = incomingMessage.substring(endIndex, incomingMessage.length());
335                                         bytesToRecieve = 0;// Triggers search next time for Content-Length:
336                                         bytesAlreadyRecieved = incomingMessage.length() - endIndex;
337                                     }
338                                     super.channelRead(ctx, reply);
339                                 }
340                             }
341                             // Foscam needs this as will other cameras with chunks//
342                             if (isChunked && bytesAlreadyRecieved != 0) {
343                                 logger.debug("Reply is chunked.");
344                                 reply = incomingMessage;
345                                 super.channelRead(ctx, reply);
346                             }
347                         }
348                     }
349                 } else { // msg is not HttpContent
350                     // Foscam cameras need this
351                     if (!contentType.contains("image/jp") && bytesAlreadyRecieved != 0) {
352                         reply = incomingMessage;
353                         logger.debug("Packet back from camera is {}", incomingMessage);
354                         super.channelRead(ctx, reply);
355                     }
356                 }
357             } finally {
358                 ReferenceCountUtil.release(msg);
359             }
360         }
361
362         @Override
363         public void channelReadComplete(@Nullable ChannelHandlerContext ctx) {
364         }
365
366         @Override
367         public void handlerAdded(@Nullable ChannelHandlerContext ctx) {
368         }
369
370         @Override
371         public void handlerRemoved(@Nullable ChannelHandlerContext ctx) {
372         }
373
374         @Override
375         public void exceptionCaught(@Nullable ChannelHandlerContext ctx, @Nullable Throwable cause) {
376             if (cause == null || ctx == null) {
377                 return;
378             }
379             if (cause instanceof ArrayIndexOutOfBoundsException) {
380                 logger.debug("Camera sent {} bytes when the content-length header was {}.", bytesAlreadyRecieved,
381                         bytesToRecieve);
382             } else {
383                 logger.warn("!!!! Camera possibly closed the channel on the binding, cause reported is: {}",
384                         cause.getMessage());
385             }
386             ctx.close();
387         }
388
389         @Override
390         public void userEventTriggered(@Nullable ChannelHandlerContext ctx, @Nullable Object evt) throws Exception {
391             if (ctx == null) {
392                 return;
393             }
394             if (evt instanceof IdleStateEvent) {
395                 IdleStateEvent e = (IdleStateEvent) evt;
396                 // If camera does not use the channel for X amount of time it will close.
397                 if (e.state() == IdleState.READER_IDLE) {
398                     String urlToKeepOpen = "";
399                     switch (thing.getThingTypeUID().getId()) {
400                         case DAHUA_THING:
401                             urlToKeepOpen = "/cgi-bin/eventManager.cgi?action=attach&codes=[All]";
402                             break;
403                         case HIKVISION_THING:
404                             urlToKeepOpen = "/ISAPI/Event/notification/alertStream";
405                             break;
406                         case DOORBIRD_THING:
407                             urlToKeepOpen = "/bha-api/monitor.cgi?ring=doorbell,motionsensor";
408                             break;
409                     }
410                     ChannelTracking channelTracking = channelTrackingMap.get(urlToKeepOpen);
411                     if (channelTracking != null) {
412                         if (channelTracking.getChannel() == ctx.channel()) {
413                             return; // don't auto close this as it is for the alarms.
414                         }
415                     }
416                     ctx.close();
417                 }
418             }
419         }
420     }
421
422     public IpCameraHandler(Thing thing, @Nullable String ipAddress, GroupTracker groupTracker,
423             IpCameraDynamicStateDescriptionProvider stateDescriptionProvider) {
424         super(thing);
425         this.stateDescriptionProvider = stateDescriptionProvider;
426         if (ipAddress != null) {
427             hostIp = ipAddress;
428         } else {
429             hostIp = Helper.getLocalIpAddress();
430         }
431         this.groupTracker = groupTracker;
432     }
433
434     private IpCameraHandler getHandle() {
435         return this;
436     }
437
438     // false clears the stored user/pass hash, true creates the hash
439     public boolean setBasicAuth(boolean useBasic) {
440         if (!useBasic) {
441             logger.debug("Clearing out the stored BASIC auth now.");
442             basicAuth = "";
443             return false;
444         } else if (!basicAuth.isEmpty()) {
445             // due to camera may have been sent multiple requests before the auth was set, this may trigger falsely.
446             logger.warn("Camera is reporting your username and/or password is wrong.");
447             return false;
448         }
449         if (!cameraConfig.getUser().isEmpty() && !cameraConfig.getPassword().isEmpty()) {
450             String authString = cameraConfig.getUser() + ":" + cameraConfig.getPassword();
451             ByteBuf byteBuf = null;
452             try {
453                 byteBuf = Base64.encode(Unpooled.wrappedBuffer(authString.getBytes(CharsetUtil.UTF_8)));
454                 basicAuth = byteBuf.getCharSequence(0, byteBuf.capacity(), CharsetUtil.UTF_8).toString();
455             } finally {
456                 if (byteBuf != null) {
457                     byteBuf.release();
458                 }
459             }
460             return true;
461         } else {
462             cameraConfigError("Camera is asking for Basic Auth when you have not provided a username and/or password.");
463         }
464         return false;
465     }
466
467     private String getCorrectUrlFormat(String longUrl) {
468         String temp = longUrl;
469         URL url;
470
471         if (longUrl.isEmpty() || "ffmpeg".equals(longUrl)) {
472             return longUrl;
473         }
474
475         try {
476             url = new URL(longUrl);
477             int port = url.getPort();
478             if (port == -1) {
479                 if (url.getQuery() == null) {
480                     temp = url.getPath();
481                 } else {
482                     temp = url.getPath() + "?" + url.getQuery();
483                 }
484             } else {
485                 if (url.getQuery() == null) {
486                     temp = ":" + url.getPort() + url.getPath();
487                 } else {
488                     temp = ":" + url.getPort() + url.getPath() + "?" + url.getQuery();
489                 }
490             }
491         } catch (MalformedURLException e) {
492             cameraConfigError("A non valid URL has been given to the binding, check they work in a browser.");
493         }
494         return temp;
495     }
496
497     public void sendHttpPUT(String httpRequestURL, FullHttpRequest request) {
498         putRequestWithBody = request; // use Global so the authhandler can use it when resent with DIGEST.
499         sendHttpRequest("PUT", httpRequestURL, null);
500     }
501
502     public void sendHttpGET(String httpRequestURL) {
503         sendHttpRequest("GET", httpRequestURL, null);
504     }
505
506     public int getPortFromShortenedUrl(String httpRequestURL) {
507         if (httpRequestURL.startsWith(":")) {
508             int end = httpRequestURL.indexOf("/");
509             return Integer.parseInt(httpRequestURL.substring(1, end));
510         }
511         return cameraConfig.getPort();
512     }
513
514     public String getTinyUrl(String httpRequestURL) {
515         if (httpRequestURL.startsWith(":")) {
516             int beginIndex = httpRequestURL.indexOf("/");
517             return httpRequestURL.substring(beginIndex);
518         }
519         return httpRequestURL;
520     }
521
522     // Always use this as sendHttpGET(GET/POST/PUT/DELETE, "/foo/bar",null)//
523     // The authHandler will generate a digest string and re-send using this same function when needed.
524     @SuppressWarnings("null")
525     public void sendHttpRequest(String httpMethod, String httpRequestURLFull, @Nullable String digestString) {
526         int port = getPortFromShortenedUrl(httpRequestURLFull);
527         String httpRequestURL = getTinyUrl(httpRequestURLFull);
528
529         if (mainBootstrap == null) {
530             mainBootstrap = new Bootstrap();
531             mainBootstrap.group(mainEventLoopGroup);
532             mainBootstrap.channel(NioSocketChannel.class);
533             mainBootstrap.option(ChannelOption.SO_KEEPALIVE, true);
534             mainBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 4500);
535             mainBootstrap.option(ChannelOption.SO_SNDBUF, 1024 * 8);
536             mainBootstrap.option(ChannelOption.SO_RCVBUF, 1024 * 1024);
537             mainBootstrap.option(ChannelOption.TCP_NODELAY, true);
538             mainBootstrap.handler(new ChannelInitializer<SocketChannel>() {
539
540                 @Override
541                 public void initChannel(SocketChannel socketChannel) throws Exception {
542                     // HIK Alarm stream needs > 9sec idle to stop stream closing
543                     socketChannel.pipeline().addLast(new IdleStateHandler(18, 0, 0));
544                     socketChannel.pipeline().addLast(new HttpClientCodec());
545                     socketChannel.pipeline().addLast(AUTH_HANDLER,
546                             new MyNettyAuthHandler(cameraConfig.getUser(), cameraConfig.getPassword(), getHandle()));
547                     socketChannel.pipeline().addLast(COMMON_HANDLER, new CommonCameraHandler());
548
549                     switch (thing.getThingTypeUID().getId()) {
550                         case AMCREST_THING:
551                             socketChannel.pipeline().addLast(AMCREST_HANDLER, new AmcrestHandler(getHandle()));
552                             break;
553                         case DAHUA_THING:
554                             socketChannel.pipeline()
555                                     .addLast(new DahuaHandler(getHandle(), cameraConfig.getNvrChannel()));
556                             break;
557                         case DOORBIRD_THING:
558                             socketChannel.pipeline().addLast(new DoorBirdHandler(getHandle()));
559                             break;
560                         case FOSCAM_THING:
561                             socketChannel.pipeline().addLast(
562                                     new FoscamHandler(getHandle(), cameraConfig.getUser(), cameraConfig.getPassword()));
563                             break;
564                         case HIKVISION_THING:
565                             socketChannel.pipeline()
566                                     .addLast(new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel()));
567                             break;
568                         case INSTAR_THING:
569                             socketChannel.pipeline().addLast(INSTAR_HANDLER, new InstarHandler(getHandle()));
570                             break;
571                         default:
572                             socketChannel.pipeline().addLast(new HttpOnlyHandler(getHandle()));
573                             break;
574                     }
575                 }
576             });
577         }
578
579         FullHttpRequest request;
580         if (!"PUT".equals(httpMethod) || (useDigestAuth && digestString == null)) {
581             request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod(httpMethod), httpRequestURL);
582             request.headers().set("Host", cameraConfig.getIp() + ":" + port);
583             request.headers().set("Connection", HttpHeaderValues.KEEP_ALIVE);
584         } else {
585             request = putRequestWithBody;
586         }
587
588         if (!basicAuth.isEmpty()) {
589             if (useDigestAuth) {
590                 logger.warn("Camera at IP:{} had both Basic and Digest set to be used", cameraConfig.getIp());
591                 setBasicAuth(false);
592             } else {
593                 request.headers().set("Authorization", "Basic " + basicAuth);
594             }
595         }
596
597         if (useDigestAuth) {
598             if (digestString != null) {
599                 request.headers().set("Authorization", "Digest " + digestString);
600             }
601         }
602
603         mainBootstrap.connect(new InetSocketAddress(cameraConfig.getIp(), port))
604                 .addListener(new ChannelFutureListener() {
605
606                     @Override
607                     public void operationComplete(@Nullable ChannelFuture future) {
608                         if (future == null) {
609                             return;
610                         }
611                         if (future.isDone() && future.isSuccess()) {
612                             Channel ch = future.channel();
613                             openChannels.add(ch);
614                             if (!isOnline) {
615                                 bringCameraOnline();
616                             }
617                             logger.trace("Sending camera: {}: http://{}:{}{}", httpMethod, cameraConfig.getIp(), port,
618                                     httpRequestURL);
619
620                             openChannel(ch, httpRequestURL);
621                             CommonCameraHandler commonHandler = (CommonCameraHandler) ch.pipeline().get(COMMON_HANDLER);
622                             commonHandler.setURL(httpRequestURLFull);
623                             MyNettyAuthHandler authHandler = (MyNettyAuthHandler) ch.pipeline().get(AUTH_HANDLER);
624                             authHandler.setURL(httpMethod, httpRequestURL);
625
626                             switch (thing.getThingTypeUID().getId()) {
627                                 case AMCREST_THING:
628                                     AmcrestHandler amcrestHandler = (AmcrestHandler) ch.pipeline().get(AMCREST_HANDLER);
629                                     amcrestHandler.setURL(httpRequestURL);
630                                     break;
631                                 case INSTAR_THING:
632                                     InstarHandler instarHandler = (InstarHandler) ch.pipeline().get(INSTAR_HANDLER);
633                                     instarHandler.setURL(httpRequestURL);
634                                     break;
635                             }
636                             ch.writeAndFlush(request);
637                         } else { // an error occured
638                             cameraCommunicationError(
639                                     "Connection Timeout: Check your IP and PORT are correct and the camera can be reached.");
640                         }
641                     }
642                 });
643     }
644
645     public void processSnapshot(byte[] incommingSnapshot) {
646         lockCurrentSnapshot.lock();
647         try {
648             currentSnapshot = incommingSnapshot;
649             if (cameraConfig.getGifPreroll() > 0) {
650                 fifoSnapshotBuffer.add(incommingSnapshot);
651                 if (fifoSnapshotBuffer.size() > (cameraConfig.getGifPreroll() + gifRecordTime)) {
652                     fifoSnapshotBuffer.removeFirst();
653                 }
654             }
655         } finally {
656             lockCurrentSnapshot.unlock();
657         }
658
659         if (streamingSnapshotMjpeg) {
660             sendMjpegFrame(incommingSnapshot, snapshotMjpegChannelGroup);
661         }
662         if (streamingAutoFps) {
663             if (motionDetected) {
664                 sendMjpegFrame(incommingSnapshot, autoSnapshotMjpegChannelGroup);
665             } else if (updateAutoFps) {
666                 // only happens every 8 seconds as some browsers need a frame that often to keep stream alive.
667                 sendMjpegFrame(incommingSnapshot, autoSnapshotMjpegChannelGroup);
668                 updateAutoFps = false;
669             }
670         }
671
672         if (updateImageChannel) {
673             updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
674         } else if (firstMotionAlarm || motionAlarmUpdateSnapshot) {
675             updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
676             firstMotionAlarm = motionAlarmUpdateSnapshot = false;
677         } else if (firstAudioAlarm || audioAlarmUpdateSnapshot) {
678             updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
679             firstAudioAlarm = audioAlarmUpdateSnapshot = false;
680         }
681     }
682
683     public void stopStreamServer() {
684         serversLoopGroup.shutdownGracefully();
685         serverBootstrap = null;
686     }
687
688     @SuppressWarnings("null")
689     public void startStreamServer() {
690         if (serverBootstrap == null) {
691             try {
692                 serversLoopGroup = new NioEventLoopGroup();
693                 serverBootstrap = new ServerBootstrap();
694                 serverBootstrap.group(serversLoopGroup);
695                 serverBootstrap.channel(NioServerSocketChannel.class);
696                 // IP "0.0.0.0" will bind the server to all network connections//
697                 serverBootstrap.localAddress(new InetSocketAddress("0.0.0.0", cameraConfig.getServerPort()));
698                 serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
699                     @Override
700                     protected void initChannel(SocketChannel socketChannel) throws Exception {
701                         socketChannel.pipeline().addLast("idleStateHandler", new IdleStateHandler(0, 60, 0));
702                         socketChannel.pipeline().addLast("HttpServerCodec", new HttpServerCodec());
703                         socketChannel.pipeline().addLast("ChunkedWriteHandler", new ChunkedWriteHandler());
704                         socketChannel.pipeline().addLast("streamServerHandler", new StreamServerHandler(getHandle()));
705                     }
706                 });
707                 serverFuture = serverBootstrap.bind().sync();
708                 serverFuture.await(4000);
709                 logger.debug("File server for camera at {} has started on port {} for all NIC's.", cameraConfig.getIp(),
710                         cameraConfig.getServerPort());
711                 updateState(CHANNEL_MJPEG_URL,
712                         new StringType("http://" + hostIp + ":" + cameraConfig.getServerPort() + "/ipcamera.mjpeg"));
713                 updateState(CHANNEL_HLS_URL,
714                         new StringType("http://" + hostIp + ":" + cameraConfig.getServerPort() + "/ipcamera.m3u8"));
715                 updateState(CHANNEL_IMAGE_URL,
716                         new StringType("http://" + hostIp + ":" + cameraConfig.getServerPort() + "/ipcamera.jpg"));
717             } catch (Exception e) {
718                 cameraConfigError("Exception when starting server. Try changing the Server Port to another number.");
719             }
720             if (thing.getThingTypeUID().getId().equals(INSTAR_THING)) {
721                 logger.debug("Setting up the Alarm Server settings in the camera now");
722                 sendHttpGET(
723                         "/param.cgi?cmd=setmdalarm&-aname=server2&-switch=on&-interval=1&cmd=setalarmserverattr&-as_index=3&-as_server="
724                                 + hostIp + "&-as_port=" + cameraConfig.getServerPort()
725                                 + "&-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");
726             }
727         }
728     }
729
730     public void setupSnapshotStreaming(boolean stream, ChannelHandlerContext ctx, boolean auto) {
731         if (stream) {
732             sendMjpegFirstPacket(ctx);
733             if (auto) {
734                 autoSnapshotMjpegChannelGroup.add(ctx.channel());
735                 lockCurrentSnapshot.lock();
736                 try {
737                     sendMjpegFrame(currentSnapshot, autoSnapshotMjpegChannelGroup);
738                     // iOS uses a FIFO? and needs two frames to display a pic
739                     sendMjpegFrame(currentSnapshot, autoSnapshotMjpegChannelGroup);
740                 } finally {
741                     lockCurrentSnapshot.unlock();
742                 }
743                 streamingAutoFps = true;
744             } else {
745                 snapshotMjpegChannelGroup.add(ctx.channel());
746                 lockCurrentSnapshot.lock();
747                 try {
748                     sendMjpegFrame(currentSnapshot, snapshotMjpegChannelGroup);
749                 } finally {
750                     lockCurrentSnapshot.unlock();
751                 }
752                 streamingSnapshotMjpeg = true;
753                 startSnapshotPolling();
754             }
755         } else {
756             snapshotMjpegChannelGroup.remove(ctx.channel());
757             autoSnapshotMjpegChannelGroup.remove(ctx.channel());
758             if (streamingSnapshotMjpeg && snapshotMjpegChannelGroup.isEmpty()) {
759                 streamingSnapshotMjpeg = false;
760                 stopSnapshotPolling();
761                 logger.debug("All snapshots.mjpeg streams have stopped.");
762             } else if (streamingAutoFps && autoSnapshotMjpegChannelGroup.isEmpty()) {
763                 streamingAutoFps = false;
764                 stopSnapshotPolling();
765                 logger.debug("All autofps.mjpeg streams have stopped.");
766             }
767         }
768     }
769
770     // If start is true the CTX is added to the list to stream video to, false stops
771     // the stream.
772     public void setupMjpegStreaming(boolean start, ChannelHandlerContext ctx) {
773         if (start) {
774             if (mjpegChannelGroup.isEmpty()) {// first stream being requested.
775                 mjpegChannelGroup.add(ctx.channel());
776                 if (mjpegUri.isEmpty() || "ffmpeg".equals(mjpegUri)) {
777                     sendMjpegFirstPacket(ctx);
778                     setupFfmpegFormat(FFmpegFormat.MJPEG);
779                 } else {
780                     try {
781                         // fix Dahua reboots when refreshing a mjpeg stream.
782                         TimeUnit.MILLISECONDS.sleep(500);
783                     } catch (InterruptedException e) {
784                     }
785                     sendHttpGET(mjpegUri);
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," + motionThreshold
1039                                 + ")',metadata=print";
1040                     } else {
1041                         filterOptions = filterOptions + " " + usersMotionOptions + " -vf select='gte(scene,"
1042                                 + motionThreshold + ")',metadata=print";
1043                     }
1044                 } else if (motionAlarmEnabled) {
1045                     filterOptions = filterOptions
1046                             .concat(" -vf select='gte(scene," + motionThreshold + ")',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 {
1266                         motionAlarmEnabled = true;
1267                         motionThreshold = Double.valueOf(command.toString());
1268                         motionThreshold = motionThreshold / 10000;
1269                     }
1270                     setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
1271                     return;
1272                 case CHANNEL_START_STREAM:
1273                     Ffmpeg localHLS;
1274                     if (OnOffType.ON.equals(command)) {
1275                         localHLS = ffmpegHLS;
1276                         if (localHLS == null) {
1277                             setupFfmpegFormat(FFmpegFormat.HLS);
1278                             localHLS = ffmpegHLS;
1279                         }
1280                         if (localHLS != null) {
1281                             localHLS.setKeepAlive(-1);// Now will run till manually stopped.
1282                             localHLS.startConverting();
1283                         }
1284                     } else {
1285                         localHLS = ffmpegHLS;
1286                         if (localHLS != null) {
1287                             // Still runs but will be able to auto stop when the HLS stream is no longer used.
1288                             localHLS.setKeepAlive(1);
1289                         }
1290                     }
1291                     return;
1292                 case CHANNEL_EXTERNAL_MOTION:
1293                     if (OnOffType.ON.equals(command)) {
1294                         motionDetected(CHANNEL_EXTERNAL_MOTION);
1295                     } else {
1296                         noMotionDetected(CHANNEL_EXTERNAL_MOTION);
1297                     }
1298                     return;
1299                 case CHANNEL_GOTO_PRESET:
1300                     if (onvifCamera.supportsPTZ()) {
1301                         onvifCamera.gotoPreset(Integer.valueOf(command.toString()));
1302                     }
1303                     return;
1304                 case CHANNEL_POLL_IMAGE:
1305                     if (OnOffType.ON.equals(command)) {
1306                         if (snapshotUri.isEmpty()) {
1307                             ffmpegSnapshotGeneration = true;
1308                             setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1309                             updateImageChannel = false;
1310                         } else {
1311                             updateImageChannel = true;
1312                             sendHttpGET(snapshotUri);// Allows this to change Image FPS on demand
1313                         }
1314                     } else {
1315                         Ffmpeg localSnaps = ffmpegSnapshot;
1316                         if (localSnaps != null) {
1317                             localSnaps.stopConverting();
1318                             ffmpegSnapshotGeneration = false;
1319                         }
1320                         updateImageChannel = false;
1321                     }
1322                     return;
1323                 case CHANNEL_PAN:
1324                     if (onvifCamera.supportsPTZ()) {
1325                         if (command instanceof IncreaseDecreaseType) {
1326                             if (command == IncreaseDecreaseType.INCREASE) {
1327                                 if (cameraConfig.getPtzContinuous()) {
1328                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveLeft);
1329                                 } else {
1330                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveLeft);
1331                                 }
1332                             } else {
1333                                 if (cameraConfig.getPtzContinuous()) {
1334                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveRight);
1335                                 } else {
1336                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveRight);
1337                                 }
1338                             }
1339                             return;
1340                         } else if (OnOffType.OFF.equals(command)) {
1341                             onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1342                             return;
1343                         }
1344                         onvifCamera.setAbsolutePan(Float.valueOf(command.toString()));
1345                         threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1346                     }
1347                     return;
1348                 case CHANNEL_TILT:
1349                     if (onvifCamera.supportsPTZ()) {
1350                         if (command instanceof IncreaseDecreaseType) {
1351                             if (IncreaseDecreaseType.INCREASE.equals(command)) {
1352                                 if (cameraConfig.getPtzContinuous()) {
1353                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveUp);
1354                                 } else {
1355                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveUp);
1356                                 }
1357                             } else {
1358                                 if (cameraConfig.getPtzContinuous()) {
1359                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveDown);
1360                                 } else {
1361                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveDown);
1362                                 }
1363                             }
1364                             return;
1365                         } else if (OnOffType.OFF.equals(command)) {
1366                             onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1367                             return;
1368                         }
1369                         onvifCamera.setAbsoluteTilt(Float.valueOf(command.toString()));
1370                         threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1371                     }
1372                     return;
1373                 case CHANNEL_ZOOM:
1374                     if (onvifCamera.supportsPTZ()) {
1375                         if (command instanceof IncreaseDecreaseType) {
1376                             if (IncreaseDecreaseType.INCREASE.equals(command)) {
1377                                 if (cameraConfig.getPtzContinuous()) {
1378                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveIn);
1379                                 } else {
1380                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveIn);
1381                                 }
1382                             } else {
1383                                 if (cameraConfig.getPtzContinuous()) {
1384                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveOut);
1385                                 } else {
1386                                     onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveOut);
1387                                 }
1388                             }
1389                             return;
1390                         } else if (OnOffType.OFF.equals(command)) {
1391                             onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1392                             return;
1393                         }
1394                         onvifCamera.setAbsoluteZoom(Float.valueOf(command.toString()));
1395                         threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1396                     }
1397                     return;
1398             }
1399         }
1400         // commands and refresh now get passed to brand handlers
1401         switch (thing.getThingTypeUID().getId()) {
1402             case AMCREST_THING:
1403                 AmcrestHandler amcrestHandler = new AmcrestHandler(getHandle());
1404                 amcrestHandler.handleCommand(channelUID, command);
1405                 if (lowPriorityRequests.isEmpty()) {
1406                     lowPriorityRequests = amcrestHandler.getLowPriorityRequests();
1407                 }
1408                 break;
1409             case DAHUA_THING:
1410                 DahuaHandler dahuaHandler = new DahuaHandler(getHandle(), cameraConfig.getNvrChannel());
1411                 dahuaHandler.handleCommand(channelUID, command);
1412                 if (lowPriorityRequests.isEmpty()) {
1413                     lowPriorityRequests = dahuaHandler.getLowPriorityRequests();
1414                 }
1415                 break;
1416             case DOORBIRD_THING:
1417                 DoorBirdHandler doorBirdHandler = new DoorBirdHandler(getHandle());
1418                 doorBirdHandler.handleCommand(channelUID, command);
1419                 if (lowPriorityRequests.isEmpty()) {
1420                     lowPriorityRequests = doorBirdHandler.getLowPriorityRequests();
1421                 }
1422                 break;
1423             case HIKVISION_THING:
1424                 HikvisionHandler hikvisionHandler = new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel());
1425                 hikvisionHandler.handleCommand(channelUID, command);
1426                 if (lowPriorityRequests.isEmpty()) {
1427                     lowPriorityRequests = hikvisionHandler.getLowPriorityRequests();
1428                 }
1429                 break;
1430             case FOSCAM_THING:
1431                 FoscamHandler foscamHandler = new FoscamHandler(getHandle(), cameraConfig.getUser(),
1432                         cameraConfig.getPassword());
1433                 foscamHandler.handleCommand(channelUID, command);
1434                 if (lowPriorityRequests.isEmpty()) {
1435                     lowPriorityRequests = foscamHandler.getLowPriorityRequests();
1436                 }
1437                 break;
1438             case INSTAR_THING:
1439                 InstarHandler instarHandler = new InstarHandler(getHandle());
1440                 instarHandler.handleCommand(channelUID, command);
1441                 if (lowPriorityRequests.isEmpty()) {
1442                     lowPriorityRequests = instarHandler.getLowPriorityRequests();
1443                 }
1444                 break;
1445             default:
1446                 HttpOnlyHandler defaultHandler = new HttpOnlyHandler(getHandle());
1447                 defaultHandler.handleCommand(channelUID, command);
1448                 if (lowPriorityRequests.isEmpty()) {
1449                     lowPriorityRequests = defaultHandler.getLowPriorityRequests();
1450                 }
1451                 break;
1452         }
1453     }
1454
1455     public void setChannelState(String channelToUpdate, State valueOf) {
1456         updateState(channelToUpdate, valueOf);
1457     }
1458
1459     private void bringCameraOnline() {
1460         isOnline = true;
1461         updateStatus(ThingStatus.ONLINE);
1462         groupTracker.listOfOnlineCameraHandlers.add(this);
1463         groupTracker.listOfOnlineCameraUID.add(getThing().getUID().getId());
1464         Future<?> localFuture = cameraConnectionJob;
1465         if (localFuture != null) {
1466             localFuture.cancel(false);
1467         }
1468
1469         if (cameraConfig.getGifPreroll() > 0 || cameraConfig.getUpdateImageWhen().contains("1")) {
1470             snapshotPolling = true;
1471             snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 1000, cameraConfig.getPollTime(),
1472                     TimeUnit.MILLISECONDS);
1473         }
1474
1475         pollCameraJob = threadPool.scheduleWithFixedDelay(this::pollCameraRunnable, 1000, 8000, TimeUnit.MILLISECONDS);
1476
1477         if (!rtspUri.isEmpty()) {
1478             updateState(CHANNEL_RTSP_URL, new StringType(rtspUri));
1479         }
1480         if (updateImageChannel) {
1481             updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1482         } else {
1483             updateState(CHANNEL_POLL_IMAGE, OnOffType.OFF);
1484         }
1485         if (!groupTracker.listOfGroupHandlers.isEmpty()) {
1486             for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1487                 handle.cameraOnline(getThing().getUID().getId());
1488             }
1489         }
1490     }
1491
1492     void snapshotIsFfmpeg() {
1493         bringCameraOnline();
1494         snapshotUri = "";// ffmpeg is a valid option. Simplify further checks.
1495         logger.debug(
1496                 "Binding has no snapshot url. Will use your CPU and FFmpeg to create snapshots from the cameras RTSP.");
1497         if (!rtspUri.isEmpty()) {
1498             updateImageChannel = false;
1499             ffmpegSnapshotGeneration = true;
1500             setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1501             updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1502         } else {
1503             cameraConfigError("Binding can not find a RTSP url for this camera, please provide a FFmpeg Input URL.");
1504         }
1505     }
1506
1507     void pollingCameraConnection() {
1508         if (thing.getThingTypeUID().getId().equals(GENERIC_THING)) {
1509             if (rtspUri.isEmpty()) {
1510                 logger.warn("Binding has not been supplied with a FFmpeg Input URL, so some features will not work.");
1511             }
1512             if (snapshotUri.isEmpty() || "ffmpeg".equals(snapshotUri)) {
1513                 snapshotIsFfmpeg();
1514             } else {
1515                 sendHttpRequest("GET", snapshotUri, null);
1516             }
1517             return;
1518         }
1519         if (!onvifCamera.isConnected()) {
1520             logger.debug("About to connect to the IP Camera using the ONVIF PORT at IP:{}:{}", cameraConfig.getIp(),
1521                     cameraConfig.getOnvifPort());
1522             onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1523         }
1524         if ("ffmpeg".equals(snapshotUri)) {
1525             snapshotIsFfmpeg();
1526         } else if (!snapshotUri.isEmpty()) {
1527             sendHttpRequest("GET", snapshotUri, null);
1528         } else if (!rtspUri.isEmpty()) {
1529             snapshotIsFfmpeg();
1530         } else {
1531             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
1532                     "Camera failed to report a valid Snaphot and/or RTSP URL. See readme on how to use the SNAPSHOT_URL_OVERRIDE feature.");
1533         }
1534     }
1535
1536     public void cameraConfigError(String reason) {
1537         // wont try to reconnect again due to a config error being the cause.
1538         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, reason);
1539         dispose();
1540     }
1541
1542     public void cameraCommunicationError(String reason) {
1543         // will try to reconnect again as camera may be rebooting.
1544         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
1545         if (isOnline) {// if already offline dont try reconnecting in 6 seconds, we want 30sec wait.
1546             resetAndRetryConnecting();
1547         }
1548     }
1549
1550     boolean streamIsStopped(String url) {
1551         ChannelTracking channelTracking = channelTrackingMap.get(url);
1552         if (channelTracking != null) {
1553             if (channelTracking.getChannel().isActive()) {
1554                 return false; // stream is running.
1555             }
1556         }
1557         return true; // Stream stopped or never started.
1558     }
1559
1560     void snapshotRunnable() {
1561         // Snapshot should be first to keep consistent time between shots
1562         sendHttpGET(snapshotUri);
1563         if (snapCount > 0) {
1564             if (--snapCount == 0) {
1565                 setupFfmpegFormat(FFmpegFormat.GIF);
1566             }
1567         }
1568     }
1569
1570     public void stopSnapshotPolling() {
1571         Future<?> localFuture;
1572         if (!streamingSnapshotMjpeg && cameraConfig.getGifPreroll() == 0
1573                 && !cameraConfig.getUpdateImageWhen().contains("1")) {
1574             snapshotPolling = false;
1575             localFuture = snapshotJob;
1576             if (localFuture != null) {
1577                 localFuture.cancel(true);
1578             }
1579         } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // only during Motion Alarms
1580             snapshotPolling = false;
1581             localFuture = snapshotJob;
1582             if (localFuture != null) {
1583                 localFuture.cancel(true);
1584             }
1585         }
1586     }
1587
1588     public void startSnapshotPolling() {
1589         if (snapshotPolling || ffmpegSnapshotGeneration) {
1590             return; // Already polling or creating with FFmpeg from RTSP
1591         }
1592         if (streamingSnapshotMjpeg || streamingAutoFps) {
1593             snapshotPolling = true;
1594             snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 200, cameraConfig.getPollTime(),
1595                     TimeUnit.MILLISECONDS);
1596         } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1597             snapshotPolling = true;
1598             snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 200, cameraConfig.getPollTime(),
1599                     TimeUnit.MILLISECONDS);
1600         }
1601     }
1602
1603     /**
1604      * {@link pollCameraRunnable} Polls every 8 seconds, to check camera is still ONLINE and keep mjpeg and alarm
1605      * streams open and more.
1606      *
1607      */
1608     void pollCameraRunnable() {
1609         // Snapshot should be first to keep consistent time between shots
1610         if (streamingAutoFps) {
1611             updateAutoFps = true;
1612             if (!snapshotPolling && !ffmpegSnapshotGeneration) {
1613                 // Dont need to poll if creating from RTSP stream with FFmpeg or we are polling at full rate already.
1614                 sendHttpGET(snapshotUri);
1615             }
1616         } else if (!snapshotUri.isEmpty() && !snapshotPolling) {// we need to check camera is still online.
1617             sendHttpGET(snapshotUri);
1618         }
1619         // NOTE: Use lowPriorityRequests if get request is not needed every poll.
1620         if (!lowPriorityRequests.isEmpty()) {
1621             if (lowPriorityCounter >= lowPriorityRequests.size()) {
1622                 lowPriorityCounter = 0;
1623             }
1624             sendHttpGET(lowPriorityRequests.get(lowPriorityCounter++));
1625         }
1626         // what needs to be done every poll//
1627         switch (thing.getThingTypeUID().getId()) {
1628             case GENERIC_THING:
1629                 break;
1630             case ONVIF_THING:
1631                 if (!onvifCamera.isConnected()) {
1632                     onvifCamera.connect(true);
1633                 }
1634                 break;
1635             case INSTAR_THING:
1636                 noMotionDetected(CHANNEL_MOTION_ALARM);
1637                 noMotionDetected(CHANNEL_PIR_ALARM);
1638                 noAudioDetected();
1639                 break;
1640             case HIKVISION_THING:
1641                 if (streamIsStopped("/ISAPI/Event/notification/alertStream")) {
1642                     logger.info("The alarm stream was not running for camera {}, re-starting it now",
1643                             cameraConfig.getIp());
1644                     sendHttpGET("/ISAPI/Event/notification/alertStream");
1645                 }
1646                 break;
1647             case AMCREST_THING:
1648                 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=VideoMotion");
1649                 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=AudioMutation");
1650                 break;
1651             case DAHUA_THING:
1652                 // Check for alarms, channel for NVRs appears not to work at filtering.
1653                 if (streamIsStopped("/cgi-bin/eventManager.cgi?action=attach&codes=[All]")) {
1654                     logger.info("The alarm stream was not running for camera {}, re-starting it now",
1655                             cameraConfig.getIp());
1656                     sendHttpGET("/cgi-bin/eventManager.cgi?action=attach&codes=[All]");
1657                 }
1658                 break;
1659             case DOORBIRD_THING:
1660                 // Check for alarms, channel for NVRs appears not to work at filtering.
1661                 if (streamIsStopped("/bha-api/monitor.cgi?ring=doorbell,motionsensor")) {
1662                     logger.info("The alarm stream was not running for camera {}, re-starting it now",
1663                             cameraConfig.getIp());
1664                     sendHttpGET("/bha-api/monitor.cgi?ring=doorbell,motionsensor");
1665                 }
1666                 break;
1667             case FOSCAM_THING:
1668                 sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=getDevState&usr=" + cameraConfig.getUser() + "&pwd="
1669                         + cameraConfig.getPassword());
1670                 break;
1671         }
1672         Ffmpeg localHLS = ffmpegHLS;
1673         if (localHLS != null) {
1674             localHLS.checkKeepAlive();
1675         }
1676         if (openChannels.size() > 18) {
1677             logger.debug("There are {} open Channels being tracked.", openChannels.size());
1678             cleanChannels();
1679         }
1680     }
1681
1682     @Override
1683     public void initialize() {
1684         cameraConfig = getConfigAs(CameraConfig.class);
1685         snapshotUri = getCorrectUrlFormat(cameraConfig.getSnapshotUrl());
1686         mjpegUri = getCorrectUrlFormat(cameraConfig.getMjpegUrl());
1687         rtspUri = cameraConfig.getFfmpegInput();
1688         if (cameraConfig.getFfmpegOutput().isEmpty()) {
1689             cameraConfig
1690                     .setFfmpegOutput(OpenHAB.getUserDataFolder() + "/ipcamera/" + this.thing.getUID().getId() + "/");
1691         }
1692
1693         if (cameraConfig.getServerPort() < 1) {
1694             logger.warn(
1695                     "The Server Port is not set to a valid number which disables a lot of binding features. See readme for more info.");
1696         } else if (cameraConfig.getServerPort() < 1025) {
1697             logger.warn("The Server Port is <= 1024 and may cause permission errors under Linux, try a higher number.");
1698         }
1699
1700         // Known cameras will connect quicker if we skip ONVIF questions.
1701         switch (thing.getThingTypeUID().getId()) {
1702             case AMCREST_THING:
1703             case DAHUA_THING:
1704                 if (mjpegUri.isEmpty()) {
1705                     mjpegUri = "/cgi-bin/mjpg/video.cgi?channel=" + cameraConfig.getNvrChannel() + "&subtype=1";
1706                 }
1707                 if (snapshotUri.isEmpty()) {
1708                     snapshotUri = "/cgi-bin/snapshot.cgi?channel=" + cameraConfig.getNvrChannel();
1709                 }
1710                 break;
1711             case DOORBIRD_THING:
1712                 if (mjpegUri.isEmpty()) {
1713                     mjpegUri = "/bha-api/video.cgi";
1714                 }
1715                 if (snapshotUri.isEmpty()) {
1716                     snapshotUri = "/bha-api/image.cgi";
1717                 }
1718                 break;
1719             case FOSCAM_THING:
1720                 // Foscam needs any special char like spaces (%20) to be encoded for URLs.
1721                 cameraConfig.setUser(Helper.encodeSpecialChars(cameraConfig.getUser()));
1722                 cameraConfig.setPassword(Helper.encodeSpecialChars(cameraConfig.getPassword()));
1723                 if (mjpegUri.isEmpty()) {
1724                     mjpegUri = "/cgi-bin/CGIStream.cgi?cmd=GetMJStream&usr=" + cameraConfig.getUser() + "&pwd="
1725                             + cameraConfig.getPassword();
1726                 }
1727                 if (snapshotUri.isEmpty()) {
1728                     snapshotUri = "/cgi-bin/CGIProxy.fcgi?usr=" + cameraConfig.getUser() + "&pwd="
1729                             + cameraConfig.getPassword() + "&cmd=snapPicture2";
1730                 }
1731                 break;
1732             case HIKVISION_THING:// The 02 gives you the first sub stream which needs to be set to MJPEG
1733                 if (mjpegUri.isEmpty()) {
1734                     mjpegUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "02" + "/httppreview";
1735                 }
1736                 if (snapshotUri.isEmpty()) {
1737                     snapshotUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "01/picture";
1738                 }
1739                 break;
1740             case INSTAR_THING:
1741                 if (snapshotUri.isEmpty()) {
1742                     snapshotUri = "/tmpfs/snap.jpg";
1743                 }
1744                 if (mjpegUri.isEmpty()) {
1745                     mjpegUri = "/mjpegstream.cgi?-chn=12";
1746                 }
1747                 break;
1748         }
1749
1750         // Onvif and Instar event handling needs the host IP and the server started.
1751         if (cameraConfig.getServerPort() > 0) {
1752             startStreamServer();
1753         }
1754
1755         if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)) {
1756             onvifCamera = new OnvifConnection(this, cameraConfig.getIp() + ":" + cameraConfig.getOnvifPort(),
1757                     cameraConfig.getUser(), cameraConfig.getPassword());
1758             onvifCamera.setSelectedMediaProfile(cameraConfig.getOnvifMediaProfile());
1759             // Only use ONVIF events if it is not an API camera.
1760             onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1761         }
1762
1763         // for poll times 9 seconds and above don't display a warning about the Image channel.
1764         if (9000 > cameraConfig.getPollTime() && cameraConfig.getUpdateImageWhen().contains("1")) {
1765             logger.warn(
1766                     "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.");
1767         }
1768         // Waiting 3 seconds for ONVIF to discover the urls before running.
1769         cameraConnectionJob = threadPool.scheduleWithFixedDelay(this::pollingCameraConnection, 4, 30, TimeUnit.SECONDS);
1770     }
1771
1772     // What the camera needs to re-connect if the initialize() is not called.
1773     private void resetAndRetryConnecting() {
1774         dispose();
1775         initialize();
1776     }
1777
1778     @Override
1779     public void dispose() {
1780         isOnline = false;
1781         snapshotPolling = false;
1782         onvifCamera.disconnect();
1783         Future<?> localFuture = pollCameraJob;
1784         if (localFuture != null) {
1785             localFuture.cancel(true);
1786         }
1787         localFuture = snapshotJob;
1788         if (localFuture != null) {
1789             localFuture.cancel(true);
1790         }
1791         localFuture = cameraConnectionJob;
1792         if (localFuture != null) {
1793             localFuture.cancel(true);
1794         }
1795         threadPool.shutdown();
1796         threadPool = Executors.newScheduledThreadPool(4);
1797
1798         groupTracker.listOfOnlineCameraHandlers.remove(this);
1799         groupTracker.listOfOnlineCameraUID.remove(getThing().getUID().getId());
1800         // inform all group handlers that this camera has gone offline
1801         for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1802             handle.cameraOffline(this);
1803         }
1804         basicAuth = ""; // clear out stored Password hash
1805         useDigestAuth = false;
1806         stopStreamServer();
1807         openChannels.close();
1808
1809         Ffmpeg localFfmpeg = ffmpegHLS;
1810         if (localFfmpeg != null) {
1811             localFfmpeg.stopConverting();
1812             localFfmpeg = null;
1813         }
1814         localFfmpeg = ffmpegRecord;
1815         if (localFfmpeg != null) {
1816             localFfmpeg.stopConverting();
1817         }
1818         localFfmpeg = ffmpegGIF;
1819         if (localFfmpeg != null) {
1820             localFfmpeg.stopConverting();
1821         }
1822         localFfmpeg = ffmpegRtspHelper;
1823         if (localFfmpeg != null) {
1824             localFfmpeg.stopConverting();
1825         }
1826         localFfmpeg = ffmpegMjpeg;
1827         if (localFfmpeg != null) {
1828             localFfmpeg.stopConverting();
1829         }
1830         localFfmpeg = ffmpegSnapshot;
1831         if (localFfmpeg != null) {
1832             localFfmpeg.stopConverting();
1833         }
1834         channelTrackingMap.clear();
1835     }
1836
1837     public void setStreamServerHandler(StreamServerHandler streamServerHandler2) {
1838         streamServerHandler = streamServerHandler2;
1839     }
1840
1841     public String getWhiteList() {
1842         return cameraConfig.getIpWhitelist();
1843     }
1844
1845     @Override
1846     public Collection<Class<? extends ThingHandlerService>> getServices() {
1847         return Collections.singleton(IpCameraActions.class);
1848     }
1849 }