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];
256 // 401 errors already handled in pipeline by MyNettyAuthHandler.java
260 if (msg instanceof HttpContent) {
261 HttpContent content = (HttpContent) msg;
262 if (mjpegUri.equals(requestUrl) && !(content instanceof LastHttpContent)) {
263 // multiple MJPEG stream packets come back as this.
264 byte[] chunkedFrame = new byte[content.content().readableBytes()];
265 content.content().getBytes(content.content().readerIndex(), chunkedFrame);
266 CameraServlet localServlet = servlet;
267 if (localServlet != null) {
268 localServlet.openStreams.queueFrame(chunkedFrame);
271 // Found some cameras use Content-Type: image/jpg instead of image/jpeg
272 if (contentType.contains("image/jp")) {
273 for (int i = 0; i < content.content().capacity(); i++) {
274 incomingJpeg[bytesAlreadyRecieved++] = content.content().getByte(i);
276 if (content instanceof LastHttpContent) {
277 processSnapshot(incomingJpeg);
280 } else { // incomingMessage that is not an IMAGE
281 if (incomingMessage.isEmpty()) {
282 incomingMessage = content.content().toString(CharsetUtil.UTF_8);
284 incomingMessage += content.content().toString(CharsetUtil.UTF_8);
286 bytesAlreadyRecieved = incomingMessage.length();
287 if (content instanceof LastHttpContent) {
288 // If it is not an image send it on to the next handler//
289 if (bytesAlreadyRecieved != 0) {
290 reply = incomingMessage;
291 super.channelRead(ctx, reply);
294 // Alarm Streams never have a LastHttpContent as they always stay open//
295 else if (contentType.contains("multipart")) {
296 int beginIndex, endIndex;
297 if (bytesToRecieve == 0) {
298 beginIndex = incomingMessage.indexOf("Content-Length:");
299 if (beginIndex != -1) {
300 endIndex = incomingMessage.indexOf("\r\n", beginIndex);
301 if (endIndex != -1) {
302 bytesToRecieve = Integer.parseInt(
303 incomingMessage.substring(beginIndex + 15, endIndex).strip());
307 // --boundary and headers are not included in the Content-Length value
308 if (bytesAlreadyRecieved > bytesToRecieve) {
309 // Check if message has a second --boundary
310 endIndex = incomingMessage.indexOf("--" + boundary, bytesToRecieve);
311 if (endIndex == -1) {
312 reply = incomingMessage;
313 incomingMessage = "";
315 bytesAlreadyRecieved = 0;
317 reply = incomingMessage.substring(0, endIndex);
318 incomingMessage = incomingMessage.substring(endIndex, incomingMessage.length());
319 bytesToRecieve = 0;// Triggers search next time for Content-Length:
320 bytesAlreadyRecieved = incomingMessage.length() - endIndex;
322 super.channelRead(ctx, reply);
325 // Foscam needs this as will other cameras with chunks//
326 if (isChunked && bytesAlreadyRecieved != 0) {
327 logger.debug("Reply is chunked.");
328 reply = incomingMessage;
329 super.channelRead(ctx, reply);
333 } else { // msg is not HttpContent
334 // Foscam cameras need this
335 if (!contentType.contains("image/jp") && bytesAlreadyRecieved != 0) {
336 reply = incomingMessage;
337 logger.debug("Packet back from camera is {}", incomingMessage);
338 super.channelRead(ctx, reply);
342 ReferenceCountUtil.release(msg);
347 public void exceptionCaught(@Nullable ChannelHandlerContext ctx, @Nullable Throwable cause) {
348 if (cause == null || ctx == null) {
351 if (cause instanceof ArrayIndexOutOfBoundsException) {
352 logger.debug("Camera sent {} bytes when the content-length header was {}.", bytesAlreadyRecieved,
355 logger.warn("!!!! Camera possibly closed the channel on the binding, cause reported is: {}",
362 @SuppressWarnings("PMD.CompareObjectsWithEquals")
363 public void userEventTriggered(@Nullable ChannelHandlerContext ctx, @Nullable Object evt) throws Exception {
367 if (evt instanceof IdleStateEvent) {
368 IdleStateEvent e = (IdleStateEvent) evt;
369 // If camera does not use the channel for X amount of time it will close.
370 if (e.state() == IdleState.READER_IDLE) {
371 String urlToKeepOpen = "";
372 switch (thing.getThingTypeUID().getId()) {
374 urlToKeepOpen = "/cgi-bin/eventManager.cgi?action=attach&codes=[All]";
377 urlToKeepOpen = "/bha-api/monitor.cgi?ring=doorbell,motionsensor";
380 ChannelTracking channelTracking = channelTrackingMap.get(urlToKeepOpen);
381 if (channelTracking != null) {
382 if (channelTracking.getChannel() == ctx.channel()) {
383 return; // don't auto close this as it is for the alarms.
386 logger.debug("Closing an idle channel for camera:{}", cameraConfig.getIp());
393 public IpCameraHandler(Thing thing, @Nullable String ipAddress, GroupTracker groupTracker,
394 IpCameraDynamicStateDescriptionProvider stateDescriptionProvider, HttpService httpService) {
396 this.stateDescriptionProvider = stateDescriptionProvider;
397 if (ipAddress != null) {
400 hostIp = Helper.getLocalIpAddress();
402 this.groupTracker = groupTracker;
403 this.httpService = httpService;
406 private IpCameraHandler getHandle() {
410 // false clears the stored user/pass hash, true creates the hash
411 public boolean setBasicAuth(boolean useBasic) {
413 logger.debug("Clearing out the stored BASIC auth now.");
416 } else if (!basicAuth.isEmpty()) {
417 // If the binding sends multiple requests before basicAuth was set, this may trigger falsely.
418 logger.warn("Camera is reporting your username and/or password is wrong.");
421 if (!cameraConfig.getUser().isEmpty() && !cameraConfig.getPassword().isEmpty()) {
422 String authString = cameraConfig.getUser() + ":" + cameraConfig.getPassword();
423 ByteBuf byteBuf = null;
425 byteBuf = Base64.encode(Unpooled.wrappedBuffer(authString.getBytes(CharsetUtil.UTF_8)));
426 basicAuth = byteBuf.getCharSequence(0, byteBuf.capacity(), CharsetUtil.UTF_8).toString();
428 if (byteBuf != null) {
434 cameraConfigError("Camera is asking for Basic Auth when you have not provided a username and/or password.");
439 private String getCorrectUrlFormat(String longUrl) {
440 String temp = longUrl;
443 if (longUrl.isEmpty() || "ffmpeg".equals(longUrl)) {
448 url = new URL(longUrl);
449 int port = url.getPort();
451 if (url.getQuery() == null) {
452 temp = url.getPath();
454 temp = url.getPath() + "?" + url.getQuery();
457 if (url.getQuery() == null) {
458 temp = ":" + url.getPort() + url.getPath();
460 temp = ":" + url.getPort() + url.getPath() + "?" + url.getQuery();
463 } catch (MalformedURLException e) {
464 cameraConfigError("A non valid URL has been given to the binding, check they work in a browser.");
469 public void sendHttpPUT(String httpRequestURL, FullHttpRequest request) {
470 putRequestWithBody = request; // use Global so the authhandler can use it when resent with DIGEST.
471 sendHttpRequest("PUT", httpRequestURL, null);
474 public void sendHttpGET(String httpRequestURL) {
475 sendHttpRequest("GET", httpRequestURL, null);
478 public int getPortFromShortenedUrl(String httpRequestURL) {
479 if (httpRequestURL.startsWith(":")) {
480 int end = httpRequestURL.indexOf("/");
481 return Integer.parseInt(httpRequestURL.substring(1, end));
483 return cameraConfig.getPort();
486 public String getTinyUrl(String httpRequestURL) {
487 if (httpRequestURL.startsWith(":")) {
488 int beginIndex = httpRequestURL.indexOf("/");
489 return httpRequestURL.substring(beginIndex);
491 return httpRequestURL;
494 private void checkCameraConnection() {
495 if (snapshotUri.isEmpty() || snapshotPolling) {
496 // Already polling or camera has RTSP only and no HTTP server
499 Bootstrap localBootstrap = mainBootstrap;
500 if (localBootstrap != null) {
501 ChannelFuture chFuture = localBootstrap
502 .connect(new InetSocketAddress(cameraConfig.getIp(), cameraConfig.getPort()));
503 if (chFuture.awaitUninterruptibly(500)) {
504 chFuture.channel().close();
508 cameraCommunicationError(
509 "Connection Timeout: Check your IP and PORT are correct and the camera can be reached.");
512 // Always use this as sendHttpGET(GET/POST/PUT/DELETE, "/foo/bar",null)//
513 // The authHandler will generate a digest string and re-send using this same function when needed.
514 @SuppressWarnings("null")
515 public void sendHttpRequest(String httpMethod, String httpRequestURLFull, @Nullable String digestString) {
516 int port = getPortFromShortenedUrl(httpRequestURLFull);
517 String httpRequestURL = getTinyUrl(httpRequestURLFull);
519 if (mainBootstrap == null) {
520 mainBootstrap = new Bootstrap();
521 mainBootstrap.group(mainEventLoopGroup);
522 mainBootstrap.channel(NioSocketChannel.class);
523 mainBootstrap.option(ChannelOption.SO_KEEPALIVE, true);
524 mainBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 4500);
525 mainBootstrap.option(ChannelOption.SO_SNDBUF, 1024 * 8);
526 mainBootstrap.option(ChannelOption.SO_RCVBUF, 1024 * 1024);
527 mainBootstrap.option(ChannelOption.TCP_NODELAY, true);
528 mainBootstrap.handler(new ChannelInitializer<SocketChannel>() {
531 public void initChannel(SocketChannel socketChannel) throws Exception {
532 // HIK Alarm stream needs > 9sec idle to stop stream closing
533 socketChannel.pipeline().addLast(new IdleStateHandler(18, 0, 0));
534 socketChannel.pipeline().addLast(new HttpClientCodec());
535 socketChannel.pipeline().addLast(AUTH_HANDLER,
536 new MyNettyAuthHandler(cameraConfig.getUser(), cameraConfig.getPassword(), getHandle()));
537 socketChannel.pipeline().addLast(COMMON_HANDLER, new CommonCameraHandler());
539 switch (thing.getThingTypeUID().getId()) {
541 socketChannel.pipeline().addLast(AMCREST_HANDLER, new AmcrestHandler(getHandle()));
544 socketChannel.pipeline()
545 .addLast(new DahuaHandler(getHandle(), cameraConfig.getNvrChannel()));
548 socketChannel.pipeline().addLast(new DoorBirdHandler(getHandle()));
551 socketChannel.pipeline().addLast(
552 new FoscamHandler(getHandle(), cameraConfig.getUser(), cameraConfig.getPassword()));
554 case HIKVISION_THING:
555 socketChannel.pipeline()
556 .addLast(new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel()));
559 socketChannel.pipeline().addLast(INSTAR_HANDLER, new InstarHandler(getHandle()));
562 socketChannel.pipeline().addLast(new HttpOnlyHandler(getHandle()));
569 FullHttpRequest request;
570 if (!"PUT".equals(httpMethod) || (useDigestAuth && digestString == null)) {
571 request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod(httpMethod), httpRequestURL);
572 request.headers().set("Host", cameraConfig.getIp() + ":" + port);
573 request.headers().set("Connection", HttpHeaderValues.KEEP_ALIVE);
575 request = putRequestWithBody;
578 if (!basicAuth.isEmpty()) {
580 logger.warn("Camera at IP:{} had both Basic and Digest set to be used", cameraConfig.getIp());
583 request.headers().set("Authorization", "Basic " + basicAuth);
588 if (digestString != null) {
589 request.headers().set("Authorization", "Digest " + digestString);
593 mainBootstrap.connect(new InetSocketAddress(cameraConfig.getIp(), port))
594 .addListener(new ChannelFutureListener() {
597 public void operationComplete(@Nullable ChannelFuture future) {
598 if (future == null) {
601 if (future.isDone() && future.isSuccess()) {
602 Channel ch = future.channel();
603 openChannels.add(ch);
607 logger.trace("Sending camera: {}: http://{}:{}{}", httpMethod, cameraConfig.getIp(), port,
610 openChannel(ch, httpRequestURL);
611 CommonCameraHandler commonHandler = (CommonCameraHandler) ch.pipeline().get(COMMON_HANDLER);
612 commonHandler.setURL(httpRequestURLFull);
613 MyNettyAuthHandler authHandler = (MyNettyAuthHandler) ch.pipeline().get(AUTH_HANDLER);
614 authHandler.setURL(httpMethod, httpRequestURL);
616 switch (thing.getThingTypeUID().getId()) {
618 AmcrestHandler amcrestHandler = (AmcrestHandler) ch.pipeline().get(AMCREST_HANDLER);
619 amcrestHandler.setURL(httpRequestURL);
622 InstarHandler instarHandler = (InstarHandler) ch.pipeline().get(INSTAR_HANDLER);
623 instarHandler.setURL(httpRequestURL);
626 ch.writeAndFlush(request);
627 } else { // an error occured
628 cameraCommunicationError(
629 "Connection Timeout: Check your IP and PORT are correct and the camera can be reached.");
635 public void processSnapshot(byte[] incommingSnapshot) {
636 lockCurrentSnapshot.lock();
638 currentSnapshot = incommingSnapshot;
639 if (cameraConfig.getGifPreroll() > 0) {
640 fifoSnapshotBuffer.add(incommingSnapshot);
641 if (fifoSnapshotBuffer.size() > (cameraConfig.getGifPreroll() + gifRecordTime)) {
642 fifoSnapshotBuffer.removeFirst();
646 lockCurrentSnapshot.unlock();
647 currentSnapshotTime = Instant.now();
650 if (updateImageChannel) {
651 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
652 } else if (firstMotionAlarm || motionAlarmUpdateSnapshot) {
653 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
654 firstMotionAlarm = motionAlarmUpdateSnapshot = false;
655 } else if (firstAudioAlarm || audioAlarmUpdateSnapshot) {
656 updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
657 firstAudioAlarm = audioAlarmUpdateSnapshot = false;
661 public void startStreamServer() {
662 servlet = new CameraServlet(this, httpService);
663 updateState(CHANNEL_HLS_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
664 + getThing().getUID().getId() + "/ipcamera.m3u8"));
665 updateState(CHANNEL_IMAGE_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
666 + getThing().getUID().getId() + "/ipcamera.jpg"));
667 updateState(CHANNEL_MJPEG_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
668 + getThing().getUID().getId() + "/ipcamera.mjpeg"));
671 public void openCamerasStream() {
672 closeChannel(getTinyUrl(mjpegUri));
673 mainEventLoopGroup.schedule(this::openMjpegStream, 0, TimeUnit.MILLISECONDS);
676 private void openMjpegStream() {
677 sendHttpGET(mjpegUri);
680 private void openChannel(Channel channel, String httpRequestURL) {
681 ChannelTracking tracker = channelTrackingMap.get(httpRequestURL);
682 if (tracker != null && !tracker.getReply().isEmpty()) {// We need to keep the stored reply
683 tracker.setChannel(channel);
686 channelTrackingMap.put(httpRequestURL, new ChannelTracking(channel, httpRequestURL));
689 public void closeChannel(String url) {
690 ChannelTracking channelTracking = channelTrackingMap.get(url);
691 if (channelTracking != null) {
692 if (channelTracking.getChannel().isOpen()) {
693 channelTracking.getChannel().close();
700 * This method should never run under normal use, if there is a bug in a camera or binding it may be possible to
701 * open large amounts of channels. This may help to keep it under control and WARN the user every 8 seconds this is
704 @SuppressWarnings("PMD.CompareObjectsWithEquals")
705 private void cleanChannels() {
706 for (Channel channel : openChannels) {
707 boolean oldChannel = true;
708 for (ChannelTracking channelTracking : channelTrackingMap.values()) {
709 if (!channelTracking.getChannel().isOpen() && channelTracking.getReply().isEmpty()) {
710 channelTrackingMap.remove(channelTracking.getRequestUrl());
712 if (channelTracking.getChannel() == channel) {
713 logger.debug("Open channel to camera is used for URL:{}", channelTracking.getRequestUrl());
723 public void storeHttpReply(String url, String content) {
724 ChannelTracking channelTracking = channelTrackingMap.get(url);
725 if (channelTracking != null) {
726 channelTracking.setReply(content);
730 private void storeSnapshots() {
732 // Need to lock as fifoSnapshotBuffer is not thread safe and new snapshots can be incoming.
733 lockCurrentSnapshot.lock();
735 for (byte[] foo : fifoSnapshotBuffer) {
736 File file = new File(cameraConfig.getFfmpegOutput() + "snapshot" + count + ".jpg");
739 OutputStream fos = new FileOutputStream(file);
742 } catch (FileNotFoundException e) {
743 logger.warn("FileNotFoundException {}", e.getMessage());
744 } catch (IOException e) {
745 logger.warn("IOException {}", e.getMessage());
749 lockCurrentSnapshot.unlock();
753 public void setupFfmpegFormat(FFmpegFormat format) {
754 String inputOptions = cameraConfig.getFfmpegInputOptions();
755 if (cameraConfig.getFfmpegOutput().isEmpty()) {
756 logger.warn("The camera tried to use a FFmpeg feature when the output folder is not set.");
759 if (rtspUri.isEmpty()) {
760 logger.warn("The camera tried to use a FFmpeg feature when no valid input for FFmpeg is provided.");
763 if (cameraConfig.getFfmpegLocation().isEmpty()) {
764 logger.warn("The camera tried to use a FFmpeg feature when the location for FFmpeg is not known.");
767 if (rtspUri.toLowerCase().contains("rtsp")) {
768 if (inputOptions.isEmpty()) {
769 inputOptions = "-rtsp_transport tcp";
773 // Make sure the folder exists, if not create it.
774 new File(cameraConfig.getFfmpegOutput()).mkdirs();
777 if (ffmpegHLS == null) {
778 if (!inputOptions.isEmpty()) {
779 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
780 "-hide_banner -loglevel warning " + inputOptions, rtspUri,
781 cameraConfig.getHlsOutOptions(), cameraConfig.getFfmpegOutput() + "ipcamera.m3u8",
782 cameraConfig.getUser(), cameraConfig.getPassword());
784 ffmpegHLS = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
785 "-hide_banner -loglevel warning", rtspUri, cameraConfig.getHlsOutOptions(),
786 cameraConfig.getFfmpegOutput() + "ipcamera.m3u8", cameraConfig.getUser(),
787 cameraConfig.getPassword());
790 Ffmpeg localHLS = ffmpegHLS;
791 if (localHLS != null) {
792 localHLS.startConverting();
796 if (cameraConfig.getGifPreroll() > 0) {
797 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(),
798 "-y -r 1 -hide_banner -loglevel warning", cameraConfig.getFfmpegOutput() + "snapshot%d.jpg",
799 "-frames:v " + (cameraConfig.getGifPreroll() + gifRecordTime) + " "
800 + cameraConfig.getGifOutOptions(),
801 cameraConfig.getFfmpegOutput() + gifFilename + ".gif", cameraConfig.getUser(),
802 cameraConfig.getPassword());
804 if (!inputOptions.isEmpty()) {
805 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning " + inputOptions;
807 inputOptions = "-y -t " + gifRecordTime + " -hide_banner -loglevel warning";
809 ffmpegGIF = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
810 cameraConfig.getGifOutOptions(), cameraConfig.getFfmpegOutput() + gifFilename + ".gif",
811 cameraConfig.getUser(), cameraConfig.getPassword());
813 if (cameraConfig.getGifPreroll() > 0) {
816 Ffmpeg localGIF = ffmpegGIF;
817 if (localGIF != null) {
818 localGIF.startConverting();
819 if (gifHistory.isEmpty()) {
820 gifHistory = gifFilename;
821 } else if (!"ipcamera".equals(gifFilename)) {
822 gifHistory = gifFilename + "," + gifHistory;
823 if (gifHistoryLength > 49) {
824 int endIndex = gifHistory.lastIndexOf(",");
825 gifHistory = gifHistory.substring(0, endIndex);
828 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
832 if (!inputOptions.isEmpty()) {
833 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning " + inputOptions;
835 inputOptions = "-y -t " + mp4RecordTime + " -hide_banner -loglevel warning";
837 ffmpegRecord = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
838 cameraConfig.getMp4OutOptions(), cameraConfig.getFfmpegOutput() + mp4Filename + ".mp4",
839 cameraConfig.getUser(), cameraConfig.getPassword());
840 Ffmpeg localRecord = ffmpegRecord;
841 if (localRecord != null) {
842 localRecord.startConverting();
843 if (mp4History.isEmpty()) {
844 mp4History = mp4Filename;
845 } else if (!"ipcamera".equals(mp4Filename)) {
846 mp4History = mp4Filename + "," + mp4History;
847 if (mp4HistoryLength > 49) {
848 int endIndex = mp4History.lastIndexOf(",");
849 mp4History = mp4History.substring(0, endIndex);
853 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
856 Ffmpeg localAlarms = ffmpegRtspHelper;
857 if (localAlarms != null) {
858 localAlarms.stopConverting();
859 if (!audioAlarmEnabled && !motionAlarmEnabled) {
863 String input = (cameraConfig.getAlarmInputUrl().isEmpty()) ? rtspUri : cameraConfig.getAlarmInputUrl();
864 String filterOptions = "";
865 if (!audioAlarmEnabled) {
866 filterOptions = "-an";
868 filterOptions = "-af silencedetect=n=-" + audioThreshold + "dB:d=2";
870 if (!motionAlarmEnabled && !ffmpegSnapshotGeneration) {
871 filterOptions = filterOptions.concat(" -vn");
872 } else if (motionAlarmEnabled && !cameraConfig.getMotionOptions().isEmpty()) {
873 String usersMotionOptions = cameraConfig.getMotionOptions();
874 if (usersMotionOptions.startsWith("-")) {
875 // Need to put the users custom options first in the chain before the motion is detected
876 filterOptions += " " + usersMotionOptions + ",select='gte(scene,"
877 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print";
879 filterOptions = filterOptions + " " + usersMotionOptions + " -vf select='gte(scene,"
880 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print";
882 } else if (motionAlarmEnabled) {
883 filterOptions = filterOptions.concat(" -vf select='gte(scene,"
884 + motionThreshold.divide(BIG_DECIMAL_SCALE_MOTION) + ")',metadata=print");
886 ffmpegRtspHelper = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, input,
887 filterOptions, "-f null -", cameraConfig.getUser(), cameraConfig.getPassword());
888 localAlarms = ffmpegRtspHelper;
889 if (localAlarms != null) {
890 localAlarms.startConverting();
894 if (ffmpegMjpeg == null) {
895 if (inputOptions.isEmpty()) {
896 inputOptions = "-hide_banner -loglevel warning";
898 inputOptions += " -hide_banner -loglevel warning";
900 ffmpegMjpeg = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
901 cameraConfig.getMjpegOptions(), "http://127.0.0.1:" + SERVLET_PORT + "/ipcamera/"
902 + getThing().getUID().getId() + "/ipcamera.jpg",
903 cameraConfig.getUser(), cameraConfig.getPassword());
905 Ffmpeg localMjpeg = ffmpegMjpeg;
906 if (localMjpeg != null) {
907 localMjpeg.startConverting();
911 // if mjpeg stream you can use 'ffmpeg -i input -codec:v copy -bsf:v mjpeg2jpeg output.jpg'
912 if (ffmpegSnapshot == null) {
913 if (inputOptions.isEmpty()) {
915 inputOptions = "-threads 1 -skip_frame nokey -hide_banner -loglevel warning";
917 inputOptions += " -threads 1 -skip_frame nokey -hide_banner -loglevel warning";
919 ffmpegSnapshot = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
920 cameraConfig.getSnapshotOptions(), "http://127.0.0.1:" + SERVLET_PORT + "/ipcamera/"
921 + getThing().getUID().getId() + "/snapshot.jpg",
922 cameraConfig.getUser(), cameraConfig.getPassword());
924 Ffmpeg localSnaps = ffmpegSnapshot;
925 if (localSnaps != null) {
926 localSnaps.startConverting();
932 public void noMotionDetected(String thisAlarmsChannel) {
933 setChannelState(thisAlarmsChannel, OnOffType.OFF);
934 firstMotionAlarm = false;
935 motionAlarmUpdateSnapshot = false;
936 motionDetected = false;
937 if (streamingAutoFps) {
938 stopSnapshotPolling();
939 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
940 stopSnapshotPolling();
945 * The {@link changeAlarmState} To only be used to change alarms channels that are not counted as motion. This will
946 * allow logic to be added here in the future. Example more than 1 type of alarm may indicate that someone is
947 * tampering with the camera.
949 public void changeAlarmState(String thisAlarmsChannel, OnOffType state) {
950 updateState(thisAlarmsChannel, state);
953 public void motionDetected(String thisAlarmsChannel) {
954 updateState(CHANNEL_LAST_MOTION_TYPE, new StringType(thisAlarmsChannel));
955 updateState(thisAlarmsChannel, OnOffType.ON);
956 motionDetected = true;
957 if (streamingAutoFps) {
958 startSnapshotPolling();
960 if (cameraConfig.getUpdateImageWhen().contains("2")) {
961 if (!firstMotionAlarm) {
962 if (!snapshotUri.isEmpty()) {
965 firstMotionAlarm = true;// reset back to false when the jpg arrives.
967 } else if (cameraConfig.getUpdateImageWhen().contains("4")) { // During Motion Alarms
968 if (!snapshotPolling) {
969 startSnapshotPolling();
971 firstMotionAlarm = true;
972 motionAlarmUpdateSnapshot = true;
976 public void audioDetected() {
977 updateState(CHANNEL_AUDIO_ALARM, OnOffType.ON);
978 if (cameraConfig.getUpdateImageWhen().contains("3")) {
979 if (!firstAudioAlarm) {
980 if (!snapshotUri.isEmpty()) {
983 firstAudioAlarm = true;// reset back to false when the jpg arrives.
985 } else if (cameraConfig.getUpdateImageWhen().contains("5")) {// During audio alarms
986 firstAudioAlarm = true;
987 audioAlarmUpdateSnapshot = true;
991 public void noAudioDetected() {
992 setChannelState(CHANNEL_AUDIO_ALARM, OnOffType.OFF);
993 firstAudioAlarm = false;
994 audioAlarmUpdateSnapshot = false;
997 public void recordMp4(String filename, int seconds) {
998 mp4Filename = filename;
999 mp4RecordTime = seconds;
1000 setupFfmpegFormat(FFmpegFormat.RECORD);
1001 setChannelState(CHANNEL_RECORDING_MP4, DecimalType.valueOf(new String("" + seconds)));
1004 public void recordGif(String filename, int seconds) {
1005 gifFilename = filename;
1006 gifRecordTime = seconds;
1007 if (cameraConfig.getGifPreroll() > 0) {
1008 snapCount = seconds;
1010 setupFfmpegFormat(FFmpegFormat.GIF);
1012 setChannelState(CHANNEL_RECORDING_GIF, DecimalType.valueOf(new String("" + seconds)));
1015 public String returnValueFromString(String rawString, String searchedString) {
1017 int index = rawString.indexOf(searchedString);
1018 if (index != -1) // -1 means "not found"
1020 result = rawString.substring(index + searchedString.length(), rawString.length());
1021 index = result.indexOf("\r\n"); // find a carriage return to find the end of the value.
1023 return result; // Did not find a carriage return.
1025 return result.substring(0, index);
1028 return ""; // Did not find the String we were searching for
1031 private void sendPTZRequest() {
1032 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.AbsoluteMove);
1036 public void channelLinked(ChannelUID channelUID) {
1037 switch (channelUID.getId()) {
1038 case CHANNEL_MJPEG_URL:
1039 updateState(CHANNEL_MJPEG_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1040 + getThing().getUID().getId() + "/ipcamera.mjpeg"));
1042 case CHANNEL_HLS_URL:
1043 updateState(CHANNEL_HLS_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1044 + getThing().getUID().getId() + "/ipcamera.m3u8"));
1046 case CHANNEL_IMAGE_URL:
1047 updateState(CHANNEL_IMAGE_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
1048 + getThing().getUID().getId() + "/ipcamera.jpg"));
1054 public void handleCommand(ChannelUID channelUID, Command command) {
1055 if (command instanceof RefreshType) {
1056 switch (channelUID.getId()) {
1058 if (onvifCamera.supportsPTZ()) {
1059 updateState(CHANNEL_PAN, new PercentType(Math.round(onvifCamera.getAbsolutePan())));
1063 if (onvifCamera.supportsPTZ()) {
1064 updateState(CHANNEL_TILT, new PercentType(Math.round(onvifCamera.getAbsoluteTilt())));
1068 if (onvifCamera.supportsPTZ()) {
1069 updateState(CHANNEL_ZOOM, new PercentType(Math.round(onvifCamera.getAbsoluteZoom())));
1072 case CHANNEL_GOTO_PRESET:
1073 if (onvifCamera.supportsPTZ()) {
1074 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.GetPresets);
1078 } // caution "REFRESH" can still progress to brand Handlers below the else.
1080 switch (channelUID.getId()) {
1081 case CHANNEL_MP4_HISTORY_LENGTH:
1082 if (DecimalType.ZERO.equals(command)) {
1083 mp4HistoryLength = 0;
1085 setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
1088 case CHANNEL_GIF_HISTORY_LENGTH:
1089 if (DecimalType.ZERO.equals(command)) {
1090 gifHistoryLength = 0;
1092 setChannelState(CHANNEL_GIF_HISTORY, new StringType(gifHistory));
1095 case CHANNEL_FFMPEG_MOTION_CONTROL:
1096 if (OnOffType.ON.equals(command)) {
1097 motionAlarmEnabled = true;
1098 } else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
1099 motionAlarmEnabled = false;
1100 noMotionDetected(CHANNEL_FFMPEG_MOTION_ALARM);
1101 } else if (command instanceof PercentType) {
1102 motionAlarmEnabled = true;
1103 motionThreshold = ((PercentType) command).toBigDecimal();
1105 setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
1107 case CHANNEL_START_STREAM:
1109 if (OnOffType.ON.equals(command)) {
1110 localHLS = ffmpegHLS;
1111 if (localHLS == null) {
1112 setupFfmpegFormat(FFmpegFormat.HLS);
1113 localHLS = ffmpegHLS;
1115 if (localHLS != null) {
1116 localHLS.setKeepAlive(-1);// Now will run till manually stopped.
1117 localHLS.startConverting();
1120 localHLS = ffmpegHLS;
1121 if (localHLS != null) {
1122 // Still runs but will be able to auto stop when the HLS stream is no longer used.
1123 localHLS.setKeepAlive(1);
1127 case CHANNEL_EXTERNAL_MOTION:
1128 if (OnOffType.ON.equals(command)) {
1129 motionDetected(CHANNEL_EXTERNAL_MOTION);
1131 noMotionDetected(CHANNEL_EXTERNAL_MOTION);
1134 case CHANNEL_GOTO_PRESET:
1135 if (onvifCamera.supportsPTZ()) {
1136 onvifCamera.gotoPreset(Integer.valueOf(command.toString()));
1139 case CHANNEL_POLL_IMAGE:
1140 if (OnOffType.ON.equals(command)) {
1141 if (snapshotUri.isEmpty()) {
1142 ffmpegSnapshotGeneration = true;
1143 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1144 updateImageChannel = false;
1146 updateImageChannel = true;
1147 updateSnapshot();// Allows this to change Image FPS on demand
1150 Ffmpeg localSnaps = ffmpegSnapshot;
1151 if (localSnaps != null) {
1152 localSnaps.stopConverting();
1153 ffmpegSnapshotGeneration = false;
1155 updateImageChannel = false;
1159 if (onvifCamera.supportsPTZ()) {
1160 if (command instanceof IncreaseDecreaseType) {
1161 if (command == IncreaseDecreaseType.INCREASE) {
1162 if (cameraConfig.getPtzContinuous()) {
1163 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveLeft);
1165 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveLeft);
1168 if (cameraConfig.getPtzContinuous()) {
1169 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveRight);
1171 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveRight);
1175 } else if (OnOffType.OFF.equals(command)) {
1176 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1179 onvifCamera.setAbsolutePan(Float.valueOf(command.toString()));
1180 mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1184 if (onvifCamera.supportsPTZ()) {
1185 if (command instanceof IncreaseDecreaseType) {
1186 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1187 if (cameraConfig.getPtzContinuous()) {
1188 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveUp);
1190 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveUp);
1193 if (cameraConfig.getPtzContinuous()) {
1194 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveDown);
1196 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveDown);
1200 } else if (OnOffType.OFF.equals(command)) {
1201 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1204 onvifCamera.setAbsoluteTilt(Float.valueOf(command.toString()));
1205 mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1209 if (onvifCamera.supportsPTZ()) {
1210 if (command instanceof IncreaseDecreaseType) {
1211 if (IncreaseDecreaseType.INCREASE.equals(command)) {
1212 if (cameraConfig.getPtzContinuous()) {
1213 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveIn);
1215 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveIn);
1218 if (cameraConfig.getPtzContinuous()) {
1219 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.ContinuousMoveOut);
1221 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.RelativeMoveOut);
1225 } else if (OnOffType.OFF.equals(command)) {
1226 onvifCamera.sendPTZRequest(OnvifConnection.RequestType.Stop);
1229 onvifCamera.setAbsoluteZoom(Float.valueOf(command.toString()));
1230 mainEventLoopGroup.schedule(this::sendPTZRequest, 500, TimeUnit.MILLISECONDS);
1235 // commands and refresh now get passed to brand handlers
1236 switch (thing.getThingTypeUID().getId()) {
1238 AmcrestHandler amcrestHandler = new AmcrestHandler(getHandle());
1239 amcrestHandler.handleCommand(channelUID, command);
1240 if (lowPriorityRequests.isEmpty()) {
1241 lowPriorityRequests = amcrestHandler.getLowPriorityRequests();
1245 DahuaHandler dahuaHandler = new DahuaHandler(getHandle(), cameraConfig.getNvrChannel());
1246 dahuaHandler.handleCommand(channelUID, command);
1247 if (lowPriorityRequests.isEmpty()) {
1248 lowPriorityRequests = dahuaHandler.getLowPriorityRequests();
1251 case DOORBIRD_THING:
1252 DoorBirdHandler doorBirdHandler = new DoorBirdHandler(getHandle());
1253 doorBirdHandler.handleCommand(channelUID, command);
1254 if (lowPriorityRequests.isEmpty()) {
1255 lowPriorityRequests = doorBirdHandler.getLowPriorityRequests();
1258 case HIKVISION_THING:
1259 HikvisionHandler hikvisionHandler = new HikvisionHandler(getHandle(), cameraConfig.getNvrChannel());
1260 hikvisionHandler.handleCommand(channelUID, command);
1261 if (lowPriorityRequests.isEmpty()) {
1262 lowPriorityRequests = hikvisionHandler.getLowPriorityRequests();
1266 FoscamHandler foscamHandler = new FoscamHandler(getHandle(), cameraConfig.getUser(),
1267 cameraConfig.getPassword());
1268 foscamHandler.handleCommand(channelUID, command);
1269 if (lowPriorityRequests.isEmpty()) {
1270 lowPriorityRequests = foscamHandler.getLowPriorityRequests();
1274 InstarHandler instarHandler = new InstarHandler(getHandle());
1275 instarHandler.handleCommand(channelUID, command);
1276 if (lowPriorityRequests.isEmpty()) {
1277 lowPriorityRequests = instarHandler.getLowPriorityRequests();
1281 HttpOnlyHandler defaultHandler = new HttpOnlyHandler(getHandle());
1282 defaultHandler.handleCommand(channelUID, command);
1283 if (lowPriorityRequests.isEmpty()) {
1284 lowPriorityRequests = defaultHandler.getLowPriorityRequests();
1290 public void setChannelState(String channelToUpdate, State valueOf) {
1291 updateState(channelToUpdate, valueOf);
1294 private void bringCameraOnline() {
1296 updateStatus(ThingStatus.ONLINE);
1297 groupTracker.listOfOnlineCameraHandlers.add(this);
1298 groupTracker.listOfOnlineCameraUID.add(getThing().getUID().getId());
1299 Future<?> localFuture = cameraConnectionJob;
1300 if (localFuture != null) {
1301 localFuture.cancel(false);
1302 cameraConnectionJob = null;
1304 if (!snapshotUri.isEmpty()) {
1305 if (cameraConfig.getGifPreroll() > 0 || cameraConfig.getUpdateImageWhen().contains("1")) {
1306 snapshotPolling = true;
1307 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 1000,
1308 cameraConfig.getPollTime(), TimeUnit.MILLISECONDS);
1312 pollCameraJob = threadPool.scheduleWithFixedDelay(this::pollCameraRunnable, 1000, 8000, TimeUnit.MILLISECONDS);
1314 if (!rtspUri.isEmpty()) {
1315 updateState(CHANNEL_RTSP_URL, new StringType(rtspUri));
1317 if (updateImageChannel) {
1318 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1320 updateState(CHANNEL_POLL_IMAGE, OnOffType.OFF);
1322 if (!groupTracker.listOfGroupHandlers.isEmpty()) {
1323 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1324 handle.cameraOnline(getThing().getUID().getId());
1329 void snapshotIsFfmpeg() {
1330 snapshotUri = "";// ffmpeg is a valid option. Simplify further checks.
1332 "Binding has no snapshot url. Will use your CPU and FFmpeg to create snapshots from the cameras RTSP.");
1333 bringCameraOnline();
1334 if (!rtspUri.isEmpty()) {
1335 updateImageChannel = false;
1336 ffmpegSnapshotGeneration = true;
1337 setupFfmpegFormat(FFmpegFormat.SNAPSHOT);
1338 updateState(CHANNEL_POLL_IMAGE, OnOffType.ON);
1340 cameraConfigError("Binding can not find a RTSP url for this camera, please provide a FFmpeg Input URL.");
1344 void pollingCameraConnection() {
1345 if (thing.getThingTypeUID().getId().equals(GENERIC_THING)) {
1346 if (rtspUri.isEmpty()) {
1347 logger.warn("Binding has not been supplied with a FFmpeg Input URL, so some features will not work.");
1349 if (snapshotUri.isEmpty() || "ffmpeg".equals(snapshotUri)) {
1356 if (!onvifCamera.isConnected()) {
1357 logger.debug("About to connect to the IP Camera using the ONVIF PORT at IP:{}:{}", cameraConfig.getIp(),
1358 cameraConfig.getOnvifPort());
1359 onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1361 if ("ffmpeg".equals(snapshotUri)) {
1363 } else if (!snapshotUri.isEmpty()) {
1365 } else if (!rtspUri.isEmpty()) {
1368 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
1369 "Camera failed to report a valid Snaphot and/or RTSP URL. See readme on how to use the SNAPSHOT_URL_OVERRIDE feature.");
1373 public void cameraConfigError(String reason) {
1374 // wont try to reconnect again due to a config error being the cause.
1375 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, reason);
1379 public void cameraCommunicationError(String reason) {
1380 // will try to reconnect again as camera may be rebooting.
1381 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
1382 if (isOnline) {// if already offline dont try reconnecting in 6 seconds, we want 30sec wait.
1383 resetAndRetryConnecting();
1387 boolean streamIsStopped(String url) {
1388 ChannelTracking channelTracking = channelTrackingMap.get(url);
1389 if (channelTracking != null) {
1390 if (channelTracking.getChannel().isActive()) {
1391 return false; // stream is running.
1394 return true; // Stream stopped or never started.
1397 void snapshotRunnable() {
1398 // Snapshot should be first to keep consistent time between shots
1400 if (snapCount > 0) {
1401 if (--snapCount == 0) {
1402 setupFfmpegFormat(FFmpegFormat.GIF);
1407 private void takeSnapshot() {
1408 sendHttpGET(snapshotUri);
1411 private void updateSnapshot() {
1412 lastSnapshotRequest = Instant.now();
1413 mainEventLoopGroup.schedule(this::takeSnapshot, 0, TimeUnit.MILLISECONDS);
1416 public byte[] getSnapshot() {
1418 // Single gray pixel JPG to keep streams open when the camera goes offline so they dont stop.
1419 return new byte[] { (byte) 0xff, (byte) 0xd8, (byte) 0xff, (byte) 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46,
1420 0x00, 0x01, 0x01, 0x01, 0x00, 0x48, 0x00, 0x48, 0x00, 0x00, (byte) 0xff, (byte) 0xdb, 0x00, 0x43,
1421 0x00, 0x03, 0x02, 0x02, 0x02, 0x02, 0x02, 0x03, 0x02, 0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x04,
1422 0x06, 0x04, 0x04, 0x04, 0x04, 0x04, 0x08, 0x06, 0x06, 0x05, 0x06, 0x09, 0x08, 0x0a, 0x0a, 0x09,
1423 0x08, 0x09, 0x09, 0x0a, 0x0c, 0x0f, 0x0c, 0x0a, 0x0b, 0x0e, 0x0b, 0x09, 0x09, 0x0d, 0x11, 0x0d,
1424 0x0e, 0x0f, 0x10, 0x10, 0x11, 0x10, 0x0a, 0x0c, 0x12, 0x13, 0x12, 0x10, 0x13, 0x0f, 0x10, 0x10,
1425 0x10, (byte) 0xff, (byte) 0xc9, 0x00, 0x0b, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00,
1426 (byte) 0xff, (byte) 0xcc, 0x00, 0x06, 0x00, 0x10, 0x10, 0x05, (byte) 0xff, (byte) 0xda, 0x00, 0x08,
1427 0x01, 0x01, 0x00, 0x00, 0x3f, 0x00, (byte) 0xd2, (byte) 0xcf, 0x20, (byte) 0xff, (byte) 0xd9 };
1429 // Most cameras will return a 503 busy error if snapshot is faster than 1 second
1430 long lastUpdatedMs = Duration.between(lastSnapshotRequest, Instant.now()).toMillis();
1431 if (!snapshotPolling && !ffmpegSnapshotGeneration && lastUpdatedMs >= cameraConfig.getPollTime()) {
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 || cameraConfig.getUpdateImageWhen().contains("4")) {
1465 snapshotPolling = true;
1466 snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 0, cameraConfig.getPollTime(),
1467 TimeUnit.MILLISECONDS);
1472 * {@link pollCameraRunnable} Polls every 8 seconds, to check camera is still ONLINE and keep alarm
1473 * streams open and more.
1476 void pollCameraRunnable() {
1477 // NOTE: Use lowPriorityRequests if get request is not needed every poll.
1478 if (!lowPriorityRequests.isEmpty()) {
1479 if (lowPriorityCounter >= lowPriorityRequests.size()) {
1480 lowPriorityCounter = 0;
1482 sendHttpGET(lowPriorityRequests.get(lowPriorityCounter++));
1484 // what needs to be done every poll//
1485 switch (thing.getThingTypeUID().getId()) {
1487 if (!snapshotUri.isEmpty() && !snapshotPolling) {
1488 checkCameraConnection();
1490 // RTSP stream has stopped and we need it for snapshots
1491 if (ffmpegSnapshotGeneration) {
1492 Ffmpeg localSnapshot = ffmpegSnapshot;
1493 if (localSnapshot != null && !localSnapshot.getIsAlive()) {
1494 localSnapshot.startConverting();
1499 if (!snapshotPolling) {
1500 checkCameraConnection();
1502 if (!onvifCamera.isConnected()) {
1503 onvifCamera.connect(true);
1507 if (!snapshotPolling) {
1508 checkCameraConnection();
1510 noMotionDetected(CHANNEL_MOTION_ALARM);
1511 noMotionDetected(CHANNEL_PIR_ALARM);
1514 case HIKVISION_THING:
1515 if (streamIsStopped("/ISAPI/Event/notification/alertStream")) {
1516 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1517 cameraConfig.getIp());
1518 sendHttpGET("/ISAPI/Event/notification/alertStream");
1522 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=VideoMotion");
1523 sendHttpGET("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=AudioMutation");
1526 if (!snapshotPolling) {
1527 checkCameraConnection();
1529 // Check for alarms, channel for NVRs appears not to work at filtering.
1530 if (streamIsStopped("/cgi-bin/eventManager.cgi?action=attach&codes=[All]")) {
1531 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1532 cameraConfig.getIp());
1533 sendHttpGET("/cgi-bin/eventManager.cgi?action=attach&codes=[All]");
1536 case DOORBIRD_THING:
1537 if (!snapshotPolling) {
1538 checkCameraConnection();
1540 // Check for alarms, channel for NVRs appears not to work at filtering.
1541 if (streamIsStopped("/bha-api/monitor.cgi?ring=doorbell,motionsensor")) {
1542 logger.info("The alarm stream was not running for camera {}, re-starting it now",
1543 cameraConfig.getIp());
1544 sendHttpGET("/bha-api/monitor.cgi?ring=doorbell,motionsensor");
1548 sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=getDevState&usr=" + cameraConfig.getUser() + "&pwd="
1549 + cameraConfig.getPassword());
1552 Ffmpeg localHLS = ffmpegHLS;
1553 if (localHLS != null) {
1554 localHLS.checkKeepAlive();
1556 if (openChannels.size() > 10) {
1557 logger.debug("There are {} open Channels being tracked.", openChannels.size());
1563 public void initialize() {
1564 cameraConfig = getConfigAs(CameraConfig.class);
1565 threadPool = Executors.newScheduledThreadPool(2);
1566 mainEventLoopGroup = new NioEventLoopGroup(3);
1567 snapshotUri = getCorrectUrlFormat(cameraConfig.getSnapshotUrl());
1568 mjpegUri = getCorrectUrlFormat(cameraConfig.getMjpegUrl());
1569 rtspUri = cameraConfig.getFfmpegInput();
1570 if (cameraConfig.getFfmpegOutput().isEmpty()) {
1572 .setFfmpegOutput(OpenHAB.getUserDataFolder() + "/ipcamera/" + this.thing.getUID().getId() + "/");
1574 // Known cameras will connect quicker if we skip ONVIF questions.
1575 switch (thing.getThingTypeUID().getId()) {
1578 if (mjpegUri.isEmpty()) {
1579 mjpegUri = "/cgi-bin/mjpg/video.cgi?channel=" + cameraConfig.getNvrChannel() + "&subtype=1";
1581 if (snapshotUri.isEmpty()) {
1582 snapshotUri = "/cgi-bin/snapshot.cgi?channel=" + cameraConfig.getNvrChannel();
1585 case DOORBIRD_THING:
1586 if (mjpegUri.isEmpty()) {
1587 mjpegUri = "/bha-api/video.cgi";
1589 if (snapshotUri.isEmpty()) {
1590 snapshotUri = "/bha-api/image.cgi";
1594 // Foscam needs any special char like spaces (%20) to be encoded for URLs.
1595 cameraConfig.setUser(Helper.encodeSpecialChars(cameraConfig.getUser()));
1596 cameraConfig.setPassword(Helper.encodeSpecialChars(cameraConfig.getPassword()));
1597 if (mjpegUri.isEmpty()) {
1598 mjpegUri = "/cgi-bin/CGIStream.cgi?cmd=GetMJStream&usr=" + cameraConfig.getUser() + "&pwd="
1599 + cameraConfig.getPassword();
1601 if (snapshotUri.isEmpty()) {
1602 snapshotUri = "/cgi-bin/CGIProxy.fcgi?usr=" + cameraConfig.getUser() + "&pwd="
1603 + cameraConfig.getPassword() + "&cmd=snapPicture2";
1606 case HIKVISION_THING:// The 02 gives you the first sub stream which needs to be set to MJPEG
1607 if (mjpegUri.isEmpty()) {
1608 mjpegUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "02" + "/httppreview";
1610 if (snapshotUri.isEmpty()) {
1611 snapshotUri = "/ISAPI/Streaming/channels/" + cameraConfig.getNvrChannel() + "01/picture";
1615 if (snapshotUri.isEmpty()) {
1616 snapshotUri = "/tmpfs/snap.jpg";
1618 if (mjpegUri.isEmpty()) {
1619 mjpegUri = "/mjpegstream.cgi?-chn=12";
1622 "/param.cgi?cmd=setmdalarm&-aname=server2&-switch=on&-interval=1&cmd=setalarmserverattr&-as_index=3&-as_server="
1623 + hostIp + "&-as_port=" + SERVLET_PORT + "&-as_path=/ipcamera/"
1624 + getThing().getUID().getId()
1625 + "/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");
1628 // for poll times 9 seconds and above don't display a warning about the Image channel.
1629 if (9000 > cameraConfig.getPollTime() && cameraConfig.getUpdateImageWhen().contains("1")) {
1631 "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.");
1633 // ONVIF and Instar event handling need the server started before connecting.
1634 startStreamServer();
1638 private void tryConnecting() {
1639 if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)) {
1640 onvifCamera = new OnvifConnection(this, cameraConfig.getIp() + ":" + cameraConfig.getOnvifPort(),
1641 cameraConfig.getUser(), cameraConfig.getPassword());
1642 onvifCamera.setSelectedMediaProfile(cameraConfig.getOnvifMediaProfile());
1643 // Only use ONVIF events if it is not an API camera.
1644 onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
1646 cameraConnectionJob = threadPool.scheduleWithFixedDelay(this::pollingCameraConnection, 4, 30, TimeUnit.SECONDS);
1649 // What the camera needs to re-connect if the initialize() is not called.
1650 private void resetAndRetryConnecting() {
1655 private void offline() {
1657 snapshotPolling = false;
1658 Future<?> localFuture = pollCameraJob;
1659 if (localFuture != null) {
1660 localFuture.cancel(true);
1663 localFuture = snapshotJob;
1664 if (localFuture != null) {
1665 localFuture.cancel(true);
1668 localFuture = cameraConnectionJob;
1669 if (localFuture != null) {
1670 localFuture.cancel(true);
1673 Ffmpeg localFfmpeg = ffmpegHLS;
1674 if (localFfmpeg != null) {
1675 localFfmpeg.stopConverting();
1678 localFfmpeg = ffmpegRecord;
1679 if (localFfmpeg != null) {
1680 localFfmpeg.stopConverting();
1681 ffmpegRecord = null;
1683 localFfmpeg = ffmpegGIF;
1684 if (localFfmpeg != null) {
1685 localFfmpeg.stopConverting();
1688 localFfmpeg = ffmpegRtspHelper;
1689 if (localFfmpeg != null) {
1690 localFfmpeg.stopConverting();
1691 ffmpegRtspHelper = null;
1693 localFfmpeg = ffmpegMjpeg;
1694 if (localFfmpeg != null) {
1695 localFfmpeg.stopConverting();
1698 localFfmpeg = ffmpegSnapshot;
1699 if (localFfmpeg != null) {
1700 localFfmpeg.stopConverting();
1701 ffmpegSnapshot = null;
1703 onvifCamera.disconnect();
1704 openChannels.close();
1708 public void dispose() {
1710 CameraServlet localServlet = servlet;
1711 if (localServlet != null) {
1712 localServlet.dispose();
1713 localServlet = null;
1715 threadPool.shutdown();
1716 // inform all group handlers that this camera has gone offline
1717 groupTracker.listOfOnlineCameraHandlers.remove(this);
1718 groupTracker.listOfOnlineCameraUID.remove(getThing().getUID().getId());
1719 for (IpCameraGroupHandler handle : groupTracker.listOfGroupHandlers) {
1720 handle.cameraOffline(this);
1722 basicAuth = ""; // clear out stored Password hash
1723 useDigestAuth = false;
1724 mainEventLoopGroup.shutdownGracefully();
1725 mainBootstrap = null;
1726 channelTrackingMap.clear();
1729 public String getWhiteList() {
1730 return cameraConfig.getIpWhitelist();
1734 public Collection<Class<? extends ThingHandlerService>> getServices() {
1735 return Collections.singleton(IpCameraActions.class);