2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.ipcamera.internal.handler;
15 import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
18 import java.io.FileNotFoundException;
19 import java.io.FileOutputStream;
20 import java.io.IOException;
21 import java.io.OutputStream;
22 import java.math.BigDecimal;
23 import java.net.InetSocketAddress;
24 import java.net.MalformedURLException;
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;
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;
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.onvif.OnvifConnection;
59 import org.openhab.binding.ipcamera.internal.servlet.CameraServlet;
60 import org.openhab.core.OpenHAB;
61 import org.openhab.core.library.types.DecimalType;
62 import org.openhab.core.library.types.IncreaseDecreaseType;
63 import org.openhab.core.library.types.OnOffType;
64 import org.openhab.core.library.types.PercentType;
65 import org.openhab.core.library.types.RawType;
66 import org.openhab.core.library.types.StringType;
67 import org.openhab.core.thing.ChannelUID;
68 import org.openhab.core.thing.Thing;
69 import org.openhab.core.thing.ThingStatus;
70 import org.openhab.core.thing.ThingStatusDetail;
71 import org.openhab.core.thing.binding.BaseThingHandler;
72 import org.openhab.core.thing.binding.ThingHandlerService;
73 import org.openhab.core.types.Command;
74 import org.openhab.core.types.RefreshType;
75 import org.openhab.core.types.State;
76 import org.osgi.service.http.HttpService;
77 import org.slf4j.Logger;
78 import org.slf4j.LoggerFactory;
80 import io.netty.bootstrap.Bootstrap;
81 import io.netty.buffer.ByteBuf;
82 import io.netty.buffer.Unpooled;
83 import io.netty.channel.Channel;
84 import io.netty.channel.ChannelDuplexHandler;
85 import io.netty.channel.ChannelFuture;
86 import io.netty.channel.ChannelFutureListener;
87 import io.netty.channel.ChannelHandlerContext;
88 import io.netty.channel.ChannelInitializer;
89 import io.netty.channel.ChannelOption;
90 import io.netty.channel.EventLoopGroup;
91 import io.netty.channel.group.ChannelGroup;
92 import io.netty.channel.group.DefaultChannelGroup;
93 import io.netty.channel.nio.NioEventLoopGroup;
94 import io.netty.channel.socket.SocketChannel;
95 import io.netty.channel.socket.nio.NioSocketChannel;
96 import io.netty.handler.codec.base64.Base64;
97 import io.netty.handler.codec.http.DefaultFullHttpRequest;
98 import io.netty.handler.codec.http.FullHttpRequest;
99 import io.netty.handler.codec.http.HttpClientCodec;
100 import io.netty.handler.codec.http.HttpContent;
101 import io.netty.handler.codec.http.HttpHeaderValues;
102 import io.netty.handler.codec.http.HttpMessage;
103 import io.netty.handler.codec.http.HttpMethod;
104 import io.netty.handler.codec.http.HttpResponse;
105 import io.netty.handler.codec.http.HttpVersion;
106 import io.netty.handler.codec.http.LastHttpContent;
107 import io.netty.handler.timeout.IdleState;
108 import io.netty.handler.timeout.IdleStateEvent;
109 import io.netty.handler.timeout.IdleStateHandler;
110 import io.netty.util.CharsetUtil;
111 import io.netty.util.ReferenceCountUtil;
112 import io.netty.util.concurrent.GlobalEventExecutor;
115 * The {@link IpCameraHandler} is responsible for handling commands, which are
116 * sent to one of the channels.
118 * @author Matthew Skinner - Initial contribution
122 public class IpCameraHandler extends BaseThingHandler {
123 public final Logger logger = LoggerFactory.getLogger(getClass());
124 public final IpCameraDynamicStateDescriptionProvider stateDescriptionProvider;
125 private ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(4);
126 private GroupTracker groupTracker;
127 public CameraConfig cameraConfig = new CameraConfig();
129 // ChannelGroup is thread safe
130 public final ChannelGroup openChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
131 private final HttpService httpService;
132 private @Nullable CameraServlet servlet;
133 public String mjpegContentType = "";
134 public @Nullable Ffmpeg ffmpegHLS = null;
135 public @Nullable Ffmpeg ffmpegRecord = null;
136 public @Nullable Ffmpeg ffmpegGIF = null;
137 public @Nullable Ffmpeg ffmpegRtspHelper = null;
138 public @Nullable Ffmpeg ffmpegMjpeg = null;
139 public @Nullable Ffmpeg ffmpegSnapshot = null;
140 public boolean streamingAutoFps = false;
141 public boolean motionDetected = false;
143 private @Nullable ScheduledFuture<?> cameraConnectionJob = null;
144 private @Nullable ScheduledFuture<?> pollCameraJob = null;
145 private @Nullable ScheduledFuture<?> snapshotJob = null;
146 private @Nullable Bootstrap mainBootstrap;
147 private EventLoopGroup mainEventLoopGroup = new NioEventLoopGroup();
148 private FullHttpRequest putRequestWithBody = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod("PUT"),
150 private String gifFilename = "ipcamera";
151 private String gifHistory = "";
152 private String mp4History = "";
153 public int gifHistoryLength;
154 public int mp4HistoryLength;
155 private String mp4Filename = "ipcamera";
156 private int mp4RecordTime;
157 private int gifRecordTime = 5;
158 private LinkedList<byte[]> fifoSnapshotBuffer = new LinkedList<byte[]>();
159 private int snapCount;
160 private boolean updateImageChannel = false;
161 private byte lowPriorityCounter = 0;
162 public String hostIp;
163 public Map<String, ChannelTracking> channelTrackingMap = new ConcurrentHashMap<>();
164 public List<String> lowPriorityRequests = new ArrayList<>(0);
166 // basicAuth MUST remain private as it holds the cameraConfig.getPassword()
167 private String basicAuth = "";
168 public boolean useBasicAuth = false;
169 public boolean useDigestAuth = false;
170 public String snapshotUri = "";
171 public String mjpegUri = "";
172 private byte[] currentSnapshot = new byte[] { (byte) 0x00 };
173 public ReentrantLock lockCurrentSnapshot = new ReentrantLock();
174 public String rtspUri = "";
175 public boolean audioAlarmUpdateSnapshot = false;
176 private boolean motionAlarmUpdateSnapshot = false;
177 private boolean isOnline = false; // Used so only 1 error is logged when a network issue occurs.
178 private boolean firstAudioAlarm = false;
179 private boolean firstMotionAlarm = false;
180 public BigDecimal motionThreshold = BigDecimal.ZERO;
181 public int audioThreshold = 35;
182 public boolean streamingSnapshotMjpeg = false;
183 public boolean motionAlarmEnabled = false;
184 public boolean audioAlarmEnabled = false;
185 public boolean ffmpegSnapshotGeneration = false;
186 public boolean snapshotPolling = false;
187 public OnvifConnection onvifCamera = new OnvifConnection(this, "", "", "");
189 // These methods handle the response from all camera brands, nothing specific to 1 brand.
190 private class CommonCameraHandler extends ChannelDuplexHandler {
191 private int bytesToRecieve = 0;
192 private int bytesAlreadyRecieved = 0;
193 private byte[] incomingJpeg = new byte[0];
194 private String incomingMessage = "";
195 private String contentType = "empty";
196 private String boundary = "";
197 private Object reply = new Object();
198 private String requestUrl = "";
199 private boolean closeConnection = true;
200 private boolean isChunked = false;
202 public void setURL(String url) {
207 public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
208 if (msg == null || ctx == null) {
212 if (msg instanceof HttpResponse) {
213 HttpResponse response = (HttpResponse) msg;
214 if (response.status().code() != 401) {
215 if (!response.headers().isEmpty()) {
216 for (String name : response.headers().names()) {
217 // Some cameras use first letter uppercase and others dont.
218 switch (name.toLowerCase()) { // Possible localization issues doing this
220 contentType = response.headers().getAsString(name);
222 case "content-length":
223 bytesToRecieve = Integer.parseInt(response.headers().getAsString(name));
226 if (response.headers().getAsString(name).contains("keep-alive")) {
227 closeConnection = false;
230 case "transfer-encoding":
231 if (response.headers().getAsString(name).contains("chunked")) {
237 if (contentType.contains("multipart")) {
238 closeConnection = false;
239 if (mjpegUri.equals(requestUrl)) {
240 if (msg instanceof HttpMessage) {
241 // very start of stream only
242 mjpegContentType = contentType;
243 CameraServlet localServlet = servlet;
244 if (localServlet != null) {
245 localServlet.openStreams.updateContentType(contentType);
249 boundary = Helper.searchString(contentType, "boundary=");
251 } else if (contentType.contains("image/jp")) {
252 if (bytesToRecieve == 0) {
253 bytesToRecieve = 768000; // 0.768 Mbyte when no Content-Length is sent
254 logger.debug("Camera has no Content-Length header, we have to guess how much RAM.");
256 incomingJpeg = new byte[bytesToRecieve];
261 if (msg instanceof HttpContent) {
262 if (mjpegUri.equals(requestUrl)) {
263 // multiple MJPEG stream packets come back as this.
264 HttpContent content = (HttpContent) msg;
265 byte[] chunkedFrame = new byte[content.content().readableBytes()];
266 content.content().getBytes(content.content().readerIndex(), chunkedFrame);
267 CameraServlet localServlet = servlet;
268 if (localServlet != null) {
269 localServlet.openStreams.queueFrame(chunkedFrame);
272 HttpContent content = (HttpContent) msg;
273 // Found some cameras use Content-Type: image/jpg instead of image/jpeg
274 if (contentType.contains("image/jp")) {
275 for (int i = 0; i < content.content().capacity(); i++) {
276 incomingJpeg[bytesAlreadyRecieved++] = content.content().getByte(i);
278 if (content instanceof LastHttpContent) {
279 processSnapshot(incomingJpeg);
280 // testing next line and if works need to do a full cleanup of this function.
281 closeConnection = true;
282 if (closeConnection) {
286 bytesAlreadyRecieved = 0;
289 } else { // incomingMessage that is not an IMAGE
290 if (incomingMessage.isEmpty()) {
291 incomingMessage = content.content().toString(CharsetUtil.UTF_8);
293 incomingMessage += content.content().toString(CharsetUtil.UTF_8);
295 bytesAlreadyRecieved = incomingMessage.length();
296 if (content instanceof LastHttpContent) {
297 // If it is not an image send it on to the next handler//
298 if (bytesAlreadyRecieved != 0) {
299 reply = incomingMessage;
300 super.channelRead(ctx, reply);
303 // Alarm Streams never have a LastHttpContent as they always stay open//
304 else if (contentType.contains("multipart")) {
305 int beginIndex, endIndex;
306 if (bytesToRecieve == 0) {
307 beginIndex = incomingMessage.indexOf("Content-Length:");
308 if (beginIndex != -1) {
309 endIndex = incomingMessage.indexOf("\r\n", beginIndex);
310 if (endIndex != -1) {
311 bytesToRecieve = Integer.parseInt(
312 incomingMessage.substring(beginIndex + 15, endIndex).strip());
316 // --boundary and headers are not included in the Content-Length value
317 if (bytesAlreadyRecieved > bytesToRecieve) {
318 // Check if message has a second --boundary
319 endIndex = incomingMessage.indexOf("--" + boundary, bytesToRecieve);
320 if (endIndex == -1) {
321 reply = incomingMessage;
322 incomingMessage = "";
324 bytesAlreadyRecieved = 0;
326 reply = incomingMessage.substring(0, endIndex);
327 incomingMessage = incomingMessage.substring(endIndex, incomingMessage.length());
328 bytesToRecieve = 0;// Triggers search next time for Content-Length:
329 bytesAlreadyRecieved = incomingMessage.length() - endIndex;
331 super.channelRead(ctx, reply);
334 // Foscam needs this as will other cameras with chunks//
335 if (isChunked && bytesAlreadyRecieved != 0) {
336 logger.debug("Reply is chunked.");
337 reply = incomingMessage;
338 super.channelRead(ctx, reply);
342 } else { // msg is not HttpContent
343 // Foscam cameras need this
344 if (!contentType.contains("image/jp") && bytesAlreadyRecieved != 0) {
345 reply = incomingMessage;
346 logger.debug("Packet back from camera is {}", incomingMessage);
347 super.channelRead(ctx, reply);
351 ReferenceCountUtil.release(msg);
356 public void channelReadComplete(@Nullable ChannelHandlerContext ctx) {
360 public void handlerAdded(@Nullable ChannelHandlerContext ctx) {
364 public void handlerRemoved(@Nullable ChannelHandlerContext ctx) {
368 public void exceptionCaught(@Nullable ChannelHandlerContext ctx, @Nullable Throwable cause) {
369 if (cause == null || ctx == null) {
372 if (cause instanceof ArrayIndexOutOfBoundsException) {
373 logger.debug("Camera sent {} bytes when the content-length header was {}.", bytesAlreadyRecieved,
376 logger.warn("!!!! Camera possibly closed the channel on the binding, cause reported is: {}",
383 public void userEventTriggered(@Nullable ChannelHandlerContext ctx, @Nullable Object evt) throws Exception {
387 if (evt instanceof IdleStateEvent) {
388 IdleStateEvent e = (IdleStateEvent) evt;
389 // If camera does not use the channel for X amount of time it will close.
390 if (e.state() == IdleState.READER_IDLE) {
391 String urlToKeepOpen = "";
392 switch (thing.getThingTypeUID().getId()) {
394 urlToKeepOpen = "/cgi-bin/eventManager.cgi?action=attach&codes=[All]";
396 case HIKVISION_THING:
397 urlToKeepOpen = "/ISAPI/Event/notification/alertStream";
400 urlToKeepOpen = "/bha-api/monitor.cgi?ring=doorbell,motionsensor";
403 ChannelTracking channelTracking = channelTrackingMap.get(urlToKeepOpen);
404 if (channelTracking != null) {
405 if (channelTracking.getChannel() == ctx.channel()) {
406 return; // don't auto close this as it is for the alarms.
415 public IpCameraHandler(Thing thing, @Nullable String ipAddress, GroupTracker groupTracker,
416 IpCameraDynamicStateDescriptionProvider stateDescriptionProvider, HttpService httpService) {
418 this.stateDescriptionProvider = stateDescriptionProvider;
419 if (ipAddress != null) {
422 hostIp = Helper.getLocalIpAddress();
424 this.groupTracker = groupTracker;
425 this.httpService = httpService;
428 private IpCameraHandler getHandle() {
432 // false clears the stored user/pass hash, true creates the hash
433 public boolean setBasicAuth(boolean useBasic) {
435 logger.debug("Clearing out the stored BASIC auth now.");
438 } else if (!basicAuth.isEmpty()) {
439 // due to camera may have been sent multiple requests before the auth was set, this may trigger falsely.
440 logger.warn("Camera is reporting your username and/or password is wrong.");
443 if (!cameraConfig.getUser().isEmpty() && !cameraConfig.getPassword().isEmpty()) {
444 String authString = cameraConfig.getUser() + ":" + cameraConfig.getPassword();
445 ByteBuf byteBuf = null;
447 byteBuf = Base64.encode(Unpooled.wrappedBuffer(authString.getBytes(CharsetUtil.UTF_8)));
448 basicAuth = byteBuf.getCharSequence(0, byteBuf.capacity(), CharsetUtil.UTF_8).toString();
450 if (byteBuf != null) {
456 cameraConfigError("Camera is asking for Basic Auth when you have not provided a username and/or password.");
461 private String getCorrectUrlFormat(String longUrl) {
462 String temp = longUrl;
465 if (longUrl.isEmpty() || "ffmpeg".equals(longUrl)) {
470 url = new URL(longUrl);
471 int port = url.getPort();
473 if (url.getQuery() == null) {
474 temp = url.getPath();
476 temp = url.getPath() + "?" + url.getQuery();
479 if (url.getQuery() == null) {
480 temp = ":" + url.getPort() + url.getPath();
482 temp = ":" + url.getPort() + url.getPath() + "?" + url.getQuery();
485 } catch (MalformedURLException e) {
486 cameraConfigError("A non valid URL has been given to the binding, check they work in a browser.");
491 public void sendHttpPUT(String httpRequestURL, FullHttpRequest request) {
492 putRequestWithBody = request; // use Global so the authhandler can use it when resent with DIGEST.
493 sendHttpRequest("PUT", httpRequestURL, null);
496 public void sendHttpGET(String httpRequestURL) {
497 sendHttpRequest("GET", httpRequestURL, null);
500 public int getPortFromShortenedUrl(String httpRequestURL) {
501 if (httpRequestURL.startsWith(":")) {
502 int end = httpRequestURL.indexOf("/");
503 return Integer.parseInt(httpRequestURL.substring(1, end));
505 return cameraConfig.getPort();
508 public String getTinyUrl(String httpRequestURL) {
509 if (httpRequestURL.startsWith(":")) {
510 int beginIndex = httpRequestURL.indexOf("/");
511 return httpRequestURL.substring(beginIndex);
513 return httpRequestURL;
516 private void checkCameraConnection() {
517 Bootstrap localBootstrap = mainBootstrap;
518 if (localBootstrap != null) {
519 ChannelFuture chFuture = localBootstrap
520 .connect(new InetSocketAddress(cameraConfig.getIp(), cameraConfig.getPort()));
521 if (chFuture.awaitUninterruptibly(500)) {
522 chFuture.channel().close();
526 cameraCommunicationError(
527 "Connection Timeout: Check your IP and PORT are correct and the camera can be reached.");
530 // Always use this as sendHttpGET(GET/POST/PUT/DELETE, "/foo/bar",null)//
531 // The authHandler will generate a digest string and re-send using this same function when needed.
532 @SuppressWarnings("null")
533 public void sendHttpRequest(String httpMethod, String httpRequestURLFull, @Nullable String digestString) {
534 int port = getPortFromShortenedUrl(httpRequestURLFull);
535 String httpRequestURL = getTinyUrl(httpRequestURLFull);
537 if (mainBootstrap == null) {
538 mainBootstrap = new Bootstrap();
539 mainBootstrap.group(mainEventLoopGroup);
540 mainBootstrap.channel(NioSocketChannel.class);
541 mainBootstrap.option(ChannelOption.SO_KEEPALIVE, true);
542 mainBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 4500);
543 mainBootstrap.option(ChannelOption.SO_SNDBUF, 1024 * 8);
544 mainBootstrap.option(ChannelOption.SO_RCVBUF, 1024 * 1024);
545 mainBootstrap.option(ChannelOption.TCP_NODELAY, true);
546 mainBootstrap.handler(new ChannelInitializer<SocketChannel>() {
549 public void initChannel(SocketChannel socketChannel) throws Exception {
550 // HIK Alarm stream needs > 9sec idle to stop stream closing
551 socketChannel.pipeline().addLast(new IdleStateHandler(18, 0, 0));
552 socketChannel.pipeline().addLast(new HttpClientCodec());
553 socketChannel.pipeline().addLast(AUTH_HANDLER,
554 new MyNettyAuthHandler(cameraConfig.getUser(), cameraConfig.getPassword(), getHandle()));
555 socketChannel.pipeline().addLast(COMMON_HANDLER, new CommonCameraHandler());
557 switch (thing.getThingTypeUID().getId()) {
559 socketChannel.pipeline().addLast(AMCREST_HANDLER, new AmcrestHandler(getHandle()));
562 socketChannel.pipeline()
563 .addLast(new DahuaHandler(getHandle(), cameraConfig.getNvrChannel()));
566 socketChannel.pipeline().addLast(new DoorBirdHandler(getHandle()));
569 socketChannel.pipeline().addLast(
570 new FoscamHandler(getHandle(), cameraConfig.getUser(), cameraConfig.getPassword()));
572 case HIKVISION_THING:
573 socketChannel.pipeline()
574 .addLast(new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel()));
577 socketChannel.pipeline().addLast(INSTAR_HANDLER, new InstarHandler(getHandle()));
580 socketChannel.pipeline().addLast(new HttpOnlyHandler(getHandle()));
587 FullHttpRequest request;
588 if (!"PUT".equals(httpMethod) || (useDigestAuth && digestString == null)) {
589 request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod(httpMethod), httpRequestURL);
590 request.headers().set("Host", cameraConfig.getIp() + ":" + port);
591 request.headers().set("Connection", HttpHeaderValues.KEEP_ALIVE);
593 request = putRequestWithBody;
596 if (!basicAuth.isEmpty()) {
598 logger.warn("Camera at IP:{} had both Basic and Digest set to be used", cameraConfig.getIp());
601 request.headers().set("Authorization", "Basic " + basicAuth);
606 if (digestString != null) {
607 request.headers().set("Authorization", "Digest " + digestString);
611 mainBootstrap.connect(new InetSocketAddress(cameraConfig.getIp(), port))
612 .addListener(new ChannelFutureListener() {
615 public void operationComplete(@Nullable ChannelFuture future) {
616 if (future == null) {
619 if (future.isDone() && future.isSuccess()) {
620 Channel ch = future.channel();
621 openChannels.add(ch);
625 logger.trace("Sending camera: {}: http://{}:{}{}", httpMethod, cameraConfig.getIp(), port,
628 openChannel(ch, httpRequestURL);
629 CommonCameraHandler commonHandler = (CommonCameraHandler) ch.pipeline().get(COMMON_HANDLER);
630 commonHandler.setURL(httpRequestURLFull);
631 MyNettyAuthHandler authHandler = (MyNettyAuthHandler) ch.pipeline().get(AUTH_HANDLER);
632 authHandler.setURL(httpMethod, httpRequestURL);
634 switch (thing.getThingTypeUID().getId()) {
636 AmcrestHandler amcrestHandler = (AmcrestHandler) ch.pipeline().get(AMCREST_HANDLER);
637 amcrestHandler.setURL(httpRequestURL);
640 InstarHandler instarHandler = (InstarHandler) ch.pipeline().get(INSTAR_HANDLER);
641 instarHandler.setURL(httpRequestURL);
644 ch.writeAndFlush(request);
645 } else { // an error occured
646 cameraCommunicationError(
647 "Connection Timeout: Check your IP and PORT are correct and the camera can be reached.");
653 public void processSnapshot(byte[] incommingSnapshot) {
654 lockCurrentSnapshot.lock();
656 currentSnapshot = incommingSnapshot;
657 if (cameraConfig.getGifPreroll() > 0) {
658 fifoSnapshotBuffer.add(incommingSnapshot);
659 if (fifoSnapshotBuffer.size() > (cameraConfig.getGifPreroll() + gifRecordTime)) {
660 fifoSnapshotBuffer.removeFirst();
664 lockCurrentSnapshot.unlock();
667 if (updateImageChannel) {
668 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
669 } else if (firstMotionAlarm || motionAlarmUpdateSnapshot) {
670 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
671 firstMotionAlarm = motionAlarmUpdateSnapshot = false;
672 } else if (firstAudioAlarm || audioAlarmUpdateSnapshot) {
673 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
674 firstAudioAlarm = audioAlarmUpdateSnapshot = false;
678 public void startStreamServer() {
679 if (servlet == null) {
680 servlet = new CameraServlet(this, httpService);
683 updateState(CHANNEL_HLS_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
684 + getThing().getUID().getId() + "/ipcamera.m3u8"));
685 updateState(CHANNEL_IMAGE_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
686 + getThing().getUID().getId() + "/ipcamera.jpg"));
687 updateState(CHANNEL_MJPEG_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
688 + getThing().getUID().getId() + "/ipcamera.mjpeg"));
691 public void openCamerasStream() {
692 threadPool.schedule(this::openMjpegStream, 500, TimeUnit.MILLISECONDS);
695 private void openMjpegStream() {
696 sendHttpGET(mjpegUri);
699 void openChannel(Channel channel, String httpRequestURL) {
700 ChannelTracking tracker = channelTrackingMap.get(httpRequestURL);
701 if (tracker != null && !tracker.getReply().isEmpty()) {// We need to keep the stored reply
702 tracker.setChannel(channel);
705 channelTrackingMap.put(httpRequestURL, new ChannelTracking(channel, httpRequestURL));
708 public void closeChannel(String url) {
709 ChannelTracking channelTracking = channelTrackingMap.get(url);
710 if (channelTracking != null) {
711 if (channelTracking.getChannel().isOpen()) {
712 channelTracking.getChannel().close();
719 * This method should never run under normal use, if there is a bug in a camera or binding it may be possible to
720 * open large amounts of channels. This may help to keep it under control and WARN the user every 8 seconds this is
723 void cleanChannels() {
724 for (Channel channel : openChannels) {
725 boolean oldChannel = true;
726 for (ChannelTracking channelTracking : channelTrackingMap.values()) {
727 if (!channelTracking.getChannel().isOpen() && channelTracking.getReply().isEmpty()) {
728 channelTrackingMap.remove(channelTracking.getRequestUrl());
730 if (channelTracking.getChannel() == channel) {
731 logger.trace("Open channel to camera is used for URL:{}", channelTracking.getRequestUrl());
741 public void storeHttpReply(String url, String content) {
742 ChannelTracking channelTracking = channelTrackingMap.get(url);
743 if (channelTracking != null) {
744 channelTracking.setReply(content);
748 private void storeSnapshots() {
750 // Need to lock as fifoSnapshotBuffer is not thread safe and new snapshots can be incoming.
751 lockCurrentSnapshot.lock();
753 for (byte[] foo : fifoSnapshotBuffer) {
754 File file = new File(cameraConfig.getFfmpegOutput() + "snapshot" + count + ".jpg");
757 OutputStream fos = new FileOutputStream(file);
760 } catch (FileNotFoundException e) {
761 logger.warn("FileNotFoundException {}", e.getMessage());
762 } catch (IOException e) {
763 logger.warn("IOException {}", e.getMessage());
767 lockCurrentSnapshot.unlock();
771 public void setupFfmpegFormat(FFmpegFormat format) {
772 String inputOptions = cameraConfig.getFfmpegInputOptions();
773 if (cameraConfig.getFfmpegOutput().isEmpty()) {
774 logger.warn("The camera tried to use a FFmpeg feature when the output folder is not set.");
777 if (rtspUri.isEmpty()) {
778 logger.warn("The camera tried to use a FFmpeg feature when no valid input for FFmpeg is provided.");
781 if (cameraConfig.getFfmpegLocation().isEmpty()) {
782 logger.warn("The camera tried to use a FFmpeg feature when the location for FFmpeg is not known.");
785 if (rtspUri.toLowerCase().contains("rtsp")) {
786 if (inputOptions.isEmpty()) {
787 inputOptions = "-rtsp_transport tcp";
791 // Make sure the folder exists, if not create it.
792 new File(cameraConfig.getFfmpegOutput()).mkdirs();
795 if (ffmpegHLS == null) {
796 if (!inputOptions.isEmpty()) {
797 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
798 "-hide_banner -loglevel warning " + inputOptions, rtspUri,
799 cameraConfig.getHlsOutOptions(), cameraConfig.getFfmpegOutput() + "ipcamera.m3u8",
800 cameraConfig.getUser(), cameraConfig.getPassword());
802 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
803 "-hide_banner -loglevel warning", rtspUri, cameraConfig.getHlsOutOptions(),
804 cameraConfig.getFfmpegOutput() + "ipcamera.m3u8", cameraConfig.getUser(),
805 cameraConfig.getPassword());
808 Ffmpeg localHLS = ffmpegHLS;
809 if (localHLS != null) {
810 localHLS.startConverting();
814 if (cameraConfig.getGifPreroll() > 0) {
815 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
816 "-y -r 1 -hide_banner -loglevel warning", cameraConfig.getFfmpegOutput() + "snapshot%d.jpg",
817 "-frames:v " + (cameraConfig.getGifPreroll() + gifRecordTime) + " "
818 + cameraConfig.getGifOutOptions(),
819 cameraConfig.getFfmpegOutput() + gifFilename + ".gif", cameraConfig.getUser(),
820 cameraConfig.getPassword());
822 if (!inputOptions.isEmpty()) {
823 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning " + inputOptions;
825 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning";
827 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
828 cameraConfig.getGifOutOptions(), cameraConfig.getFfmpegOutput() + gifFilename + ".gif",
829 cameraConfig.getUser(), cameraConfig.getPassword());
831 if (cameraConfig.getGifPreroll() > 0) {
834 Ffmpeg localGIF = ffmpegGIF;
835 if (localGIF != null) {
836 localGIF.startConverting();
837 if (gifHistory.isEmpty()) {
838 gifHistory = gifFilename;
839 } else if (!"ipcamera".equals(gifFilename)) {
840 gifHistory = gifFilename + "," + gifHistory;
841 if (gifHistoryLength > 49) {
842 int endIndex = gifHistory.lastIndexOf(",");
843 gifHistory = gifHistory.substring(0, endIndex);
846 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
850 if (!inputOptions.isEmpty()) {
851 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning " + inputOptions;
853 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning";
855 ffmpegRecord = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
856 cameraConfig.getMp4OutOptions(), cameraConfig.getFfmpegOutput() + mp4Filename + ".mp4",
857 cameraConfig.getUser(), cameraConfig.getPassword());
858 Ffmpeg localRecord = ffmpegRecord;
859 if (localRecord != null) {
860 localRecord.startConverting();
861 if (mp4History.isEmpty()) {
862 mp4History = mp4Filename;
863 } else if (!"ipcamera".equals(mp4Filename)) {
864 mp4History = mp4Filename + "," + mp4History;
865 if (mp4HistoryLength > 49) {
866 int endIndex = mp4History.lastIndexOf(",");
867 mp4History = mp4History.substring(0, endIndex);
871 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
874 Ffmpeg localAlarms = ffmpegRtspHelper;
875 if (localAlarms != null) {
876 localAlarms.stopConverting();
877 if (!audioAlarmEnabled && !motionAlarmEnabled) {
881 String input = (cameraConfig.getAlarmInputUrl().isEmpty()) ? rtspUri : cameraConfig.getAlarmInputUrl();
882 String filterOptions = "";
883 if (!audioAlarmEnabled) {
884 filterOptions = "-an";
886 filterOptions = "-af silencedetect=n=-" + audioThreshold + "dB:d=2";
888 if (!motionAlarmEnabled && !ffmpegSnapshotGeneration) {
889 filterOptions = filterOptions.concat(" -vn");
890 } else if (motionAlarmEnabled && !cameraConfig.getMotionOptions().isEmpty()) {
891 String usersMotionOptions = cameraConfig.getMotionOptions();
892 if (usersMotionOptions.startsWith("-")) {
893 // Need to put the users custom options first in the chain before the motion is detected
894 filterOptions += " " + usersMotionOptions + ",select='gte(scene,"
895 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print";
897 filterOptions = filterOptions + " " + usersMotionOptions + " -vf select='gte(scene,"
898 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print";
900 } else if (motionAlarmEnabled) {
901 filterOptions = filterOptions.concat(" -vf select='gte(scene,"
902 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print");
904 ffmpegRtspHelper = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, input,
905 filterOptions, "-f null -", cameraConfig.getUser(), cameraConfig.getPassword());
906 localAlarms = ffmpegRtspHelper;
907 if (localAlarms != null) {
908 localAlarms.startConverting();
912 if (ffmpegMjpeg == null) {
913 if (inputOptions.isEmpty()) {
914 inputOptions = "-hide_banner -loglevel warning";
916 inputOptions += " -hide_banner -loglevel warning";
918 ffmpegMjpeg = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
919 cameraConfig.getMjpegOptions(), "http://127.0.0.1:" + SERVLET_PORT + "/ipcamera/"
920 + getThing().getUID().getId() + "/ipcamera.jpg",
921 cameraConfig.getUser(), cameraConfig.getPassword());
923 Ffmpeg localMjpeg = ffmpegMjpeg;
924 if (localMjpeg != null) {
925 localMjpeg.startConverting();
929 // if mjpeg stream you can use 'ffmpeg -i input -codec:v copy -bsf:v mjpeg2jpeg output.jpg'
930 if (ffmpegSnapshot == null) {
931 if (inputOptions.isEmpty()) {
933 inputOptions = "-threads 1 -skip_frame nokey -hide_banner -loglevel warning";
935 inputOptions += " -threads 1 -skip_frame nokey -hide_banner -loglevel warning";
937 ffmpegSnapshot = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
938 cameraConfig.getSnapshotOptions(), "http://127.0.0.1:" + SERVLET_PORT + "/ipcamera/"
939 + getThing().getUID().getId() + "/snapshot.jpg",
940 cameraConfig.getUser(), cameraConfig.getPassword());
942 Ffmpeg localSnaps = ffmpegSnapshot;
943 if (localSnaps != null) {
944 localSnaps.startConverting();
950 public void noMotionDetected(String thisAlarmsChannel) {
951 setChannelState(thisAlarmsChannel, OnOffType.OFF);
952 firstMotionAlarm = false;
953 motionAlarmUpdateSnapshot = false;
954 motionDetected = false;
955 if (streamingAutoFps) {
956 stopSnapshotPolling();
957 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
958 stopSnapshotPolling();
963 * The {@link changeAlarmState} To only be used to change alarms channels that are not counted as motion. This will
964 * allow logic to be added here in the future. Example more than 1 type of alarm may indicate that someone is
965 * tampering with the camera.
967 public void changeAlarmState(String thisAlarmsChannel, OnOffType state) {
968 updateState(thisAlarmsChannel, state);
971 public void motionDetected(String thisAlarmsChannel) {
972 updateState(CHANNEL_LAST_MOTION_TYPE, new StringType(thisAlarmsChannel));
973 updateState(thisAlarmsChannel, OnOffType.ON);
974 motionDetected = true;
975 if (streamingAutoFps) {
976 startSnapshotPolling();
978 if (cameraConfig.getUpdateImageWhen().contains("2")) {
979 if (!firstMotionAlarm) {
980 if (!snapshotUri.isEmpty()) {
981 sendHttpGET(snapshotUri);
983 firstMotionAlarm = true;// reset back to false when the jpg arrives.
985 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
986 if (!snapshotPolling) {
987 startSnapshotPolling();
989 firstMotionAlarm = true;
990 motionAlarmUpdateSnapshot = true;
994 public void audioDetected() {
995 updateState(CHANNEL_AUDIO_ALARM, OnOffType.ON);
996 if (cameraConfig.getUpdateImageWhen().contains("3")) {
997 if (!firstAudioAlarm) {
998 if (!snapshotUri.isEmpty()) {
999 sendHttpGET(snapshotUri);
1001 firstAudioAlarm = true;// reset back to false when the jpg arrives.
1003 } else if (cameraConfig.getUpdateImageWhen().contains("5")) {// During audio alarms
1004 firstAudioAlarm = true;
1005 audioAlarmUpdateSnapshot = true;
1009 public void noAudioDetected() {
1010 setChannelState(CHANNEL_AUDIO_ALARM, OnOffType.OFF);
1011 firstAudioAlarm = false;
1012 audioAlarmUpdateSnapshot = false;
1015 public void recordMp4(String filename, int seconds) {
1016 mp4Filename = filename;
1017 mp4RecordTime = seconds;
1018 setupFfmpegFormat(FFmpegFormat.RECORD);
1019 setChannelState(CHANNEL_RECORDING_MP4, DecimalType.valueOf(new String("" + seconds)));
1022 public void recordGif(String filename, int seconds) {
1023 gifFilename = filename;
1024 gifRecordTime = seconds;
1025 if (cameraConfig.getGifPreroll() > 0) {
1026 snapCount = seconds;
1028 setupFfmpegFormat(FFmpegFormat.GIF);
1030 setChannelState(CHANNEL_RECORDING_GIF, DecimalType.valueOf(new String("" + seconds)));
1033 public String returnValueFromString(String rawString, String searchedString) {
1035 int index = rawString.indexOf(searchedString);
1036 if (index != -1) // -1 means "not found"
1038 result = rawString.substring(index + searchedString.length(), rawString.length());
1039 index = result.indexOf("\r\n"); // find a carriage return to find the end of the value.
1041 return result; // Did not find a carriage return.
1043 return result.substring(0, index);
1046 return ""; // Did not find the String we were searching for
1049 private void sendPTZRequest() {
1050 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.AbsoluteMove);
1054 public void channelLinked(ChannelUID channelUID) {
1055 switch (channelUID.getId()) {
1056 case CHANNEL_MJPEG_URL:
1057 updateState(CHANNEL_MJPEG_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1058 + getThing().getUID().getId() + "/ipcamera.mjpeg"));
1060 case CHANNEL_HLS_URL:
1061 updateState(CHANNEL_HLS_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1062 + getThing().getUID().getId() + "/ipcamera.m3u8"));
1064 case CHANNEL_IMAGE_URL:
1065 updateState(CHANNEL_IMAGE_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1066 + getThing().getUID().getId() + "/ipcamera.jpg"));
1072 public void handleCommand(ChannelUID channelUID, Command command) {
1073 if (command instanceof RefreshType) {
1074 switch (channelUID.getId()) {
1076 if (onvifCamera.supportsPTZ()) {
1077 updateState(CHANNEL_PAN, new PercentType(Math.round(onvifCamera.getAbsolutePan())));
1081 if (onvifCamera.supportsPTZ()) {
1082 updateState(CHANNEL_TILT, new PercentType(Math.round(onvifCamera.getAbsoluteTilt())));
1086 if (onvifCamera.supportsPTZ()) {
1087 updateState(CHANNEL_ZOOM, new PercentType(Math.round(onvifCamera.getAbsoluteZoom())));
1090 case CHANNEL_GOTO_PRESET:
1091 if (onvifCamera.supportsPTZ()) {
1092 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.GetPresets);
1096 } // caution "REFRESH" can still progress to brand Handlers below the else.
1098 switch (channelUID.getId()) {
1099 case CHANNEL_MP4_HISTORY_LENGTH:
1100 if (DecimalType.ZERO.equals(command)) {
1101 mp4HistoryLength = 0;
1103 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
1106 case CHANNEL_GIF_HISTORY_LENGTH:
1107 if (DecimalType.ZERO.equals(command)) {
1108 gifHistoryLength = 0;
1110 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
1113 case CHANNEL_FFMPEG_MOTION_CONTROL:
1114 if (OnOffType.ON.equals(command)) {
1115 motionAlarmEnabled = true;
1116 } else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
1117 motionAlarmEnabled = false;
1118 noMotionDetected(CHANNEL_FFMPEG_MOTION_ALARM);
1119 } else if (command instanceof PercentType) {
1120 motionAlarmEnabled = true;
1121 motionThreshold = ((PercentType) command).toBigDecimal();
1123 setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
1125 case CHANNEL_START_STREAM:
1127 if (OnOffType.ON.equals(command)) {
1128 localHLS = ffmpegHLS;
1129 if (localHLS == null) {
1130 setupFfmpegFormat(FFmpegFormat.HLS);
1131 localHLS = ffmpegHLS;
1133 if (localHLS != null) {
1134 localHLS.setKeepAlive(-1);// Now will run till manually stopped.
1135 localHLS.startConverting();
1138 localHLS = ffmpegHLS;
1139 if (localHLS != null) {
1140 // Still runs but will be able to auto stop when the HLS stream is no longer used.
1141 localHLS.setKeepAlive(1);
1145 case CHANNEL_EXTERNAL_MOTION:
1146 if (OnOffType.ON.equals(command)) {
1147 motionDetected(CHANNEL_EXTERNAL_MOTION);
1149 noMotionDetected(CHANNEL_EXTERNAL_MOTION);
1152 case CHANNEL_GOTO_PRESET:
1153 if (onvifCamera.supportsPTZ()) {
1154 onvifCamera.gotoPreset(Integer.valueOf(command.toString()));
1157 case CHANNEL_POLL_IMAGE:
1158 if (OnOffType.ON.equals(command)) {
1159 if (snapshotUri.isEmpty()) {
1160 ffmpegSnapshotGeneration = true;
1161 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1162 updateImageChannel = false;
1164 updateImageChannel = true;
1165 sendHttpGET(snapshotUri);// Allows this to change Image FPS on demand
1168 Ffmpeg localSnaps = ffmpegSnapshot;
1169 if (localSnaps != null) {
1170 localSnaps.stopConverting();
1171 ffmpegSnapshotGeneration = false;
1173 updateImageChannel = false;
1177 if (onvifCamera.supportsPTZ()) {
1178 if (command instanceof IncreaseDecreaseType) {
1179 if (command == IncreaseDecreaseType.INCREASE) {
1180 if (cameraConfig.getPtzContinuous()) {
1181 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveLeft);
1183 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveLeft);
1186 if (cameraConfig.getPtzContinuous()) {
1187 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveRight);
1189 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveRight);
1193 } else if (OnOffType.OFF.equals(command)) {
1194 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1197 onvifCamera.setAbsolutePan(Float.valueOf(command.toString()));
1198 threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1202 if (onvifCamera.supportsPTZ()) {
1203 if (command instanceof IncreaseDecreaseType) {
1204 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1205 if (cameraConfig.getPtzContinuous()) {
1206 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveUp);
1208 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveUp);
1211 if (cameraConfig.getPtzContinuous()) {
1212 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveDown);
1214 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveDown);
1218 } else if (OnOffType.OFF.equals(command)) {
1219 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1222 onvifCamera.setAbsoluteTilt(Float.valueOf(command.toString()));
1223 threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1227 if (onvifCamera.supportsPTZ()) {
1228 if (command instanceof IncreaseDecreaseType) {
1229 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1230 if (cameraConfig.getPtzContinuous()) {
1231 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveIn);
1233 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveIn);
1236 if (cameraConfig.getPtzContinuous()) {
1237 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveOut);
1239 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveOut);
1243 } else if (OnOffType.OFF.equals(command)) {
1244 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1247 onvifCamera.setAbsoluteZoom(Float.valueOf(command.toString()));
1248 threadPool.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1253 // commands and refresh now get passed to brand handlers
1254 switch (thing.getThingTypeUID().getId()) {
1256 AmcrestHandler amcrestHandler = new AmcrestHandler(getHandle());
1257 amcrestHandler.handleCommand(channelUID, command);
1258 if (lowPriorityRequests.isEmpty()) {
1259 lowPriorityRequests = amcrestHandler.getLowPriorityRequests();
1263 DahuaHandler dahuaHandler = new DahuaHandler(getHandle(), cameraConfig.getNvrChannel());
1264 dahuaHandler.handleCommand(channelUID, command);
1265 if (lowPriorityRequests.isEmpty()) {
1266 lowPriorityRequests = dahuaHandler.getLowPriorityRequests();
1269 case DOORBIRD_THING:
1270 DoorBirdHandler doorBirdHandler = new DoorBirdHandler(getHandle());
1271 doorBirdHandler.handleCommand(channelUID, command);
1272 if (lowPriorityRequests.isEmpty()) {
1273 lowPriorityRequests = doorBirdHandler.getLowPriorityRequests();
1276 case HIKVISION_THING:
1277 HikvisionHandler hikvisionHandler = new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel());
1278 hikvisionHandler.handleCommand(channelUID, command);
1279 if (lowPriorityRequests.isEmpty()) {
1280 lowPriorityRequests = hikvisionHandler.getLowPriorityRequests();
1284 FoscamHandler foscamHandler = new FoscamHandler(getHandle(), cameraConfig.getUser(),
1285 cameraConfig.getPassword());
1286 foscamHandler.handleCommand(channelUID, command);
1287 if (lowPriorityRequests.isEmpty()) {
1288 lowPriorityRequests = foscamHandler.getLowPriorityRequests();
1292 InstarHandler instarHandler = new InstarHandler(getHandle());
1293 instarHandler.handleCommand(channelUID, command);
1294 if (lowPriorityRequests.isEmpty()) {
1295 lowPriorityRequests = instarHandler.getLowPriorityRequests();
1299 HttpOnlyHandler defaultHandler = new HttpOnlyHandler(getHandle());
1300 defaultHandler.handleCommand(channelUID, command);
1301 if (lowPriorityRequests.isEmpty()) {
1302 lowPriorityRequests = defaultHandler.getLowPriorityRequests();
1308 public void setChannelState(String channelToUpdate, State valueOf) {
1309 updateState(channelToUpdate, valueOf);
1312 private void bringCameraOnline() {
1314 updateStatus(ThingStatus.ONLINE);
1315 groupTracker.listOfOnlineCameraHandlers.add(this);
1316 groupTracker.listOfOnlineCameraUID.add(getThing().getUID().getId());
1317 Future<?> localFuture = cameraConnectionJob;
1318 if (localFuture != null) {
1319 localFuture.cancel(false);
1321 if (thing.getThingTypeUID().getId().equals(INSTAR_THING)) {
1322 logger.debug("Setting up the Alarm Server settings in the camera now");
1324 "/param.cgi?cmd=setmdalarm&-aname=server2&-switch=on&-interval=1&cmd=setalarmserverattr&-as_index=3&-as_server="
1325 + hostIp + "&-as_port=" + SERVLET_PORT + "&-as_path=/ipcamera/"
1326 + getThing().getUID().getId()
1327 + "/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");
1329 if (cameraConfig.getGifPreroll() > 0 || cameraConfig.getUpdateImageWhen().contains("1")) {
1330 snapshotPolling = true;
1331 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 1000, cameraConfig.getPollTime(),
1332 TimeUnit.MILLISECONDS);
1335 pollCameraJob = threadPool.scheduleWithFixedDelay(this::pollCameraRunnable, 1000, 8000, TimeUnit.MILLISECONDS);
1337 if (!rtspUri.isEmpty()) {
1338 updateState(CHANNEL_RTSP_URL, new StringType(rtspUri));
1340 if (updateImageChannel) {
1341 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1343 updateState(CHANNEL_POLL_IMAGE, OnOffType.OFF);
1345 if (!groupTracker.listOfGroupHandlers.isEmpty()) {
1346 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1347 handle.cameraOnline(getThing().getUID().getId());
1352 void snapshotIsFfmpeg() {
1353 bringCameraOnline();
1354 snapshotUri = "";// ffmpeg is a valid option. Simplify further checks.
1356 "Binding has no snapshot url. Will use your CPU and FFmpeg to create snapshots from the cameras RTSP.");
1357 if (!rtspUri.isEmpty()) {
1358 updateImageChannel = false;
1359 ffmpegSnapshotGeneration = true;
1360 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1361 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1363 cameraConfigError("Binding can not find a RTSP url for this camera, please provide a FFmpeg Input URL.");
1367 void pollingCameraConnection() {
1368 if (thing.getThingTypeUID().getId().equals(GENERIC_THING)) {
1369 if (rtspUri.isEmpty()) {
1370 logger.warn("Binding has not been supplied with a FFmpeg Input URL, so some features will not work.");
1372 if (snapshotUri.isEmpty() || "ffmpeg".equals(snapshotUri)) {
1375 sendHttpRequest("GET", snapshotUri, null);
1379 if (!onvifCamera.isConnected()) {
1380 logger.debug("About to connect to the IP Camera using the ONVIF PORT at IP:{}:{}", cameraConfig.getIp(),
1381 cameraConfig.getOnvifPort());
1382 onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1384 if ("ffmpeg".equals(snapshotUri)) {
1386 } else if (!snapshotUri.isEmpty()) {
1387 sendHttpRequest("GET", snapshotUri, null);
1388 } else if (!rtspUri.isEmpty()) {
1391 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
1392 "Camera failed to report a valid Snaphot and/or RTSP URL. See readme on how to use the SNAPSHOT_URL_OVERRIDE feature.");
1396 public void cameraConfigError(String reason) {
1397 // wont try to reconnect again due to a config error being the cause.
1398 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, reason);
1402 public void cameraCommunicationError(String reason) {
1403 // will try to reconnect again as camera may be rebooting.
1404 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
1405 if (isOnline) {// if already offline dont try reconnecting in 6 seconds, we want 30sec wait.
1406 resetAndRetryConnecting();
1410 boolean streamIsStopped(String url) {
1411 ChannelTracking channelTracking = channelTrackingMap.get(url);
1412 if (channelTracking != null) {
1413 if (channelTracking.getChannel().isActive()) {
1414 return false; // stream is running.
1417 return true; // Stream stopped or never started.
1420 void snapshotRunnable() {
1421 // Snapshot should be first to keep consistent time between shots
1422 sendHttpGET(snapshotUri);
1423 if (snapCount > 0) {
1424 if (--snapCount == 0) {
1425 setupFfmpegFormat(FFmpegFormat.GIF);
1430 public byte[] getSnapshot() {
1431 if (!snapshotPolling && !ffmpegSnapshotGeneration) {
1432 sendHttpGET(snapshotUri);
1434 lockCurrentSnapshot.lock();
1436 return currentSnapshot;
1438 lockCurrentSnapshot.unlock();
1442 public void stopSnapshotPolling() {
1443 Future<?> localFuture;
1444 if (!streamingSnapshotMjpeg && cameraConfig.getGifPreroll() == 0
1445 && !cameraConfig.getUpdateImageWhen().contains("1")) {
1446 snapshotPolling = false;
1447 localFuture = snapshotJob;
1448 if (localFuture != null) {
1449 localFuture.cancel(true);
1451 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // only during Motion Alarms
1452 snapshotPolling = false;
1453 localFuture = snapshotJob;
1454 if (localFuture != null) {
1455 localFuture.cancel(true);
1460 public void startSnapshotPolling() {
1461 if (snapshotPolling || ffmpegSnapshotGeneration) {
1462 return; // Already polling or creating with FFmpeg from RTSP
1464 if (streamingSnapshotMjpeg || streamingAutoFps) {
1465 snapshotPolling = true;
1466 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 200, cameraConfig.getPollTime(),
1467 TimeUnit.MILLISECONDS);
1468 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
1469 snapshotPolling = true;
1470 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 200, cameraConfig.getPollTime(),
1471 TimeUnit.MILLISECONDS);
1476 * {@link pollCameraRunnable} Polls every 8 seconds, to check camera is still ONLINE and keep mjpeg and alarm
1477 * streams open and more.
1480 void pollCameraRunnable() {
1481 // Snapshot should be first to keep consistent time between shots
1482 if (streamingAutoFps) {
1483 if (!snapshotPolling && !ffmpegSnapshotGeneration) {
1484 // Dont need to poll if creating from RTSP stream with FFmpeg or we are polling at full rate already.
1485 sendHttpGET(snapshotUri);
1487 } else if (!snapshotUri.isEmpty() && !snapshotPolling) {// we need to check camera is still online.
1488 checkCameraConnection();
1490 // NOTE: Use lowPriorityRequests if get request is not needed every poll.
1491 if (!lowPriorityRequests.isEmpty()) {
1492 if (lowPriorityCounter >= lowPriorityRequests.size()) {
1493 lowPriorityCounter = 0;
1495 sendHttpGET(lowPriorityRequests.get(lowPriorityCounter++));
1497 // what needs to be done every poll//
1498 switch (thing.getThingTypeUID().getId()) {
1502 if (!onvifCamera.isConnected()) {
1503 onvifCamera.connect(true);
1507 noMotionDetected(CHANNEL_MOTION_ALARM);
1508 noMotionDetected(CHANNEL_PIR_ALARM);
1511 case HIKVISION_THING:
1512 if (streamIsStopped("/ISAPI/Event/notification/alertStream")) {
1513 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1514 cameraConfig.getIp());
1515 sendHttpGET("/ISAPI/Event/notification/alertStream");
1519 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=VideoMotion");
1520 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=AudioMutation");
1523 // Check for alarms, channel for NVRs appears not to work at filtering.
1524 if (streamIsStopped("/cgi-bin/eventManager.cgi?action=attach&codes=[All]")) {
1525 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1526 cameraConfig.getIp());
1527 sendHttpGET("/cgi-bin/eventManager.cgi?action=attach&codes=[All]");
1530 case DOORBIRD_THING:
1531 // Check for alarms, channel for NVRs appears not to work at filtering.
1532 if (streamIsStopped("/bha-api/monitor.cgi?ring=doorbell,motionsensor")) {
1533 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1534 cameraConfig.getIp());
1535 sendHttpGET("/bha-api/monitor.cgi?ring=doorbell,motionsensor");
1539 sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=getDevState&usr=" + cameraConfig.getUser() + "&pwd="
1540 + cameraConfig.getPassword());
1543 Ffmpeg localHLS = ffmpegHLS;
1544 if (localHLS != null) {
1545 localHLS.checkKeepAlive();
1547 if (openChannels.size() > 18) {
1548 logger.debug("There are {} open Channels being tracked.", openChannels.size());
1554 public void initialize() {
1555 cameraConfig = getConfigAs(CameraConfig.class);
1556 snapshotUri = getCorrectUrlFormat(cameraConfig.getSnapshotUrl());
1557 mjpegUri = getCorrectUrlFormat(cameraConfig.getMjpegUrl());
1558 rtspUri = cameraConfig.getFfmpegInput();
1559 if (cameraConfig.getFfmpegOutput().isEmpty()) {
1561 .setFfmpegOutput(OpenHAB.getUserDataFolder() + "/ipcamera/" + this.thing.getUID().getId() + "/");
1563 // Known cameras will connect quicker if we skip ONVIF questions.
1564 switch (thing.getThingTypeUID().getId()) {
1567 if (mjpegUri.isEmpty()) {
1568 mjpegUri = "/cgi-bin/mjpg/video.cgi?channel=" + cameraConfig.getNvrChannel() + "&subtype=1";
1570 if (snapshotUri.isEmpty()) {
1571 snapshotUri = "/cgi-bin/snapshot.cgi?channel=" + cameraConfig.getNvrChannel();
1574 case DOORBIRD_THING:
1575 if (mjpegUri.isEmpty()) {
1576 mjpegUri = "/bha-api/video.cgi";
1578 if (snapshotUri.isEmpty()) {
1579 snapshotUri = "/bha-api/image.cgi";
1583 // Foscam needs any special char like spaces (%20) to be encoded for URLs.
1584 cameraConfig.setUser(Helper.encodeSpecialChars(cameraConfig.getUser()));
1585 cameraConfig.setPassword(Helper.encodeSpecialChars(cameraConfig.getPassword()));
1586 if (mjpegUri.isEmpty()) {
1587 mjpegUri = "/cgi-bin/CGIStream.cgi?cmd=GetMJStream&usr=" + cameraConfig.getUser() + "&pwd="
1588 + cameraConfig.getPassword();
1590 if (snapshotUri.isEmpty()) {
1591 snapshotUri = "/cgi-bin/CGIProxy.fcgi?usr=" + cameraConfig.getUser() + "&pwd="
1592 + cameraConfig.getPassword() + "&cmd=snapPicture2";
1595 case HIKVISION_THING:// The 02 gives you the first sub stream which needs to be set to MJPEG
1596 if (mjpegUri.isEmpty()) {
1597 mjpegUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "02" + "/httppreview";
1599 if (snapshotUri.isEmpty()) {
1600 snapshotUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "01/picture";
1604 if (snapshotUri.isEmpty()) {
1605 snapshotUri = "/tmpfs/snap.jpg";
1607 if (mjpegUri.isEmpty()) {
1608 mjpegUri = "/mjpegstream.cgi?-chn=12";
1612 // Onvif and Instar event handling need the host IP and the server started.
1613 startStreamServer();
1615 if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)) {
1616 onvifCamera = new OnvifConnection(this, cameraConfig.getIp() + ":" + cameraConfig.getOnvifPort(),
1617 cameraConfig.getUser(), cameraConfig.getPassword());
1618 onvifCamera.setSelectedMediaProfile(cameraConfig.getOnvifMediaProfile());
1619 // Only use ONVIF events if it is not an API camera.
1620 onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1623 // for poll times 9 seconds and above don't display a warning about the Image channel.
1624 if (9000 > cameraConfig.getPollTime() && cameraConfig.getUpdateImageWhen().contains("1")) {
1626 "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.");
1628 // Waiting 3 seconds for ONVIF to discover the urls before running.
1629 cameraConnectionJob = threadPool.scheduleWithFixedDelay(this::pollingCameraConnection, 4, 30, TimeUnit.SECONDS);
1632 // What the camera needs to re-connect if the initialize() is not called.
1633 private void resetAndRetryConnecting() {
1639 public void dispose() {
1641 snapshotPolling = false;
1642 Future<?> localFuture = pollCameraJob;
1643 if (localFuture != null) {
1644 localFuture.cancel(true);
1646 localFuture = snapshotJob;
1647 if (localFuture != null) {
1648 localFuture.cancel(true);
1650 localFuture = cameraConnectionJob;
1651 if (localFuture != null) {
1652 localFuture.cancel(true);
1654 threadPool.shutdown();
1655 threadPool = Executors.newScheduledThreadPool(4);
1657 groupTracker.listOfOnlineCameraHandlers.remove(this);
1658 groupTracker.listOfOnlineCameraUID.remove(getThing().getUID().getId());
1659 // inform all group handlers that this camera has gone offline
1660 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1661 handle.cameraOffline(this);
1663 basicAuth = ""; // clear out stored Password hash
1664 useDigestAuth = false;
1665 openChannels.close();
1667 Ffmpeg localFfmpeg = ffmpegHLS;
1668 if (localFfmpeg != null) {
1669 localFfmpeg.stopConverting();
1672 localFfmpeg = ffmpegRecord;
1673 if (localFfmpeg != null) {
1674 localFfmpeg.stopConverting();
1676 localFfmpeg = ffmpegGIF;
1677 if (localFfmpeg != null) {
1678 localFfmpeg.stopConverting();
1680 localFfmpeg = ffmpegRtspHelper;
1681 if (localFfmpeg != null) {
1682 localFfmpeg.stopConverting();
1684 localFfmpeg = ffmpegMjpeg;
1685 if (localFfmpeg != null) {
1686 localFfmpeg.stopConverting();
1688 localFfmpeg = ffmpegSnapshot;
1689 if (localFfmpeg != null) {
1690 localFfmpeg.stopConverting();
1692 channelTrackingMap.clear();
1693 onvifCamera.disconnect();
1696 public String getWhiteList() {
1697 return cameraConfig.getIpWhitelist();
1701 public Collection<Class<? extends ThingHandlerService>> getServices() {
1702 return Collections.singleton(IpCameraActions.class);