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.time.Duration;
27 import java.time.Instant;
28 import java.util.ArrayList;
29 import java.util.Collection;
30 import java.util.Collections;
31 import java.util.LinkedList;
32 import java.util.List;
34 import java.util.concurrent.ConcurrentHashMap;
35 import java.util.concurrent.Executors;
36 import java.util.concurrent.Future;
37 import java.util.concurrent.ScheduledExecutorService;
38 import java.util.concurrent.ScheduledFuture;
39 import java.util.concurrent.TimeUnit;
40 import java.util.concurrent.locks.ReentrantLock;
42 import org.eclipse.jdt.annotation.NonNullByDefault;
43 import org.eclipse.jdt.annotation.Nullable;
44 import org.openhab.binding.ipcamera.internal.AmcrestHandler;
45 import org.openhab.binding.ipcamera.internal.CameraConfig;
46 import org.openhab.binding.ipcamera.internal.ChannelTracking;
47 import org.openhab.binding.ipcamera.internal.DahuaHandler;
48 import org.openhab.binding.ipcamera.internal.DoorBirdHandler;
49 import org.openhab.binding.ipcamera.internal.Ffmpeg;
50 import org.openhab.binding.ipcamera.internal.FoscamHandler;
51 import org.openhab.binding.ipcamera.internal.GroupTracker;
52 import org.openhab.binding.ipcamera.internal.Helper;
53 import org.openhab.binding.ipcamera.internal.HikvisionHandler;
54 import org.openhab.binding.ipcamera.internal.HttpOnlyHandler;
55 import org.openhab.binding.ipcamera.internal.InstarHandler;
56 import org.openhab.binding.ipcamera.internal.IpCameraActions;
57 import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
58 import org.openhab.binding.ipcamera.internal.IpCameraDynamicStateDescriptionProvider;
59 import org.openhab.binding.ipcamera.internal.MyNettyAuthHandler;
60 import org.openhab.binding.ipcamera.internal.onvif.OnvifConnection;
61 import org.openhab.binding.ipcamera.internal.servlet.CameraServlet;
62 import org.openhab.core.OpenHAB;
63 import org.openhab.core.library.types.DecimalType;
64 import org.openhab.core.library.types.IncreaseDecreaseType;
65 import org.openhab.core.library.types.OnOffType;
66 import org.openhab.core.library.types.PercentType;
67 import org.openhab.core.library.types.RawType;
68 import org.openhab.core.library.types.StringType;
69 import org.openhab.core.thing.ChannelUID;
70 import org.openhab.core.thing.Thing;
71 import org.openhab.core.thing.ThingStatus;
72 import org.openhab.core.thing.ThingStatusDetail;
73 import org.openhab.core.thing.binding.BaseThingHandler;
74 import org.openhab.core.thing.binding.ThingHandlerService;
75 import org.openhab.core.types.Command;
76 import org.openhab.core.types.RefreshType;
77 import org.openhab.core.types.State;
78 import org.osgi.service.http.HttpService;
79 import org.slf4j.Logger;
80 import org.slf4j.LoggerFactory;
82 import io.netty.bootstrap.Bootstrap;
83 import io.netty.buffer.ByteBuf;
84 import io.netty.buffer.Unpooled;
85 import io.netty.channel.Channel;
86 import io.netty.channel.ChannelDuplexHandler;
87 import io.netty.channel.ChannelFuture;
88 import io.netty.channel.ChannelFutureListener;
89 import io.netty.channel.ChannelHandlerContext;
90 import io.netty.channel.ChannelInitializer;
91 import io.netty.channel.ChannelOption;
92 import io.netty.channel.EventLoopGroup;
93 import io.netty.channel.group.ChannelGroup;
94 import io.netty.channel.group.DefaultChannelGroup;
95 import io.netty.channel.nio.NioEventLoopGroup;
96 import io.netty.channel.socket.SocketChannel;
97 import io.netty.channel.socket.nio.NioSocketChannel;
98 import io.netty.handler.codec.base64.Base64;
99 import io.netty.handler.codec.http.DefaultFullHttpRequest;
100 import io.netty.handler.codec.http.FullHttpRequest;
101 import io.netty.handler.codec.http.HttpClientCodec;
102 import io.netty.handler.codec.http.HttpContent;
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.HttpVersion;
108 import io.netty.handler.codec.http.LastHttpContent;
109 import io.netty.handler.timeout.IdleState;
110 import io.netty.handler.timeout.IdleStateEvent;
111 import io.netty.handler.timeout.IdleStateHandler;
112 import io.netty.util.CharsetUtil;
113 import io.netty.util.ReferenceCountUtil;
114 import io.netty.util.concurrent.GlobalEventExecutor;
117 * The {@link IpCameraHandler} is responsible for handling commands, which are
118 * sent to one of the channels.
120 * @author Matthew Skinner - Initial contribution
124 public class IpCameraHandler extends BaseThingHandler {
125 public final Logger logger = LoggerFactory.getLogger(getClass());
126 public final IpCameraDynamicStateDescriptionProvider stateDescriptionProvider;
127 private ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(2);
128 private GroupTracker groupTracker;
129 public CameraConfig cameraConfig = new CameraConfig();
131 // ChannelGroup is thread safe
132 public final ChannelGroup openChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
133 private final HttpService httpService;
134 private @Nullable CameraServlet servlet;
135 public String mjpegContentType = "";
136 public @Nullable Ffmpeg ffmpegHLS = null;
137 public @Nullable Ffmpeg ffmpegRecord = null;
138 public @Nullable Ffmpeg ffmpegGIF = null;
139 public @Nullable Ffmpeg ffmpegRtspHelper = null;
140 public @Nullable Ffmpeg ffmpegMjpeg = null;
141 public @Nullable Ffmpeg ffmpegSnapshot = null;
142 public boolean streamingAutoFps = false;
143 public boolean motionDetected = false;
144 public Instant lastSnapshotRequest = Instant.now();
145 public Instant currentSnapshotTime = Instant.now();
146 private @Nullable ScheduledFuture<?> cameraConnectionJob = null;
147 private @Nullable ScheduledFuture<?> pollCameraJob = null;
148 private @Nullable ScheduledFuture<?> snapshotJob = null;
149 private @Nullable Bootstrap mainBootstrap;
150 private EventLoopGroup mainEventLoopGroup = new NioEventLoopGroup(1);
151 private FullHttpRequest putRequestWithBody = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod("PUT"),
153 private String gifFilename = "ipcamera";
154 private String gifHistory = "";
155 private String mp4History = "";
156 public int gifHistoryLength;
157 public int mp4HistoryLength;
158 private String mp4Filename = "ipcamera";
159 private int mp4RecordTime;
160 private int gifRecordTime = 5;
161 private LinkedList<byte[]> fifoSnapshotBuffer = new LinkedList<byte[]>();
162 private int snapCount;
163 private boolean updateImageChannel = false;
164 private byte lowPriorityCounter = 0;
165 public String hostIp;
166 public Map<String, ChannelTracking> channelTrackingMap = new ConcurrentHashMap<>();
167 public List<String> lowPriorityRequests = new ArrayList<>(0);
169 // basicAuth MUST remain private as it holds the cameraConfig.getPassword()
170 private String basicAuth = "";
171 public boolean useBasicAuth = false;
172 public boolean useDigestAuth = false;
173 public String snapshotUri = "";
174 public String mjpegUri = "";
175 private byte[] currentSnapshot = new byte[] { (byte) 0x00 };
176 public ReentrantLock lockCurrentSnapshot = new ReentrantLock();
177 public String rtspUri = "";
178 public boolean audioAlarmUpdateSnapshot = false;
179 private boolean motionAlarmUpdateSnapshot = false;
180 private boolean isOnline = false; // Used so only 1 error is logged when a network issue occurs.
181 private boolean firstAudioAlarm = false;
182 private boolean firstMotionAlarm = false;
183 public BigDecimal motionThreshold = BigDecimal.ZERO;
184 public int audioThreshold = 35;
185 public boolean streamingSnapshotMjpeg = false;
186 public boolean motionAlarmEnabled = false;
187 public boolean audioAlarmEnabled = false;
188 public boolean ffmpegSnapshotGeneration = false;
189 public boolean snapshotPolling = false;
190 public OnvifConnection onvifCamera = new OnvifConnection(this, "", "", "");
192 // These methods handle the response from all camera brands, nothing specific to 1 brand.
193 private class CommonCameraHandler extends ChannelDuplexHandler {
194 private int bytesToRecieve = 0;
195 private int bytesAlreadyRecieved = 0;
196 private byte[] incomingJpeg = new byte[0];
197 private String incomingMessage = "";
198 private String contentType = "empty";
199 private String boundary = "";
200 private Object reply = new Object();
201 private String requestUrl = "";
202 private boolean isChunked = false;
204 public void setURL(String url) {
209 public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
210 if (msg == null || ctx == null) {
214 if (msg instanceof HttpResponse) {
215 HttpResponse response = (HttpResponse) msg;
216 if (response.status().code() != 401) {
217 if (!response.headers().isEmpty()) {
218 for (String name : response.headers().names()) {
219 // Some cameras use first letter uppercase and others dont.
220 switch (name.toLowerCase()) { // Possible localization issues doing this
222 contentType = response.headers().getAsString(name);
224 case "content-length":
225 bytesToRecieve = Integer.parseInt(response.headers().getAsString(name));
227 case "transfer-encoding":
228 if (response.headers().getAsString(name).contains("chunked")) {
234 if (contentType.contains("multipart")) {
235 if (mjpegUri.equals(requestUrl)) {
236 if (msg instanceof HttpMessage) {
237 // very start of stream only
238 mjpegContentType = contentType;
239 CameraServlet localServlet = servlet;
240 if (localServlet != null) {
241 localServlet.openStreams.updateContentType(contentType);
245 boundary = Helper.searchString(contentType, "boundary=");
247 } else if (contentType.contains("image/jp")) {
248 if (bytesToRecieve == 0) {
249 bytesToRecieve = 768000; // 0.768 Mbyte when no Content-Length is sent
250 logger.debug("Camera has no Content-Length header, we have to guess how much RAM.");
252 incomingJpeg = new byte[bytesToRecieve];
257 if (msg instanceof HttpContent) {
258 if (mjpegUri.equals(requestUrl)) {
259 // multiple MJPEG stream packets come back as this.
260 HttpContent content = (HttpContent) msg;
261 byte[] chunkedFrame = new byte[content.content().readableBytes()];
262 content.content().getBytes(content.content().readerIndex(), chunkedFrame);
263 CameraServlet localServlet = servlet;
264 if (localServlet != null) {
265 localServlet.openStreams.queueFrame(chunkedFrame);
268 HttpContent content = (HttpContent) msg;
269 // Found some cameras use Content-Type: image/jpg instead of image/jpeg
270 if (contentType.contains("image/jp")) {
271 for (int i = 0; i < content.content().capacity(); i++) {
272 incomingJpeg[bytesAlreadyRecieved++] = content.content().getByte(i);
274 if (content instanceof LastHttpContent) {
275 processSnapshot(incomingJpeg);
278 } else { // incomingMessage that is not an IMAGE
279 if (incomingMessage.isEmpty()) {
280 incomingMessage = content.content().toString(CharsetUtil.UTF_8);
282 incomingMessage += content.content().toString(CharsetUtil.UTF_8);
284 bytesAlreadyRecieved = incomingMessage.length();
285 if (content instanceof LastHttpContent) {
286 // If it is not an image send it on to the next handler//
287 if (bytesAlreadyRecieved != 0) {
288 reply = incomingMessage;
289 super.channelRead(ctx, reply);
292 // Alarm Streams never have a LastHttpContent as they always stay open//
293 else if (contentType.contains("multipart")) {
294 int beginIndex, endIndex;
295 if (bytesToRecieve == 0) {
296 beginIndex = incomingMessage.indexOf("Content-Length:");
297 if (beginIndex != -1) {
298 endIndex = incomingMessage.indexOf("\r\n", beginIndex);
299 if (endIndex != -1) {
300 bytesToRecieve = Integer.parseInt(
301 incomingMessage.substring(beginIndex + 15, endIndex).strip());
305 // --boundary and headers are not included in the Content-Length value
306 if (bytesAlreadyRecieved > bytesToRecieve) {
307 // Check if message has a second --boundary
308 endIndex = incomingMessage.indexOf("--" + boundary, bytesToRecieve);
309 if (endIndex == -1) {
310 reply = incomingMessage;
311 incomingMessage = "";
313 bytesAlreadyRecieved = 0;
315 reply = incomingMessage.substring(0, endIndex);
316 incomingMessage = incomingMessage.substring(endIndex, incomingMessage.length());
317 bytesToRecieve = 0;// Triggers search next time for Content-Length:
318 bytesAlreadyRecieved = incomingMessage.length() - endIndex;
320 super.channelRead(ctx, reply);
323 // Foscam needs this as will other cameras with chunks//
324 if (isChunked && bytesAlreadyRecieved != 0) {
325 logger.debug("Reply is chunked.");
326 reply = incomingMessage;
327 super.channelRead(ctx, reply);
331 } else { // msg is not HttpContent
332 // Foscam cameras need this
333 if (!contentType.contains("image/jp") && bytesAlreadyRecieved != 0) {
334 reply = incomingMessage;
335 logger.debug("Packet back from camera is {}", incomingMessage);
336 super.channelRead(ctx, reply);
340 ReferenceCountUtil.release(msg);
345 public void exceptionCaught(@Nullable ChannelHandlerContext ctx, @Nullable Throwable cause) {
346 if (cause == null || ctx == null) {
349 if (cause instanceof ArrayIndexOutOfBoundsException) {
350 logger.debug("Camera sent {} bytes when the content-length header was {}.", bytesAlreadyRecieved,
353 logger.warn("!!!! Camera possibly closed the channel on the binding, cause reported is: {}",
360 public void userEventTriggered(@Nullable ChannelHandlerContext ctx, @Nullable Object evt) throws Exception {
364 if (evt instanceof IdleStateEvent) {
365 IdleStateEvent e = (IdleStateEvent) evt;
366 // If camera does not use the channel for X amount of time it will close.
367 if (e.state() == IdleState.READER_IDLE) {
368 String urlToKeepOpen = "";
369 switch (thing.getThingTypeUID().getId()) {
371 urlToKeepOpen = "/cgi-bin/eventManager.cgi?action=attach&codes=[All]";
374 urlToKeepOpen = "/bha-api/monitor.cgi?ring=doorbell,motionsensor";
377 ChannelTracking channelTracking = channelTrackingMap.get(urlToKeepOpen);
378 if (channelTracking != null) {
379 if (channelTracking.getChannel() == ctx.channel()) {
380 return; // don't auto close this as it is for the alarms.
383 logger.debug("Closing an idle channel for camera:{}", cameraConfig.getIp());
390 public IpCameraHandler(Thing thing, @Nullable String ipAddress, GroupTracker groupTracker,
391 IpCameraDynamicStateDescriptionProvider stateDescriptionProvider, HttpService httpService) {
393 this.stateDescriptionProvider = stateDescriptionProvider;
394 if (ipAddress != null) {
397 hostIp = Helper.getLocalIpAddress();
399 this.groupTracker = groupTracker;
400 this.httpService = httpService;
403 private IpCameraHandler getHandle() {
407 // false clears the stored user/pass hash, true creates the hash
408 public boolean setBasicAuth(boolean useBasic) {
410 logger.debug("Clearing out the stored BASIC auth now.");
413 } else if (!basicAuth.isEmpty()) {
414 // If the binding sends multiple requests before basicAuth was set, this may trigger falsely.
415 logger.warn("Camera is reporting your username and/or password is wrong.");
418 if (!cameraConfig.getUser().isEmpty() && !cameraConfig.getPassword().isEmpty()) {
419 String authString = cameraConfig.getUser() + ":" + cameraConfig.getPassword();
420 ByteBuf byteBuf = null;
422 byteBuf = Base64.encode(Unpooled.wrappedBuffer(authString.getBytes(CharsetUtil.UTF_8)));
423 basicAuth = byteBuf.getCharSequence(0, byteBuf.capacity(), CharsetUtil.UTF_8).toString();
425 if (byteBuf != null) {
431 cameraConfigError("Camera is asking for Basic Auth when you have not provided a username and/or password.");
436 private String getCorrectUrlFormat(String longUrl) {
437 String temp = longUrl;
440 if (longUrl.isEmpty() || "ffmpeg".equals(longUrl)) {
445 url = new URL(longUrl);
446 int port = url.getPort();
448 if (url.getQuery() == null) {
449 temp = url.getPath();
451 temp = url.getPath() + "?" + url.getQuery();
454 if (url.getQuery() == null) {
455 temp = ":" + url.getPort() + url.getPath();
457 temp = ":" + url.getPort() + url.getPath() + "?" + url.getQuery();
460 } catch (MalformedURLException e) {
461 cameraConfigError("A non valid URL has been given to the binding, check they work in a browser.");
466 public void sendHttpPUT(String httpRequestURL, FullHttpRequest request) {
467 putRequestWithBody = request; // use Global so the authhandler can use it when resent with DIGEST.
468 sendHttpRequest("PUT", httpRequestURL, null);
471 public void sendHttpGET(String httpRequestURL) {
472 sendHttpRequest("GET", httpRequestURL, null);
475 public int getPortFromShortenedUrl(String httpRequestURL) {
476 if (httpRequestURL.startsWith(":")) {
477 int end = httpRequestURL.indexOf("/");
478 return Integer.parseInt(httpRequestURL.substring(1, end));
480 return cameraConfig.getPort();
483 public String getTinyUrl(String httpRequestURL) {
484 if (httpRequestURL.startsWith(":")) {
485 int beginIndex = httpRequestURL.indexOf("/");
486 return httpRequestURL.substring(beginIndex);
488 return httpRequestURL;
491 private void checkCameraConnection() {
492 if (snapshotUri.isEmpty() || snapshotPolling) {
493 // Already polling or camera has RTSP only and no HTTP server
496 Bootstrap localBootstrap = mainBootstrap;
497 if (localBootstrap != null) {
498 ChannelFuture chFuture = localBootstrap
499 .connect(new InetSocketAddress(cameraConfig.getIp(), cameraConfig.getPort()));
500 if (chFuture.awaitUninterruptibly(500)) {
501 chFuture.channel().close();
505 cameraCommunicationError(
506 "Connection Timeout: Check your IP and PORT are correct and the camera can be reached.");
509 // Always use this as sendHttpGET(GET/POST/PUT/DELETE, "/foo/bar",null)//
510 // The authHandler will generate a digest string and re-send using this same function when needed.
511 @SuppressWarnings("null")
512 public void sendHttpRequest(String httpMethod, String httpRequestURLFull, @Nullable String digestString) {
513 int port = getPortFromShortenedUrl(httpRequestURLFull);
514 String httpRequestURL = getTinyUrl(httpRequestURLFull);
516 if (mainBootstrap == null) {
517 mainBootstrap = new Bootstrap();
518 mainBootstrap.group(mainEventLoopGroup);
519 mainBootstrap.channel(NioSocketChannel.class);
520 mainBootstrap.option(ChannelOption.SO_KEEPALIVE, true);
521 mainBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 4500);
522 mainBootstrap.option(ChannelOption.SO_SNDBUF, 1024 * 8);
523 mainBootstrap.option(ChannelOption.SO_RCVBUF, 1024 * 1024);
524 mainBootstrap.option(ChannelOption.TCP_NODELAY, true);
525 mainBootstrap.handler(new ChannelInitializer<SocketChannel>() {
528 public void initChannel(SocketChannel socketChannel) throws Exception {
529 // HIK Alarm stream needs > 9sec idle to stop stream closing
530 socketChannel.pipeline().addLast(new IdleStateHandler(18, 0, 0));
531 socketChannel.pipeline().addLast(new HttpClientCodec());
532 socketChannel.pipeline().addLast(AUTH_HANDLER,
533 new MyNettyAuthHandler(cameraConfig.getUser(), cameraConfig.getPassword(), getHandle()));
534 socketChannel.pipeline().addLast(COMMON_HANDLER, new CommonCameraHandler());
536 switch (thing.getThingTypeUID().getId()) {
538 socketChannel.pipeline().addLast(AMCREST_HANDLER, new AmcrestHandler(getHandle()));
541 socketChannel.pipeline()
542 .addLast(new DahuaHandler(getHandle(), cameraConfig.getNvrChannel()));
545 socketChannel.pipeline().addLast(new DoorBirdHandler(getHandle()));
548 socketChannel.pipeline().addLast(
549 new FoscamHandler(getHandle(), cameraConfig.getUser(), cameraConfig.getPassword()));
551 case HIKVISION_THING:
552 socketChannel.pipeline()
553 .addLast(new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel()));
556 socketChannel.pipeline().addLast(INSTAR_HANDLER, new InstarHandler(getHandle()));
559 socketChannel.pipeline().addLast(new HttpOnlyHandler(getHandle()));
566 FullHttpRequest request;
567 if (!"PUT".equals(httpMethod) || (useDigestAuth && digestString == null)) {
568 request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod(httpMethod), httpRequestURL);
569 request.headers().set("Host", cameraConfig.getIp() + ":" + port);
570 request.headers().set("Connection", HttpHeaderValues.KEEP_ALIVE);
572 request = putRequestWithBody;
575 if (!basicAuth.isEmpty()) {
577 logger.warn("Camera at IP:{} had both Basic and Digest set to be used", cameraConfig.getIp());
580 request.headers().set("Authorization", "Basic " + basicAuth);
585 if (digestString != null) {
586 request.headers().set("Authorization", "Digest " + digestString);
590 mainBootstrap.connect(new InetSocketAddress(cameraConfig.getIp(), port))
591 .addListener(new ChannelFutureListener() {
594 public void operationComplete(@Nullable ChannelFuture future) {
595 if (future == null) {
598 if (future.isDone() && future.isSuccess()) {
599 Channel ch = future.channel();
600 openChannels.add(ch);
604 logger.trace("Sending camera: {}: http://{}:{}{}", httpMethod, cameraConfig.getIp(), port,
607 openChannel(ch, httpRequestURL);
608 CommonCameraHandler commonHandler = (CommonCameraHandler) ch.pipeline().get(COMMON_HANDLER);
609 commonHandler.setURL(httpRequestURLFull);
610 MyNettyAuthHandler authHandler = (MyNettyAuthHandler) ch.pipeline().get(AUTH_HANDLER);
611 authHandler.setURL(httpMethod, httpRequestURL);
613 switch (thing.getThingTypeUID().getId()) {
615 AmcrestHandler amcrestHandler = (AmcrestHandler) ch.pipeline().get(AMCREST_HANDLER);
616 amcrestHandler.setURL(httpRequestURL);
619 InstarHandler instarHandler = (InstarHandler) ch.pipeline().get(INSTAR_HANDLER);
620 instarHandler.setURL(httpRequestURL);
623 ch.writeAndFlush(request);
624 } else { // an error occured
625 cameraCommunicationError(
626 "Connection Timeout: Check your IP and PORT are correct and the camera can be reached.");
632 public void processSnapshot(byte[] incommingSnapshot) {
633 lockCurrentSnapshot.lock();
635 currentSnapshot = incommingSnapshot;
636 if (cameraConfig.getGifPreroll() > 0) {
637 fifoSnapshotBuffer.add(incommingSnapshot);
638 if (fifoSnapshotBuffer.size() > (cameraConfig.getGifPreroll() + gifRecordTime)) {
639 fifoSnapshotBuffer.removeFirst();
643 lockCurrentSnapshot.unlock();
644 currentSnapshotTime = Instant.now();
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;
658 public void startStreamServer() {
659 servlet = new CameraServlet(this, httpService);
660 updateState(CHANNEL_HLS_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
661 + getThing().getUID().getId() + "/ipcamera.m3u8"));
662 updateState(CHANNEL_IMAGE_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
663 + getThing().getUID().getId() + "/ipcamera.jpg"));
664 updateState(CHANNEL_MJPEG_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
665 + getThing().getUID().getId() + "/ipcamera.mjpeg"));
668 public void openCamerasStream() {
669 closeChannel(getTinyUrl(mjpegUri));
670 mainEventLoopGroup.schedule(this::openMjpegStream, 0, TimeUnit.MILLISECONDS);
673 private void openMjpegStream() {
674 sendHttpGET(mjpegUri);
677 private void openChannel(Channel channel, String httpRequestURL) {
678 ChannelTracking tracker = channelTrackingMap.get(httpRequestURL);
679 if (tracker != null && !tracker.getReply().isEmpty()) {// We need to keep the stored reply
680 tracker.setChannel(channel);
683 channelTrackingMap.put(httpRequestURL, new ChannelTracking(channel, httpRequestURL));
686 public void closeChannel(String url) {
687 ChannelTracking channelTracking = channelTrackingMap.get(url);
688 if (channelTracking != null) {
689 if (channelTracking.getChannel().isOpen()) {
690 channelTracking.getChannel().close();
697 * This method should never run under normal use, if there is a bug in a camera or binding it may be possible to
698 * open large amounts of channels. This may help to keep it under control and WARN the user every 8 seconds this is
701 private void cleanChannels() {
702 for (Channel channel : openChannels) {
703 boolean oldChannel = true;
704 for (ChannelTracking channelTracking : channelTrackingMap.values()) {
705 if (!channelTracking.getChannel().isOpen() && channelTracking.getReply().isEmpty()) {
706 channelTrackingMap.remove(channelTracking.getRequestUrl());
708 if (channelTracking.getChannel() == channel) {
709 logger.debug("Open channel to camera is used for URL:{}", channelTracking.getRequestUrl());
719 public void storeHttpReply(String url, String content) {
720 ChannelTracking channelTracking = channelTrackingMap.get(url);
721 if (channelTracking != null) {
722 channelTracking.setReply(content);
726 private void storeSnapshots() {
728 // Need to lock as fifoSnapshotBuffer is not thread safe and new snapshots can be incoming.
729 lockCurrentSnapshot.lock();
731 for (byte[] foo : fifoSnapshotBuffer) {
732 File file = new File(cameraConfig.getFfmpegOutput() + "snapshot" + count + ".jpg");
735 OutputStream fos = new FileOutputStream(file);
738 } catch (FileNotFoundException e) {
739 logger.warn("FileNotFoundException {}", e.getMessage());
740 } catch (IOException e) {
741 logger.warn("IOException {}", e.getMessage());
745 lockCurrentSnapshot.unlock();
749 public void setupFfmpegFormat(FFmpegFormat format) {
750 String inputOptions = cameraConfig.getFfmpegInputOptions();
751 if (cameraConfig.getFfmpegOutput().isEmpty()) {
752 logger.warn("The camera tried to use a FFmpeg feature when the output folder is not set.");
755 if (rtspUri.isEmpty()) {
756 logger.warn("The camera tried to use a FFmpeg feature when no valid input for FFmpeg is provided.");
759 if (cameraConfig.getFfmpegLocation().isEmpty()) {
760 logger.warn("The camera tried to use a FFmpeg feature when the location for FFmpeg is not known.");
763 if (rtspUri.toLowerCase().contains("rtsp")) {
764 if (inputOptions.isEmpty()) {
765 inputOptions = "-rtsp_transport tcp";
769 // Make sure the folder exists, if not create it.
770 new File(cameraConfig.getFfmpegOutput()).mkdirs();
773 if (ffmpegHLS == null) {
774 if (!inputOptions.isEmpty()) {
775 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
776 "-hide_banner -loglevel warning " + inputOptions, rtspUri,
777 cameraConfig.getHlsOutOptions(), cameraConfig.getFfmpegOutput() + "ipcamera.m3u8",
778 cameraConfig.getUser(), cameraConfig.getPassword());
780 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
781 "-hide_banner -loglevel warning", rtspUri, cameraConfig.getHlsOutOptions(),
782 cameraConfig.getFfmpegOutput() + "ipcamera.m3u8", cameraConfig.getUser(),
783 cameraConfig.getPassword());
786 Ffmpeg localHLS = ffmpegHLS;
787 if (localHLS != null) {
788 localHLS.startConverting();
792 if (cameraConfig.getGifPreroll() > 0) {
793 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
794 "-y -r 1 -hide_banner -loglevel warning", cameraConfig.getFfmpegOutput() + "snapshot%d.jpg",
795 "-frames:v " + (cameraConfig.getGifPreroll() + gifRecordTime) + " "
796 + cameraConfig.getGifOutOptions(),
797 cameraConfig.getFfmpegOutput() + gifFilename + ".gif", cameraConfig.getUser(),
798 cameraConfig.getPassword());
800 if (!inputOptions.isEmpty()) {
801 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning " + inputOptions;
803 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning";
805 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
806 cameraConfig.getGifOutOptions(), cameraConfig.getFfmpegOutput() + gifFilename + ".gif",
807 cameraConfig.getUser(), cameraConfig.getPassword());
809 if (cameraConfig.getGifPreroll() > 0) {
812 Ffmpeg localGIF = ffmpegGIF;
813 if (localGIF != null) {
814 localGIF.startConverting();
815 if (gifHistory.isEmpty()) {
816 gifHistory = gifFilename;
817 } else if (!"ipcamera".equals(gifFilename)) {
818 gifHistory = gifFilename + "," + gifHistory;
819 if (gifHistoryLength > 49) {
820 int endIndex = gifHistory.lastIndexOf(",");
821 gifHistory = gifHistory.substring(0, endIndex);
824 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
828 if (!inputOptions.isEmpty()) {
829 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning " + inputOptions;
831 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning";
833 ffmpegRecord = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
834 cameraConfig.getMp4OutOptions(), cameraConfig.getFfmpegOutput() + mp4Filename + ".mp4",
835 cameraConfig.getUser(), cameraConfig.getPassword());
836 Ffmpeg localRecord = ffmpegRecord;
837 if (localRecord != null) {
838 localRecord.startConverting();
839 if (mp4History.isEmpty()) {
840 mp4History = mp4Filename;
841 } else if (!"ipcamera".equals(mp4Filename)) {
842 mp4History = mp4Filename + "," + mp4History;
843 if (mp4HistoryLength > 49) {
844 int endIndex = mp4History.lastIndexOf(",");
845 mp4History = mp4History.substring(0, endIndex);
849 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
852 Ffmpeg localAlarms = ffmpegRtspHelper;
853 if (localAlarms != null) {
854 localAlarms.stopConverting();
855 if (!audioAlarmEnabled && !motionAlarmEnabled) {
859 String input = (cameraConfig.getAlarmInputUrl().isEmpty()) ? rtspUri : cameraConfig.getAlarmInputUrl();
860 String filterOptions = "";
861 if (!audioAlarmEnabled) {
862 filterOptions = "-an";
864 filterOptions = "-af silencedetect=n=-" + audioThreshold + "dB:d=2";
866 if (!motionAlarmEnabled && !ffmpegSnapshotGeneration) {
867 filterOptions = filterOptions.concat(" -vn");
868 } else if (motionAlarmEnabled && !cameraConfig.getMotionOptions().isEmpty()) {
869 String usersMotionOptions = cameraConfig.getMotionOptions();
870 if (usersMotionOptions.startsWith("-")) {
871 // Need to put the users custom options first in the chain before the motion is detected
872 filterOptions += " " + usersMotionOptions + ",select='gte(scene,"
873 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print";
875 filterOptions = filterOptions + " " + usersMotionOptions + " -vf select='gte(scene,"
876 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print";
878 } else if (motionAlarmEnabled) {
879 filterOptions = filterOptions.concat(" -vf select='gte(scene,"
880 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print");
882 ffmpegRtspHelper = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, input,
883 filterOptions, "-f null -", cameraConfig.getUser(), cameraConfig.getPassword());
884 localAlarms = ffmpegRtspHelper;
885 if (localAlarms != null) {
886 localAlarms.startConverting();
890 if (ffmpegMjpeg == null) {
891 if (inputOptions.isEmpty()) {
892 inputOptions = "-hide_banner -loglevel warning";
894 inputOptions += " -hide_banner -loglevel warning";
896 ffmpegMjpeg = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
897 cameraConfig.getMjpegOptions(), "http://127.0.0.1:" + SERVLET_PORT + "/ipcamera/"
898 + getThing().getUID().getId() + "/ipcamera.jpg",
899 cameraConfig.getUser(), cameraConfig.getPassword());
901 Ffmpeg localMjpeg = ffmpegMjpeg;
902 if (localMjpeg != null) {
903 localMjpeg.startConverting();
907 // if mjpeg stream you can use 'ffmpeg -i input -codec:v copy -bsf:v mjpeg2jpeg output.jpg'
908 if (ffmpegSnapshot == null) {
909 if (inputOptions.isEmpty()) {
911 inputOptions = "-threads 1 -skip_frame nokey -hide_banner -loglevel warning";
913 inputOptions += " -threads 1 -skip_frame nokey -hide_banner -loglevel warning";
915 ffmpegSnapshot = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
916 cameraConfig.getSnapshotOptions(), "http://127.0.0.1:" + SERVLET_PORT + "/ipcamera/"
917 + getThing().getUID().getId() + "/snapshot.jpg",
918 cameraConfig.getUser(), cameraConfig.getPassword());
920 Ffmpeg localSnaps = ffmpegSnapshot;
921 if (localSnaps != null) {
922 localSnaps.startConverting();
928 public void noMotionDetected(String thisAlarmsChannel) {
929 setChannelState(thisAlarmsChannel, OnOffType.OFF);
930 firstMotionAlarm = false;
931 motionAlarmUpdateSnapshot = false;
932 motionDetected = false;
933 if (streamingAutoFps) {
934 stopSnapshotPolling();
935 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
936 stopSnapshotPolling();
941 * The {@link changeAlarmState} To only be used to change alarms channels that are not counted as motion. This will
942 * allow logic to be added here in the future. Example more than 1 type of alarm may indicate that someone is
943 * tampering with the camera.
945 public void changeAlarmState(String thisAlarmsChannel, OnOffType state) {
946 updateState(thisAlarmsChannel, state);
949 public void motionDetected(String thisAlarmsChannel) {
950 updateState(CHANNEL_LAST_MOTION_TYPE, new StringType(thisAlarmsChannel));
951 updateState(thisAlarmsChannel, OnOffType.ON);
952 motionDetected = true;
953 if (streamingAutoFps) {
954 startSnapshotPolling();
956 if (cameraConfig.getUpdateImageWhen().contains("2")) {
957 if (!firstMotionAlarm) {
958 if (!snapshotUri.isEmpty()) {
961 firstMotionAlarm = true;// reset back to false when the jpg arrives.
963 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
964 if (!snapshotPolling) {
965 startSnapshotPolling();
967 firstMotionAlarm = true;
968 motionAlarmUpdateSnapshot = true;
972 public void audioDetected() {
973 updateState(CHANNEL_AUDIO_ALARM, OnOffType.ON);
974 if (cameraConfig.getUpdateImageWhen().contains("3")) {
975 if (!firstAudioAlarm) {
976 if (!snapshotUri.isEmpty()) {
979 firstAudioAlarm = true;// reset back to false when the jpg arrives.
981 } else if (cameraConfig.getUpdateImageWhen().contains("5")) {// During audio alarms
982 firstAudioAlarm = true;
983 audioAlarmUpdateSnapshot = true;
987 public void noAudioDetected() {
988 setChannelState(CHANNEL_AUDIO_ALARM, OnOffType.OFF);
989 firstAudioAlarm = false;
990 audioAlarmUpdateSnapshot = false;
993 public void recordMp4(String filename, int seconds) {
994 mp4Filename = filename;
995 mp4RecordTime = seconds;
996 setupFfmpegFormat(FFmpegFormat.RECORD);
997 setChannelState(CHANNEL_RECORDING_MP4, DecimalType.valueOf(new String("" + seconds)));
1000 public void recordGif(String filename, int seconds) {
1001 gifFilename = filename;
1002 gifRecordTime = seconds;
1003 if (cameraConfig.getGifPreroll() > 0) {
1004 snapCount = seconds;
1006 setupFfmpegFormat(FFmpegFormat.GIF);
1008 setChannelState(CHANNEL_RECORDING_GIF, DecimalType.valueOf(new String("" + seconds)));
1011 public String returnValueFromString(String rawString, String searchedString) {
1013 int index = rawString.indexOf(searchedString);
1014 if (index != -1) // -1 means "not found"
1016 result = rawString.substring(index + searchedString.length(), rawString.length());
1017 index = result.indexOf("\r\n"); // find a carriage return to find the end of the value.
1019 return result; // Did not find a carriage return.
1021 return result.substring(0, index);
1024 return ""; // Did not find the String we were searching for
1027 private void sendPTZRequest() {
1028 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.AbsoluteMove);
1032 public void channelLinked(ChannelUID channelUID) {
1033 switch (channelUID.getId()) {
1034 case CHANNEL_MJPEG_URL:
1035 updateState(CHANNEL_MJPEG_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1036 + getThing().getUID().getId() + "/ipcamera.mjpeg"));
1038 case CHANNEL_HLS_URL:
1039 updateState(CHANNEL_HLS_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1040 + getThing().getUID().getId() + "/ipcamera.m3u8"));
1042 case CHANNEL_IMAGE_URL:
1043 updateState(CHANNEL_IMAGE_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1044 + getThing().getUID().getId() + "/ipcamera.jpg"));
1050 public void handleCommand(ChannelUID channelUID, Command command) {
1051 if (command instanceof RefreshType) {
1052 switch (channelUID.getId()) {
1054 if (onvifCamera.supportsPTZ()) {
1055 updateState(CHANNEL_PAN, new PercentType(Math.round(onvifCamera.getAbsolutePan())));
1059 if (onvifCamera.supportsPTZ()) {
1060 updateState(CHANNEL_TILT, new PercentType(Math.round(onvifCamera.getAbsoluteTilt())));
1064 if (onvifCamera.supportsPTZ()) {
1065 updateState(CHANNEL_ZOOM, new PercentType(Math.round(onvifCamera.getAbsoluteZoom())));
1068 case CHANNEL_GOTO_PRESET:
1069 if (onvifCamera.supportsPTZ()) {
1070 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.GetPresets);
1074 } // caution "REFRESH" can still progress to brand Handlers below the else.
1076 switch (channelUID.getId()) {
1077 case CHANNEL_MP4_HISTORY_LENGTH:
1078 if (DecimalType.ZERO.equals(command)) {
1079 mp4HistoryLength = 0;
1081 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
1084 case CHANNEL_GIF_HISTORY_LENGTH:
1085 if (DecimalType.ZERO.equals(command)) {
1086 gifHistoryLength = 0;
1088 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
1091 case CHANNEL_FFMPEG_MOTION_CONTROL:
1092 if (OnOffType.ON.equals(command)) {
1093 motionAlarmEnabled = true;
1094 } else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
1095 motionAlarmEnabled = false;
1096 noMotionDetected(CHANNEL_FFMPEG_MOTION_ALARM);
1097 } else if (command instanceof PercentType) {
1098 motionAlarmEnabled = true;
1099 motionThreshold = ((PercentType) command).toBigDecimal();
1101 setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
1103 case CHANNEL_START_STREAM:
1105 if (OnOffType.ON.equals(command)) {
1106 localHLS = ffmpegHLS;
1107 if (localHLS == null) {
1108 setupFfmpegFormat(FFmpegFormat.HLS);
1109 localHLS = ffmpegHLS;
1111 if (localHLS != null) {
1112 localHLS.setKeepAlive(-1);// Now will run till manually stopped.
1113 localHLS.startConverting();
1116 localHLS = ffmpegHLS;
1117 if (localHLS != null) {
1118 // Still runs but will be able to auto stop when the HLS stream is no longer used.
1119 localHLS.setKeepAlive(1);
1123 case CHANNEL_EXTERNAL_MOTION:
1124 if (OnOffType.ON.equals(command)) {
1125 motionDetected(CHANNEL_EXTERNAL_MOTION);
1127 noMotionDetected(CHANNEL_EXTERNAL_MOTION);
1130 case CHANNEL_GOTO_PRESET:
1131 if (onvifCamera.supportsPTZ()) {
1132 onvifCamera.gotoPreset(Integer.valueOf(command.toString()));
1135 case CHANNEL_POLL_IMAGE:
1136 if (OnOffType.ON.equals(command)) {
1137 if (snapshotUri.isEmpty()) {
1138 ffmpegSnapshotGeneration = true;
1139 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1140 updateImageChannel = false;
1142 updateImageChannel = true;
1143 updateSnapshot();// Allows this to change Image FPS on demand
1146 Ffmpeg localSnaps = ffmpegSnapshot;
1147 if (localSnaps != null) {
1148 localSnaps.stopConverting();
1149 ffmpegSnapshotGeneration = false;
1151 updateImageChannel = false;
1155 if (onvifCamera.supportsPTZ()) {
1156 if (command instanceof IncreaseDecreaseType) {
1157 if (command == IncreaseDecreaseType.INCREASE) {
1158 if (cameraConfig.getPtzContinuous()) {
1159 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveLeft);
1161 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveLeft);
1164 if (cameraConfig.getPtzContinuous()) {
1165 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveRight);
1167 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveRight);
1171 } else if (OnOffType.OFF.equals(command)) {
1172 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1175 onvifCamera.setAbsolutePan(Float.valueOf(command.toString()));
1176 mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1180 if (onvifCamera.supportsPTZ()) {
1181 if (command instanceof IncreaseDecreaseType) {
1182 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1183 if (cameraConfig.getPtzContinuous()) {
1184 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveUp);
1186 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveUp);
1189 if (cameraConfig.getPtzContinuous()) {
1190 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveDown);
1192 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveDown);
1196 } else if (OnOffType.OFF.equals(command)) {
1197 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1200 onvifCamera.setAbsoluteTilt(Float.valueOf(command.toString()));
1201 mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1205 if (onvifCamera.supportsPTZ()) {
1206 if (command instanceof IncreaseDecreaseType) {
1207 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1208 if (cameraConfig.getPtzContinuous()) {
1209 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveIn);
1211 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveIn);
1214 if (cameraConfig.getPtzContinuous()) {
1215 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveOut);
1217 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveOut);
1221 } else if (OnOffType.OFF.equals(command)) {
1222 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1225 onvifCamera.setAbsoluteZoom(Float.valueOf(command.toString()));
1226 mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1231 // commands and refresh now get passed to brand handlers
1232 switch (thing.getThingTypeUID().getId()) {
1234 AmcrestHandler amcrestHandler = new AmcrestHandler(getHandle());
1235 amcrestHandler.handleCommand(channelUID, command);
1236 if (lowPriorityRequests.isEmpty()) {
1237 lowPriorityRequests = amcrestHandler.getLowPriorityRequests();
1241 DahuaHandler dahuaHandler = new DahuaHandler(getHandle(), cameraConfig.getNvrChannel());
1242 dahuaHandler.handleCommand(channelUID, command);
1243 if (lowPriorityRequests.isEmpty()) {
1244 lowPriorityRequests = dahuaHandler.getLowPriorityRequests();
1247 case DOORBIRD_THING:
1248 DoorBirdHandler doorBirdHandler = new DoorBirdHandler(getHandle());
1249 doorBirdHandler.handleCommand(channelUID, command);
1250 if (lowPriorityRequests.isEmpty()) {
1251 lowPriorityRequests = doorBirdHandler.getLowPriorityRequests();
1254 case HIKVISION_THING:
1255 HikvisionHandler hikvisionHandler = new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel());
1256 hikvisionHandler.handleCommand(channelUID, command);
1257 if (lowPriorityRequests.isEmpty()) {
1258 lowPriorityRequests = hikvisionHandler.getLowPriorityRequests();
1262 FoscamHandler foscamHandler = new FoscamHandler(getHandle(), cameraConfig.getUser(),
1263 cameraConfig.getPassword());
1264 foscamHandler.handleCommand(channelUID, command);
1265 if (lowPriorityRequests.isEmpty()) {
1266 lowPriorityRequests = foscamHandler.getLowPriorityRequests();
1270 InstarHandler instarHandler = new InstarHandler(getHandle());
1271 instarHandler.handleCommand(channelUID, command);
1272 if (lowPriorityRequests.isEmpty()) {
1273 lowPriorityRequests = instarHandler.getLowPriorityRequests();
1277 HttpOnlyHandler defaultHandler = new HttpOnlyHandler(getHandle());
1278 defaultHandler.handleCommand(channelUID, command);
1279 if (lowPriorityRequests.isEmpty()) {
1280 lowPriorityRequests = defaultHandler.getLowPriorityRequests();
1286 public void setChannelState(String channelToUpdate, State valueOf) {
1287 updateState(channelToUpdate, valueOf);
1290 private void bringCameraOnline() {
1292 updateStatus(ThingStatus.ONLINE);
1293 groupTracker.listOfOnlineCameraHandlers.add(this);
1294 groupTracker.listOfOnlineCameraUID.add(getThing().getUID().getId());
1295 Future<?> localFuture = cameraConnectionJob;
1296 if (localFuture != null) {
1297 localFuture.cancel(false);
1298 cameraConnectionJob = null;
1300 if (!snapshotUri.isEmpty()) {
1301 if (cameraConfig.getGifPreroll() > 0 || cameraConfig.getUpdateImageWhen().contains("1")) {
1302 snapshotPolling = true;
1303 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 1000,
1304 cameraConfig.getPollTime(), TimeUnit.MILLISECONDS);
1308 pollCameraJob = threadPool.scheduleWithFixedDelay(this::pollCameraRunnable, 1000, 8000, TimeUnit.MILLISECONDS);
1310 if (!rtspUri.isEmpty()) {
1311 updateState(CHANNEL_RTSP_URL, new StringType(rtspUri));
1313 if (updateImageChannel) {
1314 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1316 updateState(CHANNEL_POLL_IMAGE, OnOffType.OFF);
1318 if (!groupTracker.listOfGroupHandlers.isEmpty()) {
1319 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1320 handle.cameraOnline(getThing().getUID().getId());
1325 void snapshotIsFfmpeg() {
1326 snapshotUri = "";// ffmpeg is a valid option. Simplify further checks.
1328 "Binding has no snapshot url. Will use your CPU and FFmpeg to create snapshots from the cameras RTSP.");
1329 bringCameraOnline();
1330 if (!rtspUri.isEmpty()) {
1331 updateImageChannel = false;
1332 ffmpegSnapshotGeneration = true;
1333 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1334 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1336 cameraConfigError("Binding can not find a RTSP url for this camera, please provide a FFmpeg Input URL.");
1340 void pollingCameraConnection() {
1341 if (thing.getThingTypeUID().getId().equals(GENERIC_THING)) {
1342 if (rtspUri.isEmpty()) {
1343 logger.warn("Binding has not been supplied with a FFmpeg Input URL, so some features will not work.");
1345 if (snapshotUri.isEmpty() || "ffmpeg".equals(snapshotUri)) {
1352 if (!onvifCamera.isConnected()) {
1353 logger.debug("About to connect to the IP Camera using the ONVIF PORT at IP:{}:{}", cameraConfig.getIp(),
1354 cameraConfig.getOnvifPort());
1355 onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1357 if ("ffmpeg".equals(snapshotUri)) {
1359 } else if (!snapshotUri.isEmpty()) {
1361 } else if (!rtspUri.isEmpty()) {
1364 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
1365 "Camera failed to report a valid Snaphot and/or RTSP URL. See readme on how to use the SNAPSHOT_URL_OVERRIDE feature.");
1369 public void cameraConfigError(String reason) {
1370 // wont try to reconnect again due to a config error being the cause.
1371 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, reason);
1375 public void cameraCommunicationError(String reason) {
1376 // will try to reconnect again as camera may be rebooting.
1377 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
1378 if (isOnline) {// if already offline dont try reconnecting in 6 seconds, we want 30sec wait.
1379 resetAndRetryConnecting();
1383 boolean streamIsStopped(String url) {
1384 ChannelTracking channelTracking = channelTrackingMap.get(url);
1385 if (channelTracking != null) {
1386 if (channelTracking.getChannel().isActive()) {
1387 return false; // stream is running.
1390 return true; // Stream stopped or never started.
1393 void snapshotRunnable() {
1394 // Snapshot should be first to keep consistent time between shots
1396 if (snapCount > 0) {
1397 if (--snapCount == 0) {
1398 setupFfmpegFormat(FFmpegFormat.GIF);
1403 private void takeSnapshot() {
1404 sendHttpGET(snapshotUri);
1407 private void updateSnapshot() {
1408 lastSnapshotRequest = Instant.now();
1409 mainEventLoopGroup.schedule(this::takeSnapshot, 0, TimeUnit.MILLISECONDS);
1412 public byte[] getSnapshot() {
1414 // Single gray pixel JPG to keep streams open when the camera goes offline so they dont stop.
1415 return new byte[] { (byte) 0xff, (byte) 0xd8, (byte) 0xff, (byte) 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46,
1416 0x00, 0x01, 0x01, 0x01, 0x00, 0x48, 0x00, 0x48, 0x00, 0x00, (byte) 0xff, (byte) 0xdb, 0x00, 0x43,
1417 0x00, 0x03, 0x02, 0x02, 0x02, 0x02, 0x02, 0x03, 0x02, 0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x04,
1418 0x06, 0x04, 0x04, 0x04, 0x04, 0x04, 0x08, 0x06, 0x06, 0x05, 0x06, 0x09, 0x08, 0x0a, 0x0a, 0x09,
1419 0x08, 0x09, 0x09, 0x0a, 0x0c, 0x0f, 0x0c, 0x0a, 0x0b, 0x0e, 0x0b, 0x09, 0x09, 0x0d, 0x11, 0x0d,
1420 0x0e, 0x0f, 0x10, 0x10, 0x11, 0x10, 0x0a, 0x0c, 0x12, 0x13, 0x12, 0x10, 0x13, 0x0f, 0x10, 0x10,
1421 0x10, (byte) 0xff, (byte) 0xc9, 0x00, 0x0b, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00,
1422 (byte) 0xff, (byte) 0xcc, 0x00, 0x06, 0x00, 0x10, 0x10, 0x05, (byte) 0xff, (byte) 0xda, 0x00, 0x08,
1423 0x01, 0x01, 0x00, 0x00, 0x3f, 0x00, (byte) 0xd2, (byte) 0xcf, 0x20, (byte) 0xff, (byte) 0xd9 };
1425 // Most cameras will return a 503 busy error if snapshot is faster than 1 second
1426 long lastUpdatedMs = Duration.between(lastSnapshotRequest, Instant.now()).toMillis();
1427 if (!snapshotPolling && !ffmpegSnapshotGeneration && lastUpdatedMs >= cameraConfig.getPollTime()) {
1430 lockCurrentSnapshot.lock();
1432 return currentSnapshot;
1434 lockCurrentSnapshot.unlock();
1438 public void stopSnapshotPolling() {
1439 Future<?> localFuture;
1440 if (!streamingSnapshotMjpeg && cameraConfig.getGifPreroll() == 0
1441 && !cameraConfig.getUpdateImageWhen().contains("1")) {
1442 snapshotPolling = false;
1443 localFuture = snapshotJob;
1444 if (localFuture != null) {
1445 localFuture.cancel(true);
1447 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // only during Motion Alarms
1448 snapshotPolling = false;
1449 localFuture = snapshotJob;
1450 if (localFuture != null) {
1451 localFuture.cancel(true);
1456 public void startSnapshotPolling() {
1457 if (snapshotPolling || ffmpegSnapshotGeneration) {
1458 return; // Already polling or creating with FFmpeg from RTSP
1460 if (streamingSnapshotMjpeg || streamingAutoFps || cameraConfig.getUpdateImageWhen().contains("4")) {
1461 snapshotPolling = true;
1462 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 0, cameraConfig.getPollTime(),
1463 TimeUnit.MILLISECONDS);
1468 * {@link pollCameraRunnable} Polls every 8 seconds, to check camera is still ONLINE and keep alarm
1469 * streams open and more.
1472 void pollCameraRunnable() {
1473 // NOTE: Use lowPriorityRequests if get request is not needed every poll.
1474 if (!lowPriorityRequests.isEmpty()) {
1475 if (lowPriorityCounter >= lowPriorityRequests.size()) {
1476 lowPriorityCounter = 0;
1478 sendHttpGET(lowPriorityRequests.get(lowPriorityCounter++));
1480 // what needs to be done every poll//
1481 switch (thing.getThingTypeUID().getId()) {
1483 if (!snapshotUri.isEmpty() && !snapshotPolling) {
1484 checkCameraConnection();
1486 // RTSP stream has stopped and we need it for snapshots
1487 if (ffmpegSnapshotGeneration) {
1488 Ffmpeg localSnapshot = ffmpegSnapshot;
1489 if (localSnapshot != null && !localSnapshot.getIsAlive()) {
1490 localSnapshot.startConverting();
1495 if (!snapshotPolling) {
1496 checkCameraConnection();
1498 if (!onvifCamera.isConnected()) {
1499 onvifCamera.connect(true);
1503 if (!snapshotPolling) {
1504 checkCameraConnection();
1506 noMotionDetected(CHANNEL_MOTION_ALARM);
1507 noMotionDetected(CHANNEL_PIR_ALARM);
1510 case HIKVISION_THING:
1511 if (streamIsStopped("/ISAPI/Event/notification/alertStream")) {
1512 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1513 cameraConfig.getIp());
1514 sendHttpGET("/ISAPI/Event/notification/alertStream");
1518 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=VideoMotion");
1519 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=AudioMutation");
1522 if (!snapshotPolling) {
1523 checkCameraConnection();
1525 // Check for alarms, channel for NVRs appears not to work at filtering.
1526 if (streamIsStopped("/cgi-bin/eventManager.cgi?action=attach&codes=[All]")) {
1527 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1528 cameraConfig.getIp());
1529 sendHttpGET("/cgi-bin/eventManager.cgi?action=attach&codes=[All]");
1532 case DOORBIRD_THING:
1533 if (!snapshotPolling) {
1534 checkCameraConnection();
1536 // Check for alarms, channel for NVRs appears not to work at filtering.
1537 if (streamIsStopped("/bha-api/monitor.cgi?ring=doorbell,motionsensor")) {
1538 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1539 cameraConfig.getIp());
1540 sendHttpGET("/bha-api/monitor.cgi?ring=doorbell,motionsensor");
1544 sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=getDevState&usr=" + cameraConfig.getUser() + "&pwd="
1545 + cameraConfig.getPassword());
1548 Ffmpeg localHLS = ffmpegHLS;
1549 if (localHLS != null) {
1550 localHLS.checkKeepAlive();
1552 if (openChannels.size() > 10) {
1553 logger.debug("There are {} open Channels being tracked.", openChannels.size());
1559 public void initialize() {
1560 cameraConfig = getConfigAs(CameraConfig.class);
1561 threadPool = Executors.newScheduledThreadPool(2);
1562 mainEventLoopGroup = new NioEventLoopGroup(3);
1563 snapshotUri = getCorrectUrlFormat(cameraConfig.getSnapshotUrl());
1564 mjpegUri = getCorrectUrlFormat(cameraConfig.getMjpegUrl());
1565 rtspUri = cameraConfig.getFfmpegInput();
1566 if (cameraConfig.getFfmpegOutput().isEmpty()) {
1568 .setFfmpegOutput(OpenHAB.getUserDataFolder() + "/ipcamera/" + this.thing.getUID().getId() + "/");
1570 // Known cameras will connect quicker if we skip ONVIF questions.
1571 switch (thing.getThingTypeUID().getId()) {
1574 if (mjpegUri.isEmpty()) {
1575 mjpegUri = "/cgi-bin/mjpg/video.cgi?channel=" + cameraConfig.getNvrChannel() + "&subtype=1";
1577 if (snapshotUri.isEmpty()) {
1578 snapshotUri = "/cgi-bin/snapshot.cgi?channel=" + cameraConfig.getNvrChannel();
1581 case DOORBIRD_THING:
1582 if (mjpegUri.isEmpty()) {
1583 mjpegUri = "/bha-api/video.cgi";
1585 if (snapshotUri.isEmpty()) {
1586 snapshotUri = "/bha-api/image.cgi";
1590 // Foscam needs any special char like spaces (%20) to be encoded for URLs.
1591 cameraConfig.setUser(Helper.encodeSpecialChars(cameraConfig.getUser()));
1592 cameraConfig.setPassword(Helper.encodeSpecialChars(cameraConfig.getPassword()));
1593 if (mjpegUri.isEmpty()) {
1594 mjpegUri = "/cgi-bin/CGIStream.cgi?cmd=GetMJStream&usr=" + cameraConfig.getUser() + "&pwd="
1595 + cameraConfig.getPassword();
1597 if (snapshotUri.isEmpty()) {
1598 snapshotUri = "/cgi-bin/CGIProxy.fcgi?usr=" + cameraConfig.getUser() + "&pwd="
1599 + cameraConfig.getPassword() + "&cmd=snapPicture2";
1602 case HIKVISION_THING:// The 02 gives you the first sub stream which needs to be set to MJPEG
1603 if (mjpegUri.isEmpty()) {
1604 mjpegUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "02" + "/httppreview";
1606 if (snapshotUri.isEmpty()) {
1607 snapshotUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "01/picture";
1611 if (snapshotUri.isEmpty()) {
1612 snapshotUri = "/tmpfs/snap.jpg";
1614 if (mjpegUri.isEmpty()) {
1615 mjpegUri = "/mjpegstream.cgi?-chn=12";
1618 "/param.cgi?cmd=setmdalarm&-aname=server2&-switch=on&-interval=1&cmd=setalarmserverattr&-as_index=3&-as_server="
1619 + hostIp + "&-as_port=" + SERVLET_PORT + "&-as_path=/ipcamera/"
1620 + getThing().getUID().getId()
1621 + "/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");
1624 // for poll times 9 seconds and above don't display a warning about the Image channel.
1625 if (9000 > cameraConfig.getPollTime() && cameraConfig.getUpdateImageWhen().contains("1")) {
1627 "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.");
1629 // ONVIF and Instar event handling need the server started before connecting.
1630 startStreamServer();
1634 private void tryConnecting() {
1635 if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)) {
1636 onvifCamera = new OnvifConnection(this, cameraConfig.getIp() + ":" + cameraConfig.getOnvifPort(),
1637 cameraConfig.getUser(), cameraConfig.getPassword());
1638 onvifCamera.setSelectedMediaProfile(cameraConfig.getOnvifMediaProfile());
1639 // Only use ONVIF events if it is not an API camera.
1640 onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1642 cameraConnectionJob = threadPool.scheduleWithFixedDelay(this::pollingCameraConnection, 4, 30, TimeUnit.SECONDS);
1645 // What the camera needs to re-connect if the initialize() is not called.
1646 private void resetAndRetryConnecting() {
1651 private void offline() {
1653 snapshotPolling = false;
1654 Future<?> localFuture = pollCameraJob;
1655 if (localFuture != null) {
1656 localFuture.cancel(true);
1659 localFuture = snapshotJob;
1660 if (localFuture != null) {
1661 localFuture.cancel(true);
1664 localFuture = cameraConnectionJob;
1665 if (localFuture != null) {
1666 localFuture.cancel(true);
1669 Ffmpeg localFfmpeg = ffmpegHLS;
1670 if (localFfmpeg != null) {
1671 localFfmpeg.stopConverting();
1674 localFfmpeg = ffmpegRecord;
1675 if (localFfmpeg != null) {
1676 localFfmpeg.stopConverting();
1677 ffmpegRecord = null;
1679 localFfmpeg = ffmpegGIF;
1680 if (localFfmpeg != null) {
1681 localFfmpeg.stopConverting();
1684 localFfmpeg = ffmpegRtspHelper;
1685 if (localFfmpeg != null) {
1686 localFfmpeg.stopConverting();
1687 ffmpegRtspHelper = null;
1689 localFfmpeg = ffmpegMjpeg;
1690 if (localFfmpeg != null) {
1691 localFfmpeg.stopConverting();
1694 localFfmpeg = ffmpegSnapshot;
1695 if (localFfmpeg != null) {
1696 localFfmpeg.stopConverting();
1697 ffmpegSnapshot = null;
1699 onvifCamera.disconnect();
1700 openChannels.close();
1704 public void dispose() {
1706 CameraServlet localServlet = servlet;
1707 if (localServlet != null) {
1708 localServlet.dispose();
1709 localServlet = null;
1711 threadPool.shutdown();
1712 // inform all group handlers that this camera has gone offline
1713 groupTracker.listOfOnlineCameraHandlers.remove(this);
1714 groupTracker.listOfOnlineCameraUID.remove(getThing().getUID().getId());
1715 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1716 handle.cameraOffline(this);
1718 basicAuth = ""; // clear out stored Password hash
1719 useDigestAuth = false;
1720 mainEventLoopGroup.shutdownGracefully();
1721 mainBootstrap = null;
1722 channelTrackingMap.clear();
1725 public String getWhiteList() {
1726 return cameraConfig.getIpWhitelist();
1730 public Collection<Class<? extends ThingHandlerService>> getServices() {
1731 return Collections.singleton(IpCameraActions.class);