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